如果要你写一个计算字符串长度的函数 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. Python Coding Interview

    Python Coding Interview Python Advanced Use enumerate() to iterate over both indices and values Debu ...

  2. Webpack 4.x 默认支持 ES6 语法

    Webpack 4.x 默认支持 ES6 语法 Q: 为什么 webpack4 默认支持 ES6 语法的压缩? A: terser 里面实现了 ES6 语法的 AST解析. webpack 4 里使用 ...

  3. W3Schools Quizzes

    W3Schools Quizzes Test your skills https://www.w3schools.com/quiztest/default.asp Quiz HOME Quiz HTM ...

  4. Top 10 JavaScript errors

    Top 10 JavaScript errors javascript errors https://rollbar.com/blog/tags/top-errors https://rollbar. ...

  5. linux & node & cli & exit(0) & exit(1)

    linux & node & cli & exit(0) & exit(1) exit(0) & exit(1) demo exit(0) === OK exi ...

  6. how to group date array by month in javascript

    how to group date array by month in javascript https://stackoverflow.com/questions/14446511/most-eff ...

  7. nasm astrncat_s函数 x86

    xxx.asm: %define p1 ebp+8 %define p2 ebp+12 %define p3 ebp+16 %define p4 ebp+20 section .text global ...

  8. 黑马来袭!NGK生态所二月上线!

    日前,加密货币交易所Coinbase Global Inc,向美国证券交易委员会申请首次公开募股,成为首家公开上市的加密货币交易所."Coinbase上市,给行业带来更多的是信心.让大家看到 ...

  9. 开源OA办公系统的“应用市场”,能够为协同办公开拓什么样的“前路”?

    在我们的日常生活中,应用市场这个词,总是与智能手机划上等号,不管使用的是iPhone还是安卓,总会接触到手机上的APP应用市场,我们可以在应用市场中,选择自己所需要的APP应用软件,下载使用后,可以让 ...

  10. vue学习遇到的问题

    1.vue脚手架的安装,解决链接:https://www.cnblogs.com/qcq0703/p/14439467.html2.2.2.0+ 的版本里,当在组件上使用 v-for 时,key 现在 ...