musl中strlen源码实现和分析
最近在學習《C 和指針》的第 6 章指針部分,在 6.12 章節看到了 strlen 函數的實現,聯想到最近有在看 musl 的源碼,于是就把 musl 中 strlen 的源碼認真地分析了一下,發現源碼中有一些有意思的點,特地寫這篇文章跟各位感興趣的小伙伴分享一下。本文重點對 musl 的 strlen 源碼中的一些有意思的點進行分析,希望能對你理解 strlen 函數實現有所幫助。
1. strlen 源碼
要計算一個字符串的長度,可以通過以下的代碼實現:
/*
** 計算一個字符串的長度
*/
#include <stdlib.h>
size_t strlen(char *string)
{
int length = 0;
// 依次訪問字符串的內容,計數字符數,直到遇見 NUL 終止符。
while(*string++ != '\0')
length++;
return length;
}
在指針到達字符串的末尾的 NUL 字符之前,while 語句中 *string++ 表達式的值一直為真。它同時增加指針的值,用于下一次測試。這個表達式甚至可以正確地處理空字符串。
需要注意的是,如果調用這個函數時,傳入的參數是 NULL 指針,那么 while 語句中的間接訪問就會失敗。這是因為在 C 語言中 NULL 是一個宏定義,表示一個空指針常量,其值為 0。當我們試圖對 NULL 指針進行解引用操作時,實際上是在嘗試訪問一個不存在的內存地址。解引用操作是指通過指針訪問其指向的內存位置的值。當我們對一個 NULL 指針進行解引用時,由于 NULL 指針并不指向任何有效的內存地址,因此無法獲取到有效的值。這會導致程序在運行時發生錯誤,通常會觸發一個異常,導致程序崩潰。
那么問題來了,既然 strlen 傳入的參數有可能為 NULL,那么有沒有必要在 strlen 函數的實現中加入指針為空的判斷呢?答案是不需要,因為檢查指針變量是否為 NULL,應該在指針變量創建之后就進行是否為 NULL 的判斷,這樣只需要檢查一次就行了,后邊再用指針變量的時候直接使用就行,不需要再對指針變量是否為 NULL 進行判斷。
2. musl 中 strlen 的源碼
上節中的示例代碼很簡單也很好理解,但是 musl 中的代碼就沒那么好理解了,musl 代碼的 strlen 實現如下:
#include <string.h>
#include <stdint.h>
#include <limits.h>
#define ALIGN (sizeof(size_t))
#define ONES ((size_t)-1/UCHAR_MAX)
#define HIGHS (ONES * (UCHAR_MAX/2+1))
#define HASZERO(x) ((x)-ONES & ~(x) & HIGHS)
size_t strlen(const char *s)
{
const char *a = s;
#ifdef __GNUC__
typedef size_t __attribute__((__may_alias__)) word;
const word *w;
for (; (uintptr_t)s % ALIGN; s++) if (!*s) return s-a;
for (w = (const void *)s; !HASZERO(*w); w++);
s = (const void *)w;
#endif
for (; *s; s++);
return s-a;
}
看到這里,建議各位小伙伴花點兒時間仔細閱讀一下上面的源碼,并試著回答以下幾個問題:
- 在 64 位操作系統中,宏
ALIGN、ONES、HIGHS的值分別為多少? -
#ifdef __GNUC__和#endif中的代碼的作用是什么?只寫#endif后面的代碼一樣可以實現字符串長度的計算,為什么還要寫#ifdef __GNUC__和#endif中的代碼呢? -
HASZERO(*w)的作用是什么呢?試著舉例說明HASZERO(*w)檢測 word 中是否存在著 0 的過程。
2.1 問題 1 分析
在 64 位操作系統中,宏 ALIGN 的值為8,ONES 的值為0x0101010101010101,HIGHS 的值為 0x8080808080808080。
計算過程如下:
-
ALIGN的計算:sizeof(size_t)的結果為8,所以ALIGN的值為 8。 -
ONES的計算:UCHAR_MAX的值為255,所以(size_t)-1/UCHAR_MAX的結果為0xffffffffffffffff/0xff=0x0101010101010101。 -
HIGHS的計算:ONES的值為0x0101010101010101,所以UCHAR_MAX/2+1的結果為128,所以ONES * (UCHAR_MAX/2+1)的結果為 0x8080808080808080。
2.2 問題 2 分析
#ifdef __GNUC__ 和 #endif 中的代碼的作用是檢查是否使用的是 GCC 編譯器。如果是 GCC 編譯器,那么就會執行 #ifdef __GNUC__ 和 #endif 之間的代碼塊。這段代碼塊中定義了一些類型別名和變量,用于優化字符串長度的計算。如果不是 GCC 編譯器,那么這段代碼塊會被忽略。
盡管只寫 #endif 后面的代碼也可以實現字符串長度的計算,但是通過使用 GCC 特定的優化技巧,可以提高計算效率。因此,為了在 GCC 編譯器下獲得更好的性能,才需要寫 #ifdef __GNUC__ 和 #endif 中的代碼。
這樣設計的好處是,在一次位運算操作中,就可以快速檢查一個 64 位無符號整數中是否存在字節值為 0 的字節,而不需要使用循環或其他復雜的邏輯。這種設計可以提高代碼的效率和性能。
2.3 問題 3 分析
HASZERO(*w) 的作用是檢查一個 size_t 類型的值中是否存在字節值為0的字節。它的計算過程是將該值減去 ONES,然后與其取反進行按位與操作,再與 HIGHS 進行按位與操作。如果最終的結果為非 0,說明該值中存在字節值為 0 的字節。
看到這里后,各位小伙伴可能對 HASZERO(*w) 的具體計算過程仍然不太理解,如果對這塊不太感興趣的話,后邊的內容可以簡單看一下,對這部分有點印象就行,等后邊有用到的時候再花點時間研究一下就好。這里列舉兩個例子來說明具體的計算過程,需要說明的是,在 64 位操作系統中 sizeof(size_t)=8,*w 為一個 8 個字節的數,例 1 中 *w 的值為 0x1101110111011101,各個字節都非 0;例 2 中 *w 的值為 0x1100110111011101,第二個字節為 0。
2.3.1 例 1 的計算過程
例 1,在 64 位操作系統中 ONES 的值為0x0101010101010101,HIGHS 的值為0x8080808080808080,*w 的值為 0x1101110111011101。那么,HASZERO(*w) 的計算過程如下:
- 將
*w減去ONES:0x1101110111011101 - 0x0101010101010101 = 0x1000100010001000 - 將結果與其取反進行按位與操作:0x1000100010001000 & ~0x1101110111011101 = 0x1000100010001000 & 0xEEFEEEFEEEFEEEFE=0x0000000000000000
- 將結果與
HIGHS進行按位與操作:0x0000000000000000 & 0x8080808080808080 = 0x0000000000000000
由于最終的結果為0,說明*w中不存在字節值為 0 的字節。
2.3.2 例 2 的計算過程
例 2,在 64 位操作系統中 ONES、HIGHS 的值同上例一樣,本例中 *w 的值為 0x1100110111011101。那么,HASZERO(*w) 的計算過程如下:
- 將
*w減去ONES:0x1100110111011101 - 0x0101010101010101 = 0x0FFF100010001000 - 將結果與其取反進行按位與操作:0x0fff100010001000 & ~0x1100110111011101 = 0x0FFF100010001000 & 0xEEFFEEFEEEFEEEFE=0x0EFF000000000000
- 將結果與
HIGHS進行按位與操作:0x0000000000000000 & 0x8080808080808080 = 0x0080000000000000
由于最終的結果不為 0,說明*w中存在字節值為 0 的字節。
2.3.3 對上述兩個例子的總結
通過上面的兩個例子可以發現,利用 HASZERO(*w) 可以有效地確定字 *w 的 8個字節(64 位操作系統中,1字=8字節)中是否存在值為 0 的字節。
上面的兩個例子很好地介紹了 HASZERO(*w) 的計算過程,但是多字節的計算過程較為復雜,接下來我們以單字節為例,來分析 HASZERO(*w) 可以檢測字中是否存在 0 的原因:
首先,我們知道一個無符號字節的表示范圍為:0~255,可以把這個范圍分為如下幾段,并計算 HASZERO(*w) 的結果(本例中以 8 位操作系統為例),由于只涉及單字節的計算過程,所以計算過程較為簡單,并且很容易發現規律:
| 范圍 | HASZERO(*w) |
|---|---|
| 0 | 0x80 |
| 1~127 | 0x00 |
| 128 | 0x00 |
| 129~255 | 0x00 |
分析上面的表格可以發現,只有在 *w=0 的時候,HASZERO(*w) 的結果才為非 0。 |
3. 總結
上面對 musl 中 strlen 的源碼實現啰嗦了這么多,那么 HASZERO(*w) 對我們的日常編碼有什么用呢?一個容易想到的用處是,可以參考 strlen 的源碼寫出判斷 int 類型或者 long 類型的數據中是否存在著字節值為 0 的字節。當然,如果用循環+移位的方法也可以很方便的實現,但在追求效率的情況下,可以考慮利用 strlen 源碼中的 HASZERO(*w) 來實現。當然知道有這么個用處只是一方面,我想更重要的是通過本文的分析,你可以對 strlen 的設計有更深的了解,對一些細節也有了更深的認識。
希望本文對你有所幫助!
總結
以上是生活随笔為你收集整理的musl中strlen源码实现和分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 从GPT定制到Turbo升级再到Assi
- 下一篇: es笔记六之聚合操作之指标聚合