Linux glibc自帶哈希表的用例及性能測試
今天來看看Linux和一些常見的BSD系統(tǒng)上自帶的hashmap。
是的,系統(tǒng)自帶的。因?yàn)镻OSIX標(biāo)準(zhǔn)定義了一些常見的數(shù)據(jù)結(jié)構(gòu)(比如哈希表、二叉搜索樹、隊(duì)列)和算法(比如二分查找和快速排序),這些接口數(shù)量不少而且實(shí)現(xiàn)起來沒什么難度,因此各個(gè)想要兼容POSIX標(biāo)準(zhǔn)的操作系統(tǒng)/C函數(shù)庫都樂意于實(shí)現(xiàn)這些接口,畢竟兼容性越高越有人用嘛。順帶一提早期的Unix里就有這些函數(shù)的原型了,雖然市面上有不少更好的替代品,但使用了這些接口的老程序應(yīng)該也不會(huì)太少,因此兼容它們一定程度上也能提升自己的Unix兼容性,對于市場占有率來說是好事。
不過別高興的太早,因?yàn)檫@些接口都是c語言的,所以泛型什么的就別想了,全是void指針和回調(diào)函數(shù);而且這些接口為了兼容性設(shè)計(jì)得也都比較怪異,用起來十分甚至九分得不方便,對于大多數(shù)用戶的大多數(shù)場景來說我想都是很難利用這些內(nèi)置的數(shù)據(jù)結(jié)構(gòu)的。
不過這不影響我們簡單學(xué)習(xí)下這些接口并做些性能對比,所以接下來我們就來簡單說說自帶的hashmap——hsearch(3)吧。
hsearch簡介
前面提到過,hsearch和它的朋友是在Unix時(shí)代就已經(jīng)出現(xiàn)然后在POSIX標(biāo)準(zhǔn)中被明確定義的一系列數(shù)據(jù)結(jié)構(gòu)和算法之一。
hsearch實(shí)現(xiàn)的是hashmap,是日常開發(fā)中很常用的一種數(shù)據(jù)結(jié)構(gòu)。
這個(gè)系統(tǒng)自帶的哈希表的api分為三個(gè)函數(shù):
hcreate,負(fù)責(zé)創(chuàng)建哈希表hsearch,負(fù)責(zé)插入鍵值對、查找鍵值對hdestory,負(fù)責(zé)釋放hcreate創(chuàng)建的哈希表,不過里面的鍵值對的資源得自己管理hdestory不會(huì)釋放鍵和值
其中hcreate創(chuàng)建的全局的哈希表,也就是一個(gè)進(jìn)程里只有一個(gè),類似全局變量,這其實(shí)已經(jīng)帶來第一個(gè)痛點(diǎn)了:全局的對象非常不安全,而且做不到多實(shí)例在作為數(shù)據(jù)結(jié)構(gòu)的實(shí)用性上要大打折扣。所以glibc和muslc出于對安全性的考慮,都實(shí)現(xiàn)了這些函數(shù)的“_r”版本:hcreate_r、hsearch_r和hdestory_r。它們在功能上和沒有r后綴的函數(shù)是一樣的除了一點(diǎn)——它們不使用全局的哈希表對象而是需要顯式傳入要操作的對象實(shí)例。
我們后面的代碼都會(huì)基于這些帶有r后綴的函數(shù),畢竟操作全局變量的東西沒啥實(shí)用性也不好做自定義包裝。但缺點(diǎn)是這些函數(shù)不是POSIX標(biāo)準(zhǔn)的一部分屬于擴(kuò)展,因此在一些BSD系統(tǒng)上有可能無法使用,但Linux用戶是不需要擔(dān)心的。
包裝hsearch
如果你看過文檔的話就會(huì)發(fā)現(xiàn)hsearch很狂野,哈希表的釋放需要手動(dòng)執(zhí)行而且里面存著的key和value是不會(huì)被釋放的,也就是說存里面的數(shù)據(jù)的生命周期得自己管理,然而hsearch并不支持遍歷功能,也就是你在想釋放hashmap的時(shí)候根本不知道里面存了什么存了多少——使用者得自己創(chuàng)建一些額外的空間來存儲(chǔ)map里有哪些數(shù)據(jù)這樣的信息。其次hsearch這個(gè)函數(shù)名字本身叫search,但實(shí)際上它既能插入數(shù)據(jù)又能查找數(shù)據(jù)——典型的一個(gè)函數(shù)干兩件完全不相干的事情,而且查找和插入的參數(shù)和返回值含義都不相同,簡直是誤用的溫床;這還不是最糟糕的,因?yàn)楦P(guān)鍵的更新操作沒有直接支持,你得用些不太安全的辦法取巧實(shí)現(xiàn)。接著,hcreate創(chuàng)建的哈希表是不能擴(kuò)容的,你得在創(chuàng)建時(shí)就制定最大長度(muslc的實(shí)現(xiàn)可以做擴(kuò)容,但glibc的不行,標(biāo)準(zhǔn)里也明確說明沒有擴(kuò)容)。最后也是最關(guān)鍵的,hsearch不支持刪除操作,你的數(shù)據(jù)存進(jìn)去之后就沒有辦法刪掉了,這點(diǎn)很要命,直接在很多場景上給hsearch判了死刑。
在功能缺失和接口難用的背景下,想直接使用這些接口是有很大挑戰(zhàn)的,至少我是沒興趣直接用。所以我們需要做一些包裝:
- 包裝遵循c++的RAII,自動(dòng)調(diào)用hdestory
- 包裝同樣不會(huì)去管理鍵和值的生命周期
- 包裝出來的對象支持移動(dòng)語義但禁止復(fù)制,畢竟除非額外存一份鍵值對,否則我們不知道m(xù)ap里有啥,在庫沒有提供相應(yīng)接口的前提下無法實(shí)現(xiàn)復(fù)制功能
- 刪除操作也不支持
- 支持更新操作
- 包裝出來的接口盡量和c++標(biāo)準(zhǔn)庫提供的對齊,包括使用方法和函數(shù)簽名
- 不支持泛型,只能存字符串,但接口里不會(huì)出現(xiàn)
void*
為啥我不額外存一份鍵值對呢,因?yàn)槟菢幼鑫疫€不如直接用標(biāo)準(zhǔn)庫或者其他第三方庫呢,它們只用存一份鍵值對接口也更友好。這個(gè)包裝充其量只是為了讓hsearch沒那么難用而已。
對于hsearch_data的包裝
hsearch_data是前面說的r后綴函數(shù)們要操作的哈希表對象,為了不再依賴全局變量,glibc定義了這一類型給使用者。雖說glibc定義成了struct,但我建議大家不要關(guān)注里面都有啥成員,因?yàn)橛貌簧希也槐WC以后不會(huì)變,大家應(yīng)該把hsearch_data當(dāng)成一個(gè)所有數(shù)據(jù)成員都是私有訪問屬性的類就行了。
對于它的包裝主要集中在生命周期的管理上,因?yàn)樗莌ashmap的實(shí)體:
class HSearchData
{
hsearch_data data_{};
friend void swap(HSearchData &lhs, HSearchData &rhs) noexcept
{
auto tmp = std::move(rhs.data_);
rhs.data_ = std::move(lhs.data_);
lhs.data_ = std::move(tmp);
}
public:
HSearchData(const size_t nelems) noexcept
{
hcreate_r(nelems == 0 ? 1 : nelems, &data_);
}
~HSearchData() noexcept
{
hdestroy_r(&data_);
}
HSearchData(const HSearchData&) = delete;
HSearchData& operator=(const HSearchData&) = delete;
HSearchData(HSearchData &&other) noexcept
: data_{std::move(other.data_)}
{
memset(&other.data_, 0, sizeof(hsearch_data));
}
HSearchData& operator=(HSearchData &&other) noexcept
{
HSearchData tmp{std::forward<HSearchData>(other)};
swap(*this, tmp);
return *this;
}
hsearch_data *get() const noexcept
{
return const_cast<hsearch_data*>(&data_);
}
};
創(chuàng)建和銷毀由構(gòu)造函數(shù)和析構(gòu)函數(shù)完成。hcreate_r很簡單,第一個(gè)參數(shù)是map的最大大小,第二個(gè)是我們需要初始化的實(shí)例對象的指針。銷毀就很簡單了,把需要銷毀的對象的指針穿進(jìn)去就行。兩個(gè)函數(shù)都只在參數(shù)是空指針時(shí)才報(bào)錯(cuò),因此錯(cuò)誤處理沒什么必要也沒法做。創(chuàng)建hsearch_data時(shí)size最低也得有1這其實(shí)只是我的習(xí)慣,傳0進(jìn)去也不會(huì)報(bào)錯(cuò)因?yàn)闃?biāo)準(zhǔn)允許庫函數(shù)自動(dòng)調(diào)整大小到一個(gè)合適的值。但這留下一個(gè)坑,因?yàn)樵试S函數(shù)自動(dòng)調(diào)整到合適的大小,所以你傳進(jìn)去的數(shù)字可能并不是哈希表能存的數(shù)據(jù)量的上限,比如你想控制只能存進(jìn)6個(gè)數(shù)據(jù),但實(shí)際上發(fā)現(xiàn)插入到第八個(gè)也沒問題。現(xiàn)實(shí)也確實(shí)如此,glibc會(huì)把數(shù)量調(diào)整到一個(gè)離參數(shù)最近的素?cái)?shù),而muslc會(huì)調(diào)整成2的冪,上述意外幾乎總是會(huì)發(fā)生的。包裝當(dāng)然對著沒有直接的辦法,但我們可以做額外的限制來保證上限有效。
復(fù)制操作都被顯式delete掉了。前面也說過除非額外再存一份鍵值對,否則拷貝是無從實(shí)現(xiàn)的。
移動(dòng)賦值使用了常見的swap and move慣用法,我們必須保證對象被移動(dòng)后仍然可以被安全地析構(gòu),因此需要將舊值交換過去或者用零值填充,因?yàn)?code>hsearch_data是個(gè)普通的c結(jié)構(gòu)體所以即使不知道其內(nèi)部結(jié)構(gòu)也可以放心用memset。前面定義的友元函數(shù)swap也是為了實(shí)現(xiàn)慣用法而編寫的。
最后提供一個(gè)類似unique_ptr的get函數(shù),畢竟hsearch接口只能操作hsearch_data*類型的數(shù)據(jù)它不認(rèn)識(shí)我們包裝出來的類,所以需要提供一種得到原始數(shù)據(jù)的辦法。一種更簡單的辦法是提供類型轉(zhuǎn)換運(yùn)算符:
opertor hearch_data*() const noexcept
{
return const_cast<hsearch_data*>(&data_);
}
這樣用不著每次調(diào)get方法了,但我還是習(xí)慣使用get,你可以酌情自己選一種習(xí)慣了的方式。
之后只需要向用std::string那樣正常用這個(gè)對象就行了,無需再額外關(guān)心生命周期問題。
包裝類整體概覽
在繼續(xù)講解插入的包裝前,我們先來看下map整體的結(jié)構(gòu):
class HMap
{
public:
HMap(size_t nelems) noexcept;
HMap(const HMap&) = delete;
HMap &operator=(const HMap&) = delete;
HMap(HMap &&other) noexcept;
HMap &operator=(HMap &&other) noexcept;
bool insert(const char *key, const char *value) noexcept;
bool contains(const char *key) const noexcept;
// 返回最多能存多少個(gè)元素
size_t capacity() const noexcept
{
return limit_;
}
// 返回當(dāng)前map里存了多少個(gè)元素
size_t size() const noexcept
{
return size_;
}
const char *get(const char *key) const noexcept;
bool update(const char *key, const char *new_data) noexcept;
private:
//std::unique_ptr<hsearch_data, decltype(&destory_heap_allocated_hsearch_data)> map_;
HSearchData map_;
size_t size_, limit_;
// 用于實(shí)現(xiàn)copy and swap慣用法的幫助函數(shù)
friend void swap(HMap &rhs, HMap &lhs) noexcept
{
using std::swap;
swap(rhs.map_, lhs.map_);
swap(rhs.size_, lhs.size_);
swap(rhs.limit_, lhs.limit_);
}
};
包裝后的類叫HMap,它沒有默認(rèn)構(gòu)造函數(shù)也不支持拷貝但支持移動(dòng)語義,只提供一個(gè)構(gòu)造函數(shù)顯式設(shè)置存儲(chǔ)元素?cái)?shù)量的最大上限。同時(shí)我們也用私有數(shù)據(jù)成員記錄了map可以容納的最大元素?cái)?shù)量和當(dāng)前包含的元素?cái)?shù)量。
操作上支持插入、更新、查找是否存在以及根據(jù)key獲取value。這就是整體結(jié)構(gòu)了,下面我們深入探討下上面沒給出具體實(shí)現(xiàn)的那些函數(shù)。
插入元素
插入元素需要用到hsearch函數(shù),具體形式是hsearch_r(struct ENTRY*, ENTER, struct ENTRY**, hsearch_data*)。
其中struct ENTRY是要存入map的鍵值對,這也是hsearch接口規(guī)定的,具體結(jié)構(gòu)如下:
typedef struct entry
{
char *key;
void *data;
}
ENTRY;
key必須是c風(fēng)格的字符串,因此我們的包裝函數(shù)也只接受字符串作為鍵;值因?yàn)槭?code>void*,理論上除了函數(shù)指針之外啥都能存,但為了免得自己處理生命周期問題,我們的包裝類也只支持存儲(chǔ)字符串常量。
現(xiàn)在函數(shù)的四個(gè)參數(shù)就好解釋了,第一個(gè)參數(shù)就是我們要存入的鍵值對,沒錯(cuò)需要我們自己把鍵值對構(gòu)建出來,函數(shù)會(huì)把這個(gè)鍵值對復(fù)制一份存進(jìn)hsearch_data里;第二個(gè)參數(shù)是操作的類型,是個(gè)宏,前面說過這個(gè)函數(shù)支持完全不同的操作,所以需要宏來告訴函數(shù)當(dāng)前操作是在做什么,插入的時(shí)候需要傳值ENTER;第三個(gè)參數(shù)是函數(shù)用來返回?cái)?shù)據(jù)的,新插入進(jìn)map的鍵值對的指針會(huì)被寫進(jìn)這個(gè)參數(shù)里,一般來說這個(gè)參數(shù)沒什么用,我們的包裝類里也沒用到,但還是得傳有效值進(jìn)去不能傳空指針;最后一個(gè)參數(shù)是我們需要操作的哈希表的實(shí)例,這個(gè)一目了然。
hsearch_r在調(diào)用失敗的時(shí)候會(huì)返回零值,失敗的原因一般是參數(shù)穿了空指針或者哈希表已達(dá)到容量上限。
插入的流程很直白,我們自己先檢查容量上限因?yàn)榍拔恼f過不能依賴hsearch_r去檢查,然后構(gòu)造鍵值對,接著調(diào)用函數(shù)并檢查執(zhí)行結(jié)果即可:
bool insert(const char *key, const char *value) noexcept
{
if (size_ >= limit_ || key == nullptr || value == nullptr) {
return false;
}
auto entry = ENTRY{
// c++里必須做轉(zhuǎn)換去掉底層cosnt,不過這里我們只存字符串常量因此是安全的,hsearch也不會(huì)修改存進(jìn)去的數(shù)據(jù)
.key = const_cast<char*>(key),
.data = const_cast<char*>(value),
};
ENTRY *p = nullptr;
int ret = hsearch_r(entry, ENTER, &p, map_,get());
if (ret == 0) {
return false;
}
++size_;
return true;
}
需要注意的是,key必須不為空,而value其實(shí)沒有硬性要求,但我們也要求不為空,這樣方便處理。
檢測元素是否存在
檢測元素模仿的是c++20中std::unordered_map新增的contains方法,這個(gè)方法接收一個(gè)key,返回key是否在map中存在。
實(shí)現(xiàn)檢測同樣需要使用hsearch_r函數(shù),這次的形式是hsearch_r(struct ENTRY*, FIND, struct ENTRY**, hsearch_data*)
第一個(gè)參數(shù)是我們要查找的key,其中data字段可以不設(shè)置,但我推薦設(shè)置成空指針,key則設(shè)置成我們需要查找的內(nèi)容。
第二個(gè)參數(shù)是宏,需要填FIND進(jìn)去。
第三個(gè)參數(shù)存的是根據(jù)key找到的鍵值對的指針,而第四個(gè)參數(shù)就不用我多說了是我們要操作的哈希表。
如果沒找到對應(yīng)的key,函數(shù)會(huì)返回零值,這里我們只需要關(guān)注這個(gè)返回值是否為零就可以了:
bool contains(const char *key) const noexcept
{
if (!key) {
return false;
}
ENTRY *p = nullptr;
auto entry = ENTRY{
.key = const_cast<char*>(key),
.data = nullptr,
};
int ret = hsearch_r(entry, FIND, &p, map_.get());
return ret != 0;
}
代碼還是很簡單的。
獲取元素
這里我們的包裝類模仿的是標(biāo)準(zhǔn)庫map的at函數(shù),不過有兩點(diǎn)區(qū)別,第一是我們不返回引用,第二是沒找到元素我們不會(huì)拋錯(cuò)而是返回空指針。
查找其實(shí)和前面的元素檢測差不多,因此不再贅述,直接看代碼:
const char *get(const char *key) const noexcept
{
if (!key) {
return nullptr;
}
ENTRY *p = nullptr;
auto entry = ENTRY{
.key = const_cast<char*>(key),
.data = nullptr,
};
int ret = hsearch_r(entry, FIND, &p, map_.get());
if (ret == 0 || p == nullptr) {
return nullptr;
}
return static_cast<const char *>(p->data);
}
如果找到了函數(shù)會(huì)返回非零值,并將結(jié)果存入p,不過我們還是小心謹(jǐn)慎一點(diǎn)額外判斷下p是否為空。然后直接返回拿到的數(shù)據(jù)即可。
更新元素
更新的實(shí)現(xiàn)比較麻煩,因?yàn)槲覀兛吹絟search沒有直接提供接口,不能想標(biāo)準(zhǔn)庫那樣m[key] = newValue。
不過我們還是可以用些取巧的辦法去實(shí)現(xiàn)的。
我們不難注意到,FIND操作返回的其實(shí)是存在map內(nèi)部的鍵值對的指針,因此只要我們修改這個(gè)指針指向的結(jié)構(gòu)體的data字段,map里對應(yīng)的鍵值對也會(huì)被修改,這不就實(shí)現(xiàn)了更新了嗎。其實(shí)ENTER操作在成功插入時(shí)也會(huì)把map內(nèi)部的鍵值對指針作為第三個(gè)參數(shù)的值返回給我們,實(shí)現(xiàn)效果是一樣的。
這里我們肯定選擇利用FIND操作,因?yàn)樗唵危?/p>
bool update(const char *key, const char *new_data) noexcept
{
if (!key) {
return false;
}
auto entry = ENTRY{
.key = const_cast<char*>(key),
.data = nullptr,
};
ENTRY *p = nullptr;
int ret = hsearch_r(entry, FIND, &p, map_.get());
if (ret == 0 || p == nullptr) {
return false;
}
p->data = const_cast<char*>(new_data);
return true;
}
我們先查找key是否存在,然后再修改函數(shù)給我們的鍵值對。
這個(gè)辦法是很取巧的,因?yàn)槲覀兿喈?dāng)于繞過了hashmap直接修改了它的內(nèi)部數(shù)據(jù),只不過恰巧這個(gè)操作是安全的。之所以我前面還說這個(gè)操作很不安全,是因?yàn)榻涌跊]有任何限制阻止我們修改鍵值對里的key,而一旦修改了key,這個(gè)哈希表就基本上報(bào)廢不能正常使用了。
其余的操作就沒啥好講解的了,移動(dòng)語義和HSearchData類實(shí)現(xiàn)的一樣,都是用move and swap慣用法,構(gòu)造函數(shù)也只是簡單設(shè)置下size和limit。
順帶一提遍歷操作也是沒法支持的,除非額外存儲(chǔ)鍵值對。
性能測試
性能測試主要對比上面包裝出來的各個(gè)功能的執(zhí)行速度,作為對比的是std::unordered_map<std::string_view, const char*>,注意必須得用string_view,這個(gè)類型作為key時(shí)標(biāo)準(zhǔn)庫才會(huì)真正計(jì)算字符串的哈希,用const char*標(biāo)準(zhǔn)庫只會(huì)對指針內(nèi)部的地址值做哈希運(yùn)算,這對hsearch來說是不公平也無意義的。
測試分為小樣本和大樣本,小樣本有16個(gè)不同的鍵值對,大樣本有70個(gè)。我們同時(shí)還會(huì)在 i5 8th 這樣的老CPU和 i7 14th 這樣的較新的CPU上做測試,畢竟哈希計(jì)算是比較考驗(yàn)CPU性能的,這幾年CPU的性能以及simd的普及會(huì)預(yù)計(jì)對哈希表的性能產(chǎn)生影響,因此選擇兩個(gè)環(huán)境作為對比。
完整的測試代碼很長,你可以在這里找到,我就不額外貼出來了,我們直接看結(jié)果。
首先是在老CPU上:
小樣本:

大樣本:

可以看到在老CPU上hsearch的速度要比標(biāo)準(zhǔn)庫快,尤其是插入和contains上。插入更慢的原因有不少,比如標(biāo)準(zhǔn)庫使用的哈希算法需要更多的計(jì)算量同時(shí)提供更好的哈希質(zhì)量,而glibc實(shí)現(xiàn)的hsearch使用的是比較簡單的哈希,因此在老CPU沒有那么多優(yōu)化運(yùn)算性能也比較弱的情況下顯然是簡單的哈希算法速度更快;同時(shí)標(biāo)準(zhǔn)庫為了指針穩(wěn)定性用了更復(fù)雜更占用內(nèi)存的存儲(chǔ)結(jié)構(gòu),在其上存入元素相比glibc使用數(shù)組+線性探測法實(shí)現(xiàn)的hsearch來說性能上肯定會(huì)打折扣。而查找更慢的原因則是因?yàn)間libc的實(shí)現(xiàn)使用數(shù)組使得鍵值對緊密排列對緩存更友好,而標(biāo)準(zhǔn)庫在緩存友好性上欠佳,這一點(diǎn)在小樣本上性能差距接近一倍而大樣本上差距縮減到不到五分之一上也可以體現(xiàn)出來,因?yàn)闃颖玖看罅酥罄螩PU的緩存壓力也顯著上升并影響到性能了。
然而在擁有更快運(yùn)算速度和更大的高速緩存的新CPU上,結(jié)果就又不一樣了:
小樣本:

大樣本:

小樣本上除了插入和contains,其他操作的性能已經(jīng)基本相同。在小樣本上緩存友好性依然是性能的重要影響因素,因此對緩存更友好的glibc的hsearch在性能上繼續(xù)保持這兩三倍的優(yōu)勢。
不過隨著數(shù)據(jù)量的上升,新CPU的大緩存就能發(fā)揮出優(yōu)勢了,在大樣本上除了插入之外其他的操作兩者相差無幾,甚至于標(biāo)準(zhǔn)庫會(huì)稍快一些。插入的差距也不像小樣本那樣有兩到三倍,現(xiàn)在縮減到兩倍以內(nèi)了,不過由于算法不同性能始終還是無法勝過hsearch的插入。
適用場景
我的建議是最好別用hsearch,尤其是用c++的時(shí)候,c++不管是標(biāo)準(zhǔn)庫還是boost、folly這樣的第三方庫都提供了更安全更好用的哈希表,沒必要用些堪比原始人的打制石器的接口來折磨自己。
所以這東西基本沒啥必須要用的場景,只要知道有這些接口并且大致知道怎么回事就足夠了。
總結(jié)
hsearch很形象地詮釋了什么叫雞肋。
最后做個(gè)特性對比作為結(jié)尾:
| 特性 | hsearch | std::unordered_map |
|---|---|---|
| 性能 | 高,存儲(chǔ)大量數(shù)據(jù)表現(xiàn)稍差 | 高,插入性能稍低 |
| 擴(kuò)容 | ? | ? |
| 更新元素 | ? | ? |
| 查找元素 | ? | ? |
| 刪除元素 | ? | ? |
| 遍歷 | ? | ? |
| 獲取元素個(gè)數(shù)1 | ? | ? |
| key元素類型 | 只能是c風(fēng)格字符串 | 可以是任意符合要求的類型 |
| value元素類型 | 函數(shù)指針外的任意類型(函數(shù)指針轉(zhuǎn)void*不具備可移植性) |
可以是任意符合要求的類型,包括函數(shù)指針 |
| 元素生命周期 | 需要自己管理 | 容器接管 |
| 易用性 | 差 | 中等 |
| 類型安全 | 不安全 | 較為安全 |
| 線程安全 | 不安全 | 不安全 |
| 跨平臺(tái)2 | ?? | ? |
1 hsearch本身不能獲取已存放元素個(gè)數(shù),但我們包裝后的可以
2 雖然glibc和muslc都支持hsearch使得能在大部分Linux/bsd系統(tǒng)上用,但它們在api的行為上有不同,所以跨平臺(tái)只算跨了一半


浙公網(wǎng)安備 33010602011771號