C++ std::unordered_map使用std::string和char *作key對比


  最近在給自己的服務器框架加上統計信息,其中一項就是統計創建的對象數,以及當前還存在的對象數,那么自然以對象名字作key。但寫着寫着,忽然糾結是用std::string還是const char *作key,哪個效率高些。由於這服務器框架業務邏輯全在lua腳本,在C++需要統計的對象沒幾個,其實用哪個沒多大區別。我糾結的是,很久之前就知道這兩者效率區別不大,但直到現在我都還沒搞清楚為啥,於是寫些代碼來測試。

V1版本的代碼如下:

#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__ */
View Code
#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;
}
View Code

上面的代碼直接使用const char *為key,MurmurHash2作為字符串hash算法(這個是stl默認的字符串hash算法),使用strcmp對比字符串。在key長為16,CPU為I5,虛擬機debian7運行情況下,效率區別真的不大:

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來分析下:

第一張圖是用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的效率高,所以白忙活。

  然后自己還是不死心,網上(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那邊創建、查詢時就慢了。

  周末在家,用自己的筆記本繼續測了一次,A8 APU,Ubuntu14.04,非虛擬機。意外地發現,差別就比較大了。

// 使用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.

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM