爲了效率,咱們能夠用的招數 之 strlen

若是要你寫一個計算字符串長度的函數 strlen,應該怎麼寫?相信你很容易寫出以下實現:c++

 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 }

運行結果以下:oop

 

 

咱們獲得了正確結果,可是這樣就夠了嗎?寫代碼,尤爲是常常被調用的代碼,效率是一個很重要的考慮方面,咱們的 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 編譯,如下同):spa

 

耗時94ms,時間有點長啊,能夠優化嗎?考慮到咱們只須要計算開始和結束地址之間的差,就獲得了長度,那麼若是省略計數變量,改爲以下會不會好些?debug

 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 的測試,修改主程序以下:3d

 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 的測試代碼:code

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 位是同樣的,只不過位數增長了。

 

相關文章
相關標籤/搜索