如果要你写一个计算字符串长度的函数 strlen,应该怎么写?相信你很容易写出如下实现:

 1 int strlen_1(const char* str) {
2 int cnt = 0;
3
4 if (NULL == str) {
5 return 0;
6 }
7
8 while (*str != '\0') {
9 cnt++;
10 str++;
11 }
12 return cnt;
13 }

那么,它的运行情况怎么样?写段代码测试一下:

 1 const char* strs[] = {
2 NULL,
3 "",
4 "1",
5 "12",
6 "123",
7 "012345678901234567890"
8 "012345678901234567890"
9 "012345678901234567890"
10 "012345678901234567890"
11 "012345678901234567890"
12 "012345678901234567890"
13 "012345678901234567890"
14 "012345678901234567890"
15 "012345678901234567890"
16 "012345678901234567890"
17 };
18
19 int main()
20 {
21 int arrSize = sizeof(strs) / sizeof(char*);
22 for (int i = 0; i < arrSize; i++) {
23 printf("%5d: %10d\n", i, strlen_1(strs[i]));
24 }
25
26 return 0;
27 }

运行结果如下:

我们得到了正确结果,但是这样就够了吗?写代码,尤其是经常被调用的代码,效率是一个很重要的考虑方面,我们的 strlen_1 的效率如何呢?为了测试效率,我们测量一个100M 个字符的超长的字符串。编辑如下测试代码:

 1 typedef size_t(*pStrLen)(const char* str);
2 void testProf(
3 pStrLen sl,
4 const char* testName,
5 const char* str) {
6
7 long start = GetTickCount64();
8 long end = 0;
9
10 int len = sl(str);
11
12 end = GetTickCount64();
13
14 printf(
15 "%s, start: %ld, end: %ld, total: %ld, result: %d\n",
16 testName,
17 start,
18 end,
19 end - start,
20 len
21 );
22 }
23
24 void testLen(pStrLen sl, const char* name) {
25 int arrSize = sizeof(strs) / sizeof(char*);
26 puts("------------------------------------------");
27 puts(name);
28 puts("\n");
29
30 for (int i = 0; i < arrSize; i++) {
31 printf("%5d: %10d\n", i, strs[i] == NULL ? 0 : sl(strs[i]));
32 }
33 }

修改主程序如下:

// 100M
#define STR_SIZE 100000000
int main()
{
char* str = (char*)malloc(sizeof(char) * STR_SIZE); if (str == NULL) {
return -1;
} memset(str, 'a', STR_SIZE - 1);
str[STR_SIZE - 1] = '\0'; testLen(strlen_1, "strlen_1"); testProf(strlen_1, "strlen_1", str); free((void*)str); return 0;
}

得到结果如下(为了去除debug信息的影响,这里使用 release x86 编译,以下同):

耗时94ms,时间有点长啊,可以优化吗?考虑到我们只需要计算开始和结束地址之间的差,就得到了长度,那么如果省略计数变量,改成如下会不会好些?

 1 size_t strlen_2(const char* str) {
2 const char* eos = str;
3 if (NULL == eos) {
4 return 0;
5 }
6 while (*eos) {
7 eos++;
8 }
9 return (eos - str);
10 }

添加 strlen_2 的测试,修改主程序如下:

 1 // 100M
2 #define STR_SIZE 100000000
3 int main()
4 {
5 char* str = (char*)malloc(sizeof(char) * STR_SIZE);
6
7 if (str == NULL) {
8 return -1;
9 }
10
11 memset(str, 'a', STR_SIZE - 1);
12 str[STR_SIZE - 1] = '\0';
13
14 testLen(strlen_1, "strlen_1");
15 testLen(strlen_2, "strlen_2");
16
17 testProf(strlen_1, "strlen_1", str);
18 testProf(strlen_2, "strlen_2", str);
19
20 free((void*)str);
21
22 return 0;
23 }

运行一下,得到如下结果:

看起来有一些效果,但这就够了吗?那么系统自带的 strlen 函数效果怎么样呢?新增 strlen 的测试代码:

1   testLen(strlen_1, "strlen_1");
2 testLen(strlen_2, "strlen_2");
3 testLen(strlen, "strlen");
4
5 testProf(strlen_1, "strlen_1", str);
6 testProf(strlen_2, "strlen_2", str);
7 testProf(strlen, "strlen", str);

运行结果如下:

哇,居然快了4倍(63/15=4.2‬),那就要了解下系统自带strlen的实现了,经过查找,找到系统 strlen 的汇编代码如下:

 1         public  strlen
2
3 strlen proc \
4 buf:ptr byte
5
6 OPTION PROLOGUE:NONE, EPILOGUE:NONE
7
8 .FPO ( 0, 1, 0, 0, 0, 0 )
9
10 string equ [esp + 4]
11
12 mov ecx,string ; ecx -> string
13 test ecx,3 ; test if string is aligned on 32 bits
14 je short main_loop
15
16 str_misaligned:
17 ; simple byte loop until string is aligned
18 mov al,byte ptr [ecx]
19 add ecx,1
20 test al,al
21 je short byte_3
22 test ecx,3
23 jne short str_misaligned
24
25 add eax,dword ptr 0 ; 5 byte nop to align label below
26
27 align 16 ; should be redundant
28
29 main_loop:
30 mov eax,dword ptr [ecx] ; read 4 bytes
31 mov edx,7efefeffh
32 add edx,eax
33 xor eax,-1
34 xor eax,edx
35 add ecx,4
36 test eax,81010100h
37 je short main_loop
38 ; found zero byte in the loop
39 mov eax,[ecx - 4]
40 test al,al ; is it byte 0
41 je short byte_0
42 test ah,ah ; is it byte 1
43 je short byte_1
44 test eax,00ff0000h ; is it byte 2
45 je short byte_2
46 test eax,0ff000000h ; is it byte 3
47 je short byte_3
48 jmp short main_loop ; taken if bits 24-30 are clear and bit
49 ; 31 is set
50
51 byte_3:
52 lea eax,[ecx - 1]
53 mov ecx,string
54 sub eax,ecx
55 ret
56 byte_2:
57 lea eax,[ecx - 2]
58 mov ecx,string
59 sub eax,ecx
60 ret
61 byte_1:
62 lea eax,[ecx - 3]
63 mov ecx,string
64 sub eax,ecx
65 ret
66 byte_0:
67 lea eax,[ecx - 4]
68 mov ecx,string
69 sub eax,ecx
70 ret
71
72 strlen endp

简单说明如下:

12 - 14 行,判断ecx 指针是否4字节对齐,如果4字节对齐,就跳转到 主循环,否则就进入str_misaligned 循环;

16 - 23 行,逐字节读取字符并判断是否为 '\0',如果找到 '\0',就跳转到第 51 行(byte_3),计算地址差(即为字符串长度),并返回;如果没有找到 '\0' 字符并且地址已经四字节对齐,就继续执行主循环(29行);

29 - 49 行,是程序主循环,逻辑可用 C 描述为:

 1   // 已经32位对齐
2 int* eos = (int*)c;
3 int val = 0;
4 while (true) {
5 val = *eos;
6 int ad = val + 0x7efefeff;
7 val ^= -1; // 0b 1111 1111 1111 1111 1111 1111 1111 1111
8 val ^= ad;
9 eos++;
10 if (!(val & 0x81010100)) {
11 continue;
12 }
13 val = *(eos - 1);
14 if ((val & 0x000000ff) == 0) {
15 return (int)eos - (int)str - 4;
16 }
17
18 if ((val & 0x0000ff00) == 0) {
19 return ((int)eos - (int)str) - 3;
20 }
21
22 if ((val & 0x00ff0000) == 0) {
23 return ((int)eos - (int)str) - 2;
24 }
25
26 if ((val & 0xff000000) == 0) {
27 return ((int)eos - (int)str) - 1;
28 }
29 // taken if bits 24-30 are clear and bit 31 is set
30 }

其中,每次读取,均读取四字节,且一次性进行是否包含 '\0' 的判断,减少操作次数位逐个字节读取的 1/4,怪不得速度上也是快了四倍左右。

那么,系统strlen是怎样一次判断四个字节呢?我们注意到两个特殊值,0x7efefeff 和 0x81010100,那么为什么可以用这两个值判断是否包含 '\0' 呢?我们看看这两个值得二进制表示:

我们看看第一步操作:

1 int ad = val + 0x7efefeff;

我们把四个字节和 0x7efefeff 这个值相加了,如果 val 的最后一个字节不为0,则会向上一个字节产生一个进位,从而导致 ad 的倒数第二个字节的最后一位不为0,则倒数第二个字节就会变成 1111 1111 的状态,第二个字节同理,如果不为0,则会补充倒数第三个字节,最后,倒数第三个字节又会补充第一个字节;这就导致,在每个字节都不为 0 的前提下,ad 每个字节的最低位肯定和 0x7efefeff 与 val 值相加对应位的本应值相反(因为产生了进位,如果当前字节相加结果的最低位为1,则因为上一个字节的进位,则最低位会变成0,如果结果的最低位为0,则因为进位,最低位为1);

我们再看第二步,val值异或 -1,这里实际上是将 val 值得各个位取反,然后再用 val 值得取反结果异或 ad; 从上一步分析我们可以知道,如果第一步从字符串取到的 4 个字节均不为 0,则经过操作,ad对应字节的最低位肯定和原始值相反,这里拿 val 值的取反结果异或 ad,则在四字节均不为 0 的情况下,各个字节的最低位肯定为0;

最后一步,拿第二步获取到的结果和 0x81010100 相与(test),则因为上一步获取到的值最低位在取到四字节均不为0的情况下,最低位肯定为 0,所以如果 val & 0x81010100 为 0,则说明四字节均不为0(即'\0');

其他步骤就好说了,读取四字节,并一次判断各个字节的值是否为 0,如果为 0,则计算结果并返回。

最后,编辑 strlen_3 如下:

 1 size_t __cdecl strlen_3(const char* str) {
2 if (NULL == str) {
3 return 0;
4 }
5
6 const char* c = str;
7
8 while (((int)c) & 3) {
9 if (*c == '\0') {
10 return c - str;
11 }
12 c++;
13 }
14
15 // 已经32位对齐
16 int* eos = (int*)c;
17 int val = 0;
18 while (true) {
19 val = *eos;
20 int ad = val + 0x7efefeff;
21 val ^= -1; // 0b 1111 1111 1111 1111 1111 1111 1111 1111
22 val ^= ad;
23 eos++;
24 if (!(val & 0x81010100)) {
25 continue;
26 }
27 val = *(eos - 1);
28 if ((val & 0x000000ff) == 0) {
29 return (int)eos - (int)str - 4;
30 }
31
32 if ((val & 0x0000ff00) == 0) {
33 return ((int)eos - (int)str) - 3;
34 }
35
36 if ((val & 0x00ff0000) == 0) {
37 return ((int)eos - (int)str) - 2;
38 }
39
40 if ((val & 0xff000000) == 0) {
41 return ((int)eos - (int)str) - 1;
42 }
43 // taken if bits 24-30 are clear and bit 31 is set
44 }
45 }

添加并执行测试代码,结果如下:

可以看到,新版本的 strlen 运行时间已经和系统 strlen 一样级别了。

最后,我们再考虑下,这里用的是 32 位系统,如果在 64 位系统上,是否也可以用类似方法呢?答案是肯定的,而且事实上,strlen 的 64 位版本也是这么做的:

可以看到,这里使用的方法和 32 位是一样的,只不过位数增加了。

为了效率,我们可以用的招数 之 strlen的更多相关文章

  1. php效率优化

    php效率优化 最近在公司一边自学一边写PHP程序,由于公司对程序的运行效率要求很高,而自己又是个新手,一开始就注意程序的效率很重要,这里就结合网上的一些资料,总结下php程序效率优化的一些策略:1. ...

  2. strlen源码剖析(可查看glibc和VC的CRT源代码)

    学习高效编程的有效途径之一就是阅读高手写的源代码,CRT(C/C++ Runtime Library)作为底层的函数库,实现必然高效.恰好手中就有glibc和VC的CRT源代码,于是挑了一个相对简单的 ...

  3. 【DDD】领域驱动设计实践 —— 业务建模小招数

    本文结合团队在ECO(社区服务系统)业务建模过程中的实践经验,总结得到一些DDD业务建模的小招数,不一定是完美的,但是对我们团队来说很有效用,希望能帮到其他人.后面会陆续将项目中业务建模的一些经典例子 ...

  4. 测试一下StringBuffer和StringBuilder及字面常量拼接三种字符串的效率

    之前一篇里写过字符串常用类的三种方式<java中的字符串相关知识整理>,只不过这个只是分析并不知道他们之间会有多大的区别,或者所谓的StringBuffer能提升多少拼接效率呢?为此写个简 ...

  5. 关于如何提高Web服务端并发效率的异步编程技术

    最近我研究技术的一个重点是java的多线程开发,在我早期学习java的时候,很多书上把java的多线程开发标榜为简单易用,这个简单易用是以C语言作为参照的,不过我也没有使用过C语言开发过多线程,我只知 ...

  6. 要想提高PHP的编程效率,你必须知道的要点

    1.当操作字符串并需要检验其长度是否满足某种要求时,你想当然地会使用strlen()函数.此函数执行起来相当快,因为它不做任何计算,只返回在zval 结构(C的内置数据结构,用于存储PHP变量)中存储 ...

  7. [原创]java使用JDBC向MySQL数据库批次插入10W条数据测试效率

    使用JDBC连接MySQL数据库进行数据插入的时候,特别是大批量数据连续插入(100000),如何提高效率呢?在JDBC编程接口中Statement 有两个方法特别值得注意:通过使用addBatch( ...

  8. 参数探测(Parameter Sniffing)影响存储过程执行效率解决方案

    如果SQL query中有参数,SQL Server 会创建一个参数嗅探进程以提高执行性能.该计划通常是最好的并被保存以重复利用.只是偶尔,不会选择最优的执行计划而影响执行效率. SQL Server ...

  9. 3D游戏中的画质与效率适配

      哪里来的需求? 众所周知,由于不同的设备配置不同.导致其CPU和GPU处理能力有高有低.同样的游戏想要在所有设备上运行流畅且画面精美,是不可能的.这就需要我们针对不同的设备能力进行画质调节,以保证 ...

随机推荐

  1. javascript中正则实现读取当前url中指定参数值方法。

    getQueryString:function(name) { var reg = new RegExp("(^|&)"+ name +"=([^&]*) ...

  2. apache与nginx的虚拟域名配置

    由于开发需求,项目有时候需要设置虚拟域名进行测试.下面是分别是apache和nginx的配置 一.apache 环境:wampserver2.5 1.修改host文件 C:\Windows\Syste ...

  3. drop和delete的区别是什么

    当你不再需要该表时, 用 drop;当你仍要保留该表,但要删除所有记录时, 用 truncate;当你要删除部分记录时(always with a WHERE clause), 用 delete.

  4. windows开发hadoop文件系统权限错误

    org.apache.hadoop.security.AccessControlException: Permission denied: user=Administrator, access=WRI ...

  5. Sqli-labs less 24

    Less-24 Ps:本关可能会有朋友和我遇到一样的问题,登录成功以后没有修改密码的相关操作.此时造成问题的主要原因是logged-in.php文件不正确.可重新下载解压,解压过程中要主要要覆盖. 本 ...

  6. ios开发多线程--GCD

    引言 虽然GCD使用很广,而且在面试时也经常问与GCD相关的问题,但是我相信深入理解关于GCD知识的人肯定不多,大部分都是人云亦云,只是使用过GCD完成一些很简单的功能.当然,使用GCD完成一些简单的 ...

  7. chrome浏览器无法设置打开特定网页

    最近chrome浏览器更新后,发现以前设置的启动浏览器“重上次停下的地方继续”功能消失了. 当我点击设置网页时,会出现如上提示. 后来有同事给了如下一个连接,里面说到这个是公司的超级管理员搞的,他定义 ...

  8. SpriteBuilder中使用GUI界面快速搭建RPG游戏中的地图名显示动画

    大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请告诉我,如果觉得不错请多多支持点赞.谢谢! hopy ;) 在RPG游戏中我们在进入一个新的场景时,比如一个房间,一个村庄, ...

  9. day52类型转换 运算符 流程控制

    0.复习 1.导入 <div id="div1" onclick="this.style.color = 'red';">12345</div ...

  10. centos下redis安全相关

    博文背景: 由于发现众多同学,在使用云服务器时,安装的redis3.0+版本都关闭了protected-mode,因而都遭遇了挖矿病毒的攻击,使得服务器99%的占用率!! 因此我们在使用redis时候 ...