最近在給本身的服務器框架加上統計信息,其中一項就是統計建立的對象數,以及當前還存在的對象數,那麼天然以對象名字做key。但寫着寫着,突然糾結是用std::string仍是const char *做key,哪一個效率高些。因爲這服務器框架業務邏輯全在lua腳本,在C++須要統計的對象沒幾個,其實用哪一個沒多大區別。我糾結的是,好久以前就知道這二者效率區別不大,但直到如今我都還沒搞清楚爲啥,因而寫些代碼來測試。html
V1版本的代碼以下:node
#ifndef __MAP_H__ #define __MAP_H__ //-------------------------------------------------------------------------- // MurmurHash2, by Austin Appleby // Note - This code makes a few assumptions about how your machine behaves - // 1. We can read a 4-byte value from any address without crashing // 2. sizeof(int) == 4 // And it has a few limitations - // 1. It will not work incrementally. // 2. It will not produce the same results on little-endian and big-endian // machines. static inline unsigned int MurmurHash2 ( const void * key, int len, unsigned int seed ) { // 'm' and 'r' are mixing constants generated offline. // They're not really 'magic', they just happen to work well. const unsigned int m = 0x5bd1e995; const int r = 24; // Initialize the hash to a 'random' value unsigned int h = seed ^ len; // Mix 4 bytes at a time into the hash const unsigned char * data = (const unsigned char *)key; while(len >= 4) { unsigned int k = *(unsigned int *)data; k *= m; k ^= k >> r; k *= m; h *= m; h ^= k; data += 4; len -= 4; } // Handle the last few bytes of the input array switch(len) { case 3: h ^= data[2] << 16; case 2: h ^= data[1] << 8; case 1: h ^= data[0]; h *= m; }; // Do a few final mixes of the hash to ensure the last few // bytes are well-incorporated. h ^= h >> 13; h *= m; h ^= h >> 15; return h; } /* 自定義類型hash也能夠放到std::hash中,暫時不這樣作 * https://en.cppreference.com/w/cpp/utility/hash */ /* the default hash function in libstdc++:MurmurHashUnaligned2 * https://sites.google.com/site/murmurhash/ by Austin Appleby * other hash function(djb2,sdbm) http://www.cse.yorku.ca/~oz/hash.html */ struct hash_c_string { size_t operator()(const char *ctx) const { return MurmurHash2(ctx,strlen(ctx),static_cast<size_t>(0xc70f6907UL)); } }; /* compare function for const char* */ struct cmp_const_char { bool operator()(const char *a, const char *b) const { return std::strcmp(a, b) < 0; } }; /* compare function for const char* */ struct equal_c_string { bool operator()(const char *a, const char *b) const { return 0 == std::strcmp(a, b); } }; /* 須要使用hash map,但又但願能兼容舊版本時使用map_t */ #if __cplusplus < 201103L /* -std=gnu99 */ #include <map> #define map_t std::map #define const_char_map_t(T) std::map<const char *,T,cmp_const_char> #else /* if support C++ 2011 */ #include <unordered_map> #define map_t std::unordered_map // TODO:template<class T> using const_char_map_t = ...,但03版本不支持 #define const_char_map_t(T) \ std::unordered_map<const char *,T,hash_c_string,equal_c_string> #endif #endif /* __MAP_H__ */
#include <map> #include <ctime> #include <cstdio> #include <vector> #include <cstdlib> /* srand, rand */ #include <cstring> #include <iostream> #include "map_v1.h" #if(__cplusplus >= 201103L) # include <unordered_map> #else # include <tr1/unordered_map> namespace std { using std::tr1::unordered_map; } #endif #include <iostream> #define M_TS 1000 #define N_TS 1000 typedef std::pair<unsigned short,unsigned short> pair_key_t; struct pair_hash { unsigned int operator () (const pair_key_t& pk) const { return (0xffff0000 & (pk.first << 16)) | (0x0000ffff & pk.second); } }; struct pair_equal { bool operator () (const pair_key_t& a, const pair_key_t& b) const { return a.first == b.first && a.second == b.second; } }; struct pair_less { bool operator () (const pair_key_t& a, const pair_key_t& b) const { return (a.first < b.first) || (a.first == b.first && a.second < b.second); } }; typedef std::map< pair_key_t,unsigned int,pair_less > std_map_t; typedef std::unordered_map< pair_key_t,unsigned int,pair_hash,pair_equal > unordered_map_t; void run_unordered_map_test() { unordered_map_t _protocol; clock_t start = clock(); for ( unsigned short m = 0;m < M_TS;m ++ ) for ( unsigned short n = 0;n < N_TS;n ++ ) { unsigned int result = (0xffff0000 & (m << 16)) | (0x0000ffff & n); _protocol.insert( std::make_pair(std::make_pair(m,n),result) ); } std::cout << "unordered_map create cost " << (float(clock()-start))/CLOCKS_PER_SEC << std::endl; start = clock(); for ( unsigned short m = 0;m < M_TS;m ++ ) for ( unsigned short n = 0;n < N_TS;n ++ ) { unordered_map_t::iterator itr = _protocol.find( std::make_pair(m,n) ); if ( itr == _protocol.end() ) { std::cout << "unordered_map error" << std::endl; return; } } std::cout << "unordered_map find cost " << (float(clock()-start))/CLOCKS_PER_SEC << std::endl; } void run_std_map_test() { std_map_t _protocol; clock_t start = clock(); for ( unsigned short m = 0;m < M_TS;m ++ ) for ( unsigned short n = 0;n < N_TS;n ++ ) { unsigned int result = (0xffff0000 & (m << 16)) | (0x0000ffff & n); _protocol.insert( std::make_pair(std::make_pair(m,n),result) ); } std::cout << "std_map create cost " << (float(clock()-start))/CLOCKS_PER_SEC << std::endl; start = clock(); for ( unsigned short m = 0;m < M_TS;m ++ ) for ( unsigned short n = 0;n < N_TS;n ++ ) { std_map_t::iterator itr = _protocol.find( std::make_pair(m,n) ); if ( itr == _protocol.end() ) { std::cout << "std_map error" << std::endl; return; } } std::cout << "std_map find cost " << (float(clock()-start))/CLOCKS_PER_SEC << std::endl; } void create_random_key(std::vector<std::string> &vt) { srand (time(NULL)); for (int idx = 0;idx < 10000000;idx ++) { int ikey = rand(); int ikey2 = rand(); char skey[64]; sprintf(skey,"%X%X",ikey,ikey2); vt.push_back(skey); } } void test_unorder_string(const std::vector<std::string> &vt) { std::unordered_map<std::string,int> test_map; clock_t start = clock(); for (int idx = 0;idx < vt.size();idx ++) { test_map[ vt[idx].c_str() ] = idx; } std::cout << "unorder_map std::string create cost " << (float(clock()-start))/CLOCKS_PER_SEC << std::endl; start = clock(); for (int idx = 0;idx < vt.size()/2;idx ++) { if (test_map.find(vt[idx].c_str()) == test_map.end()) { std::cout << "unorder_map std::string find fail" << std::endl; return; } } std::cout << "unorder_map std::string find cost " << (float(clock()-start))/CLOCKS_PER_SEC << std::endl; } void test_unorder_char(const std::vector<std::string> &vt) { std::unordered_map<const char *,int,hash_c_string,equal_c_string> test_map; clock_t start = clock(); for (int idx = 0;idx < vt.size();idx ++) { test_map[ vt[idx].c_str() ] = idx; } std::cout << "unorder_map char create cost " << (float(clock()-start))/CLOCKS_PER_SEC << std::endl; start = clock(); for (int idx = 0;idx < vt.size()/2;idx ++) { if (test_map.find(vt[idx].c_str()) == test_map.end()) { std::cout << "unorder_map char find fail" << std::endl; return; } } std::cout << "unorder_map char find cost " << (float(clock()-start))/CLOCKS_PER_SEC << std::endl; } void test_stdmap_char(const std::vector<std::string> &vt) { std::map<const char *,int,cmp_const_char> test_map; clock_t start = clock(); for (int idx = 0;idx < vt.size();idx ++) { test_map[ vt[idx].c_str() ] = idx; } std::cout << "std_map char create cost " << (float(clock()-start))/CLOCKS_PER_SEC << std::endl; start = clock(); for (int idx = 0;idx < vt.size()/2;idx ++) { if (test_map.find(vt[idx].c_str()) == test_map.end()) { std::cout << "std_map char find fail" << std::endl; return; } } std::cout << "std_map char find cost " << (float(clock()-start))/CLOCKS_PER_SEC << std::endl; } void test_stdmap_string(const std::vector<std::string> &vt) { std::map<std::string,int> test_map; clock_t start = clock(); for (int idx = 0;idx < vt.size();idx ++) { test_map[ vt[idx] ] = idx; } std::cout << "std_map string create cost " << (float(clock()-start))/CLOCKS_PER_SEC << std::endl; start = clock(); for (int idx = 0;idx < vt.size()/2;idx ++) { if (test_map.find(vt[idx]) == test_map.end()) { std::cout << "std_map char find fail" << std::endl; return; } } std::cout << "std_map string find cost " << (float(clock()-start))/CLOCKS_PER_SEC << std::endl; } /* unordered_map create cost 0.08 unordered_map find cost 0.01 std_map create cost 0.28 std_map find cost 0.13 key爲10000000時: std_map char create cost 31.73 std_map char find cost 15.69 std_map string create cost 56.44 std_map string find cost 28.48 unorder_map char create cost 11.61 unorder_map char find cost 1.17 unorder_map std::string create cost 11.75 unorder_map std::string find cost 2.13 key爲100000時 std_map char create cost 0.09 std_map char find cost 0.04 std_map string create cost 0.15 std_map string find cost 0.08 unorder_map char create cost 0.03 unorder_map char find cost 0.01 unorder_map std::string create cost 0.06 unorder_map std::string find cost 0.02 */ // valgrind --tool=callgrind --instr-atstart=no ./unoder_map int main() { //run_unordered_map_test(); //run_std_map_test(); std::vector<std::string> vt; create_random_key(vt); //test_stdmap_char(vt); //test_stdmap_string(vt); int idx = 0; // wait valgrind:callgrind_control -i on //std::cin >> idx; test_unorder_char(vt); test_unorder_string(vt); //std::cin >> idx; return 0; }
上面的代碼直接使用const char *爲key,MurmurHash2做爲字符串hash算法(這個是stl默認的字符串hash算法),使用strcmp對比字符串。在key長爲16,CPU爲I5,虛擬機debian7運行狀況下,效率區別真的不大:ios
key爲100000時: unorder_map char create cost 0.03 unorder_map char find cost 0.01 unorder_map std::string create cost 0.06 unorder_map std::string find cost 0.02 key爲10000000時: unorder_map char create cost 11.61 unorder_map char find cost 1.17 unorder_map std::string create cost 11.75 unorder_map std::string find cost 2.13
看到這個結果我是真的有點不服氣了。畢竟std::string是一個複雜的結構,怎麼也應該慢比較多才對。因而拿出了valgrind來分析下:c++
第一張圖是用const char*做key的,第二張則是用std::string做key的。能夠看到除去std::unordered_map的構造函數,剩下的基本是hash、operator new這兩個函數佔時間了。在const char*做key的時,hash函數佔了22%,new函數佔9.66%,而std::string時,new佔了15.42,hash才9.72%,所以這二者的效率沒差多少。算法
看到本身的hash函數寫得太差,尋思着改一下。hash函數由兩部分構成,一部分是用strlen算長度,另外一部分是算hash,那麼咱們能夠優化一下,把這兩個變量記一下,就不用常常調用了。服務器
struct c_string { size_t _length; unsigned int _hash; const char *_raw_ctx; c_string(const char *ctx) { _raw_ctx = ctx; _length = strlen(ctx); _hash = MurmurHash2(ctx,_length,static_cast<size_t>(0xc70f6907UL)); } };
而後再試了下,結果發現差異基本是同樣的。沒錯,hash的消耗是下來了,但是new的佔比又上去了。畢竟struct c_string這個結構和std::string這個結構沒差多少啊,本身的hash函數仍是沒STL的效率高,因此白忙活。app
而後本身仍是不死心,網上(http://www.cse.yorku.ca/~oz/hash.html)查了下,換了幾個hash函數,連strlen都去掉了框架
// djb2 unsigned long hash(unsigned char *str) { unsigned long hash = 5381; int c; while (c = *str++) hash = ((hash << 5) + hash) + c; /* hash * 33 + c */ return hash; } // sdbm static unsigned long sdbm(str) unsigned char *str; { unsigned long hash = 0; int c; while (c = *str++) hash = c + (hash << 6) + (hash << 16) - hash; return hash; }
發現也並無發生質的變化。hash函數複雜一些,效率慢一些,但衝突就少了。簡單了,衝突多了,std::unordered_map那邊建立、查詢時就慢了。less
週末在家,用本身的筆記本繼續測了一次,A8 APU,Ubuntu14.04,非虛擬機。意外地發現,差異就比較大了。dom
// 使用const char*,key爲10000000時: unorder_map char create cost 11.7611 unorder_map char find cost 1.55619 unorder_map std::string create cost 13.5376 unorder_map std::string find cost 2.33906 // 使用struct c_string,key爲10000000時: unorder_map char create cost 7.35524 unorder_map char find cost 1.60826 unorder_map std::string create cost 14.6082 unorder_map std::string find cost 2.53137
能夠看到,以c_string爲key時,效率就比較高了。由於個人筆記本APU明顯比不上I5,算hash函數就慢多了,可是內存分配沒慢多少。
std::string仍是const char *做key區別確實不大,影響的因素太多:
1. hash函數的效率和衝突機率。你本身很難寫出一個比STL更好的hash函數,STL是有作優化的,好比strlen調用的是__strlen_sse42,是用了SSE指令優化的
2. 用不一樣的結構,在不一樣的CPU和內存分配效率,基於不一樣的操做系統實現,這個都會有不一樣的表現
3. 你本身是經過什麼結構去構造、查詢。用std::string時,建立和查詢時實際上是一個引用。若是你傳入的是std::string,可能並不會建立對象。但傳入char*就會先構造一對象
4. 用char*還要注意引用的字符串生命週期問題
最終,仍是引用網上那幾句話:
don't use a char * as a key std::string keys are never your bottleneck the performance difference between a char * and a std::string is a myth.