为了效率,我们可以用的招数 之 strchr
如果要写一个从字符串中查找一个字符的函数,相信你不难想到如下代码:
1 char* __cdecl strchr_1( 2 char const* _Str, 3 int _Val 4 ) { 5 6 while (*_Str && *_Str != _Val) 7 _Str++; 8 9 if (*_Str == _Val) 10 return(_Str); 11 12 return NULL; 13 }
和 strlen 的效率之旅一样,我们先做好测试脚手架:
1 typedef char* (__cdecl* p_strchr)( 2 char const* _Str, 3 int _Val 4 ); 5 6 // 测试正确性 7 void test_function( 8 char* funName, 9 p_strchr function, 10 char* str, 11 char ch, 12 int expectIndex) { 13 int got = function(str, ch) - (int)str; 14 printf( 15 "func [%s] test value [%s] find char [%c]," 16 " expect [%d], got [%d], %s\n", 17 funName, 18 str, 19 ch, 20 got, 21 expectIndex, 22 got == expectIndex 23 ? "true" 24 : "false" 25 ); 26 } 27 28 // 正确性测试用例 29 void test_functions(char* funName, p_strchr function) { 30 struct test_item { 31 32 char* str; 33 char val; 34 int expect_index; 35 } items[] = { 36 // return NULL, expect nagtive address of "a" 37 { "a", 'b', -(int)"a" }, 38 // last is '\0', returns 1 39 { "a", '\0', 1 }, 40 { "ab", 'a', 0 }, 41 { "ab", 'b', 1 }, 42 {"abc", 'b', 1 } 43 }; 44 45 int size = sizeof(items) / sizeof(struct test_item); 46 47 for (int i = 0; i < size; i++) { 48 test_function( 49 funName, 50 function, 51 items[i].str, 52 items[i].val, 53 items[i].expect_index); 54 } 55 }
放入main函数执行,执行结果如下:
可以看到,我们获取到了我们想要的效果,那么接下来再来一个效率对比,我们的测试脚手架如下:
1 #define BUFF_SIZE 100000000 2 char buff[BUFF_SIZE + 1]; // 100M + 1BYTE 3 void test_function_prof( 4 char* funName, 5 p_strchr function, 6 char* str, 7 char ch) { 8 ULONGLONG start = 0; 9 ULONGLONG end = 0; 10 11 start = GetTickCount64(); 12 function(str, ch); 13 end = GetTickCount64(); 14 15 printf( 16 "test func [%s] start [%lld], end [%lld], cost: [%lld]\n", 17 funName, 18 start, 19 end, 20 end - start 21 ); 22 } 23 24 void test_profs() { 25 // init 26 int size = BUFF_SIZE; 27 for (int i = 0; i < size; i++) { 28 buff[i] = 'a'; 29 } 30 buff[BUFF_SIZE - 1] = 'b'; 31 buff[BUFF_SIZE] = '\0'; 32 33 test_function_prof("strchar_1", strchr_1, buff, 'b'); 34 }
在主函数中调用 test_profs 函数,得到结果如下:
一个100M长的字符串,查找到结尾需要 156ms,那么系统自带的 strchr 函数表现如何呢?向 test_profs 函数添加如下代码:
1 test_function_prof("strchar", strchr, buff, 'b');
得到结果如下:
哇,差距挺大的,居然差了8.75(140/16)倍,那么效率何来?我们到源码中找答案,找到系统 strchr 函数的实现,我们获取到如下代码:
1 page ,132 2 title strchr - search string for given character 3 ;*** 4 ;strchr.asm - search a string for a given character 5 ; 6 ; Copyright (c) Microsoft Corporation. All rights reserved. 7 ; 8 ;Purpose: 9 ; defines strchr() - search a string for a character 10 ; 11 ;******************************************************************************* 12 13 .xlist 14 include vcruntime.inc 15 .list 16 17 page 18 ;*** 19 ;char *strchr(string, chr) - search a string for a character 20 ; 21 ;Purpose: 22 ; Searches a string for a given character, which may be the 23 ; null character '\0'. 24 ; 25 ; Algorithm: 26 ; char * 27 ; strchr (string, chr) 28 ; char *string, chr; 29 ; { 30 ; while (*string && *string != chr) 31 ; string++; 32 ; if (*string == chr) 33 ; return(string); 34 ; return((char *)0); 35 ; } 36 ; 37 ;Entry: 38 ; char *string - string to search in 39 ; char chr - character to search for 40 ; 41 ;Exit: 42 ; returns pointer to the first occurence of c in string 43 ; returns NULL if chr does not occur in string 44 ; 45 ;Uses: 46 ; 47 ;Exceptions: 48 ; 49 ;******************************************************************************* 50 51 CODESEG 52 53 align 16 54 public strchr, __from_strstr_to_strchr 55 strchr proc \ 56 string:ptr byte, \ 57 chr:byte 58 59 OPTION PROLOGUE:NONE, EPILOGUE:NONE 60 61 .FPO ( 0, 2, 0, 0, 0, 0 ) 62 63 ; Include SSE2 code path for platforms that support it. 64 include strchr_sse.inc 65 66 xor eax,eax 67 mov al,[esp + 8] ; al = chr (search char) 68 69 __from_strstr_to_strchr label proc 70 71 push ebx ; PRESERVE EBX 72 mov ebx,eax ; ebx = 0/0/0/chr 73 shl eax,8 ; eax = 0/0/chr/0 74 mov edx,[esp + 8] ; edx = buffer 75 test edx,3 ; test if string is aligned on 32 bits 76 jz short main_loop_start 77 78 str_misaligned: ; simple byte loop until string is aligned 79 mov cl,[edx] 80 add edx,1 81 cmp cl,bl 82 je short found_bx 83 test cl,cl 84 jz short retnull_bx 85 test edx,3 ; now aligned ? 86 jne short str_misaligned 87 88 main_loop_start: ; set all 4 bytes of ebx to [chr] 89 or ebx,eax ; ebx = 0/0/chr/chr 90 push edi ; PRESERVE EDI 91 mov eax,ebx ; eax = 0/0/chr/chr 92 shl ebx,10h ; ebx = chr/chr/0/0 93 push esi ; PRESERVE ESI 94 or ebx,eax ; ebx = all 4 bytes = [chr] 95 96 ; in the main loop (below), we are looking for chr or for EOS (end of string) 97 98 main_loop: 99 mov ecx,[edx] ; read dword (4 bytes) 100 mov edi,7efefeffh ; work with edi & ecx for looking for chr 101 102 mov eax,ecx ; eax = dword 103 mov esi,edi ; work with esi & eax for looking for EOS 104 105 xor ecx,ebx ; eax = dword xor chr/chr/chr/chr 106 add esi,eax 107 108 add edi,ecx 109 xor ecx,-1 110 111 xor eax,-1 112 xor ecx,edi 113 114 xor eax,esi 115 add edx,4 116 117 and ecx,81010100h ; test for chr 118 jnz short chr_is_found ; chr probably has been found 119 120 ; chr was not found, check for EOS 121 122 and eax,81010100h ; is any flag set ?? 123 jz short main_loop ; EOS was not found, go get another dword 124 125 and eax,01010100h ; is it in high byte? 126 jnz short retnull ; no, definitely found EOS, return failure 127 128 and esi,80000000h ; check was high byte 0 or 80h 129 jnz short main_loop ; it just was 80h in high byte, go get 130 ; another dword 131 retnull: 132 pop esi 133 pop edi 134 retnull_bx: 135 pop ebx 136 xor eax,eax 137 ret ; _cdecl return 138 139 found_bx: 140 lea eax,[edx - 1] 141 pop ebx ; restore ebx 142 ret ; _cdecl return 143 144 chr_is_found: 145 mov eax,[edx - 4] ; let's look one more time on this dword 146 cmp al,bl ; is chr in byte 0? 147 je short byte_0 148 test al,al ; test if low byte is 0 149 je retnull 150 cmp ah,bl ; is it byte 1 151 je short byte_1 152 test ah,ah ; found EOS ? 153 je retnull 154 shr eax,10h ; is it byte 2 155 cmp al,bl 156 je short byte_2 157 test al,al ; if in al some bits were set, bl!=bh 158 je retnull 159 cmp ah,bl 160 je short byte_3 161 test ah,ah 162 jz retnull 163 jmp short main_loop ; neither chr nor EOS found, go get 164 ; another dword 165 byte_3: 166 pop esi 167 pop edi 168 lea eax,[edx - 1] 169 pop ebx ; restore ebx 170 ret ; _cdecl return 171 172 byte_2: 173 lea eax,[edx - 2] 174 pop esi 175 pop edi 176 pop ebx 177 ret ; _cdecl return 178 179 byte_1: 180 lea eax,[edx - 3] 181 pop esi 182 pop edi 183 pop ebx 184 ret ; _cdecl return 185 186 byte_0: 187 lea eax,[edx - 4] 188 pop esi ; restore esi 189 pop edi ; restore edi 190 pop ebx ; restore ebx 191 ret ; _cdecl return 192 193 strchr endp 194 end
其中,
78-86行进行地址32位对齐(硬件访问32位对齐地址更快);
88-94行我们将ebx(32位4字节)中的四个字节均设置为要查找的字符;
98-129行是对字符串的迭代查找目标字符;
在循环中,我们看到了几个特殊的值,分别列出值以及其二进制位如下:
我们先看下对 ecx 寄存器的操作:
- 读取查询字符串的四个字节(这里因为是 ASCII 字符,所以是四个字符);
- 与 ebx(chr/chr/chr/chr) 异或;
- 按位取反(xor ecx, -1);
- edi = 0x7efefeff + ecx;
- 异或edi;
- 按位与 0x81010100;
- 跳转到依次判断各个字节是否位目标字符的逻辑;
从以上的逻辑,我们可以看到,117行判断决定了是否有可能找到了目标字符。那么是怎么做到的呢?
首先,我们假设 ecx 所有字节均不是目标值,则执行 xor ecx,ebx 之后,所有字节均不为0;
第二步,按位取反,则 ecx 所有字节均不为 0;
第三步,edi = 0x7efefeff + ecx,这一步参考 strlen 的效率之旅 中对 '\0' 的判断;
第四步,判断 ecx & 81010100 的值,如果结果不为0,则可能找到了对应字符,跳转到字节判断逻辑(chr_is_found);
第五步,如果走到了122行,则说明肯定没有找到目标,这里判断是否到了查找字符串结尾,逻辑和 strlen 的效率之旅 中判断字符串结尾逻辑相同,每次读取并处理了四个字节。
那么就不难理解,效率肯定比之前单字节匹配的速度更快,但......为什么是 8 倍而不是 4 倍呢?
仔细看代码之后,发现有这么一句:
1 ; Include SSE2 code path for platforms that support it. 2 include strchr_sse.inc
由于没有找到对应源代码,所以这里我们通过反汇编的方式,获取到strchr函数真正执行的代码来看,反汇编结果如下:
1 cmp dword ptr ds:[7BEB92DCh],1 ; 7BEA42E0 2 jb 7BEA4348 ; 7BEA42E7 3 movzx eax,byte ptr [esp+8] ; 7BEA42E9 4 mov edx,eax ; 7BEA42EE 5 shl eax,8 ; 7BEA42F0 6 or edx,eax ; 7BEA42F3 7 movd xmm3,edx ; 7BEA42F5 8 pshuflw xmm3,xmm3,0 ; 7BEA42F9 9 movlhps xmm3,xmm3 ; 7BEA42FE 10 mov edx,dword ptr [esp+4] ; 7BEA4301 11 mov ecx,0Fh ; 7BEA4305 12 or eax,0FFFFFFFFh ; 7BEA430A 13 and ecx,edx ; 7BEA430D 14 shl eax,cl ; 7BEA430F 15 sub edx,ecx ; 7BEA4311 16 movdqu xmm1,xmmword ptr [edx] ; 7BEA4313 17 pxor xmm2,xmm2 ; 7BEA4317 18 pcmpeqb xmm2,xmm1 ; 7BEA431B 19 pcmpeqb xmm1,xmm3 ; 7BEA431F 20 por xmm2,xmm1 ; 7BEA4323 21 pmovmskb ecx,xmm2 ; 7BEA4327 22 and ecx,eax ; 7BEA432B 23 jne 7BEA4337 ; 7BEA432D 24 or eax,0FFFFFFFFh ; 7BEA432F 25 add edx,10h ; 7BEA4332 26 jmp 7BEA4313 ; 7BEA4335 27 bsf eax,ecx ; 7BEA4337 28 add eax,edx ; 7BEA433A 29 movd edx,xmm3 ; 7BEA433C 30 xor ecx,ecx ; 7BEA4340 31 cmp dl,byte ptr [eax] ; 7BEA4342 32 cmovne eax,ecx ; 7BEA4344 33 ret ; 7BEA4347 34 xor eax,eax ; 7BEA4348 35 mov al,byte ptr [esp+8] ; 7BEA434A 36 push ebx ; 7BEA434E 37 mov ebx,eax ; 7BEA434F 38 shl eax,8 ; 7BEA4351 39 mov edx,dword ptr [esp+8] ; 7BEA4354 40 test edx,3 ; 7BEA4358 41 je 7BEA4375 ; 7BEA435E 42 mov cl,byte ptr [edx] ; 7BEA4360 43 add edx,1 ; 7BEA4362 44 cmp cl,bl ; 7BEA4365 45 je 7BEA43C2 ; 7BEA4367 46 test cl,cl ; 7BEA4369 47 je 7BEA43BE ; 7BEA436B 48 test edx,3 ; 7BEA436D 49 jne 7BEA4360 ; 7BEA4373 50 or ebx,eax ; 7BEA4375 51 push edi ; 7BEA4377 52 mov eax,ebx ; 7BEA4378 53 shl ebx,10h ; 7BEA437A 54 push esi ; 7BEA437D 55 or ebx,eax ; 7BEA437E 56 mov ecx,dword ptr [edx] ; 7BEA4380 57 mov edi,7EFEFEFFh ; 7BEA4382 58 mov eax,ecx ; 7BEA4387 59 mov esi,edi ; 7BEA4389 60 xor ecx,ebx ; 7BEA438B 61 add esi,eax ; 7BEA438D 62 add edi,ecx ; 7BEA438F 63 xor ecx,0FFFFFFFFh ; 7BEA4391 64 xor eax,0FFFFFFFFh ; 7BEA4394 65 xor ecx,edi ; 7BEA4397 66 xor eax,esi ; 7BEA4399 67 add edx,4 ; 7BEA439B 68 and ecx,81010100h ; 7BEA439E 69 jne 7BEA43C7 ; 7BEA43A4 70 and eax,81010100h ; 7BEA43A6 71 je 7BEA4380 ; 7BEA43AB 72 and eax,1010100h ; 7BEA43AD 73 jne 7BEA43BC ; 7BEA43B2 74 and esi,80000000h ; 7BEA43B4 75 jne 7BEA4380 ; 7BEA43BA 76 pop esi ; 7BEA43BC 77 pop edi ; 7BEA43BD 78 pop ebx ; 7BEA43BE 79 xor eax,eax ; 7BEA43BF 80 ret ; 7BEA43C1 81 lea eax,[edx-1] ; 7BEA43C2 82 pop ebx ; 7BEA43C5 83 ret ; 7BEA43C6 84 mov eax,dword ptr [edx-4] ; 7BEA43C7 85 cmp al,bl ; 7BEA43CA 86 je 7BEA4404 ; 7BEA43CC 87 test al,al ; 7BEA43CE 88 je 7BEA43BC ; 7BEA43D0 89 cmp ah,bl ; 7BEA43D2 90 je 7BEA43FD ; 7BEA43D4 91 test ah,ah ; 7BEA43D6 92 je 7BEA43BC ; 7BEA43D8 93 shr eax,10h ; 7BEA43DA 94 cmp al,bl ; 7BEA43DD 95 je 7BEA43F6 ; 7BEA43DF 96 test al,al ; 7BEA43E1 97 je 7BEA43BC ; 7BEA43E3 98 cmp ah,bl ; 7BEA43E5 99 je 7BEA43EF ; 7BEA43E7 100 test ah,ah ; 7BEA43E9 101 je 7BEA43BC ; 7BEA43EB 102 jmp 7BEA4380 ; 7BEA43ED 103 pop esi ; 7BEA43EF 104 pop edi ; 7BEA43F0 105 lea eax,[edx-1] ; 7BEA43F1 106 pop ebx ; 7BEA43F4 107 ret ; 7BEA43F5 108 lea eax,[edx-2] ; 7BEA43F6 109 pop esi ; 7BEA43F9 110 pop edi ; 7BEA43FA 111 pop ebx ; 7BEA43FB 112 ret ; 7BEA43FC 113 lea eax,[edx-3] ; 7BEA43FD 114 pop esi ; 7BEA4400 115 pop edi ; 7BEA4401 116 pop ebx ; 7BEA4402 117 ret ; 7BEA4403 118 lea eax,[edx-4] ; 7BEA4404 119 pop esi ; 7BEA4407 120 pop edi ; 7BEA4408 121 pop ebx ; 7BEA4409 122 ret ; 7BEA440A
由于这里是反汇编,是加载到内存中的,没有标号,跳转的时候指定的是跳转目标地址,这里将地址放到语句之后。
这里可以看到,反汇编代码和我们之前看到的明显不同,应该就是之前包含进来的代码了。那么,它都做了些什么呢?
在这里,我们从intel文档中查看几个命令:
pshuflw:
movlhps:
movdqu:
pxor:
pcmpeqb:
pmovmskb:
cmovne:
bsf:
第一行和第二行,是一个判断,我们看到,如果 ds:[7BEB92DCh] 处的值大于或等于1,则跳转到第 34 行(7BEA4348),为什么会有这个跳转呢?我们看看34行代码,并和之前看到的汇编代码作比较:
我们发现,后面的代码和之前我们看到的汇编源代码一模一样。所以,这里我们可以认为是在判断是否支持SSE,至于为什么如此判断,暂时还没有调查清楚,如果有清楚的,麻烦在评论区回复下,不胜感激。
在了解到执行代码后面部分一致的情况下,我们此时就可以将重点放在前半部分了,为了方便,我们这里再次引用反汇编得到的代码前面部分,并忽略第一句,如下:
1 movzx eax,byte ptr [esp+8] ; 7BEA42E9 2 mov edx,eax ; 7BEA42EE 3 shl eax,8 ; 7BEA42F0 4 or edx,eax ; 7BEA42F3 5 movd xmm3,edx ; 7BEA42F5 6 pshuflw xmm3,xmm3,0 ; 7BEA42F9 7 movlhps xmm3,xmm3 ; 7BEA42FE 8 mov edx,dword ptr [esp+4] ; 7BEA4301 9 mov ecx,0Fh ; 7BEA4305 10 or eax,0FFFFFFFFh ; 7BEA430A 11 and ecx,edx ; 7BEA430D 12 shl eax,cl ; 7BEA430F 13 sub edx,ecx ; 7BEA4311 14 movdqu xmm1,xmmword ptr [edx] ; 7BEA4313 15 pxor xmm2,xmm2 ; 7BEA4317 16 pcmpeqb xmm2,xmm1 ; 7BEA431B 17 pcmpeqb xmm1,xmm3 ; 7BEA431F 18 por xmm2,xmm1 ; 7BEA4323 19 pmovmskb ecx,xmm2 ; 7BEA4327 20 and ecx,eax ; 7BEA432B 21 jne 7BEA4337 ; 7BEA432D 22 or eax,0FFFFFFFFh ; 7BEA432F 23 add edx,10h ; 7BEA4332 24 jmp 7BEA4313 ; 7BEA4335 25 bsf eax,ecx ; 7BEA4337 26 add eax,edx ; 7BEA433A 27 movd edx,xmm3 ; 7BEA433C 28 xor ecx,ecx ; 7BEA4340 29 cmp dl,byte ptr [eax] ; 7BEA4342 30 cmovne eax,ecx ; 7BEA4344 31 ret ; 7BEA4347
1 - 4行:原理和之前类似,[esp+8] 处为要查找的目标字符值,到第四行,将edx寄存器的0-7位,和8-15位均设置为查找目标字符。
5 - 7行:将 xmm3 寄存器的每字节均设置为目标字符值,如果我们查找 'x' 字符,则 xmm3 的值为 "xxxxxxxxxxxxxxxx"。
8 - 13行:将查询的源字符串的地址进行16位对齐,并指向源字符串地址之前。并将 eax 值左移目标字符串地址的低四位值(意味着将eax作为之后取值的mask);
14 - 31 行:是查询的主要逻辑,下面我们详细讨论。
第14行,将edx指针指向内存开始的16字节复制到 xmm1寄存器中;
第15行,将xmm2寄存器所有字节设置为0;
第16行,将xmm1中所有的字节与xmm2字节做比较,比较结果放到xmm2中(如果xmm1中字节和xmm2中字节相等,则xmm2对应字节将被设置为0xff,否则将被设置为0x00);
第17行,将xmm3(每个字节值均为目标字符值)和xmm1中的字节做比较,结果放置到xmm1中,如果是第一次做比较,则xmm1中可能存有查找字符串之前的内容,而且有可能包含查找的目标字符,这种情况将在第20行进行处理;
第18行,将xmm1中(源字符串中16个字符或者字符串n个字节内容+字符串前n个字符和要查找的目标字符的比对结果,即前n个字符的擦护照结果)和xmm2中内容执行字节或运算。并将结果放到 xmm2 中。
第19行,按照xmm2中字节内容,生成一个 mask值,并将其放到 ecx 寄存器;
第20行,将ecx和eax(如果第一次,有效位被第12行设置为1,无效位被置为0)执行 and 运算;
第21行,如果20行执行结果不为0(说明找到了目标值),跳转到第25行;
第22行,eax 所有位均置为1;
第23行,将源字符串指针前移16(10h)个字节;
第24行,跳转到循环初始位置,开始新的一轮循环;
第25行,查找ecx中为1的位的编号,并将编号放置到eax;
第26行,将源字符串指针前移eax个字节,这就是目标字符所在位置了;
第27行,从xmm3(每个字节均为查找目标字符)中读取4个字节,并将值放置到edx中;
第28行,置ecx为0,以备返回(目标字符未找到);
第29行,比较dl(edx最低字节)和eax指向的字符;
第30行,如果29行比较结果不相等,说明没有找到目标字符,就置eax为0,否则eax不变;
第31行,返回;
这里可以看到,因为使用了 xmm 寄存器,我们每次都处理了16个字节,那为什么效率提升不是16倍呢?只能归结为xmm寄存器增加了电路复杂性,使得处理周期增加了一倍吧。