若是要你寫一個計算字符串長度的函數 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 位是同樣的,只不過位數增長了。