您的位置:首页 > Web前端 > Node.js

btHashMap vs std::unodered_map ——两种hashmap的性能对比测试

2015-07-25 11:41 926 查看
本篇补上《bullet HashMap 内存紧密的哈希表》欠下的债(下面简称《btHashMap》)。

《btHashMap》一文只是从理论上分析了bullet hash map(
btHashMap
)和C++标准库 hash map(
std::unordered_map
)的内存布局。
btHashMap
std::unordered_map
和多数语音环境的字典一样,都被设计为能够动态增长的容器;我在《btHashMap》断言了——在size较小的时,
btHashMap
相对
std::unordered_map
有更好的性能;但并没有指出——size在什么样的数量级
btHashMap
会有更好的性能表现,以及这个数量可能和那些环境参数相关?本篇将用实验(测试代码)和数据(测试结果)回答这两个 问题。

bullet源码最近也迁移到github了,github连接:https://github.com/bulletphysics/bullet3

本文的全部测试代码:

github.com: https://github.com/xusiwei/HashMapBenchmark

(备用https://code.csdn.net/xusiwei1236/bthashmapbenchmark

热身,让btHashMap为我所用

btHashMap
是bullet项目的一部分,那么第一个问题来了——怎么把它用起来?

btHashMap
的定义和声明都位于
src/LinearMath/btHashMap.h
,根据源码不难发现,它还依赖
btAlignedObjectArray.h``btAlignedAllocator.h
,
btAlignedAllocator.cpp
,
btScalar.h
。有了这些文件,
btHashMap
就可以正常工作了。下面从一个简单的例子开始,看看如何把它用起来。

查看
btHashMap
的源码,可以发现
btHashMap
的key依赖于有
.getHash()
得到hash值, 也可以发现
btHashMap.h
中定义了几个用于做key的类,如
btHashInt
,
btHashString


有了这些基础,可以轻松的写出一个demo,如下

warmUp.cpp
:

// btHashMap warm up example, by xu, http://blog.csdn.net/xusiwei1236 #include "btHashMap.h"

#include <stdio.h>

int main()
{
    btHashMap<btHashInt, btHashInt> btMap;

    int k = 1234, v = 5678;
    btMap.insert(btHashInt(k), btHashInt(v));

    btHashInt* pVal = btMap.find(btHashInt(k));
    if(pVal == NULL) {
        printf("key: %d not found in btMap\n", k);
    }
    else {
        printf("found key: %d, value: %d in btMap\n", k, v);
    }

    return 0;
}


现在,当前目录下已有6个文件:

xusiwei1236@blog.csdn.net:~/data/test/btHashMap$ ls
btAlignedAllocator.cpp  btAlignedAllocator.h  btAlignedObjectArray.h  btHashMap.h  btScalar.h  warmUp.cpp


尝试编译
warmUp.cpp
:

xusiwei1236@blog.csdn.net:~/data/test/btHashMap$ g++ warmUp.cpp 
/tmp/ccZ7i4Vi.o: In function `btAlignedAllocator<int, 16u>::deallocate(int*)':
warmUp.cpp:(.text._ZN18btAlignedAllocatorIiLj16EE10deallocateEPi[btAlignedAllocator<int, 16u>::deallocate(int*)]+0x18): undefined reference to `btAlignedFreeInternal(void*)'
/tmp/ccZ7i4Vi.o: In function `btAlignedAllocator<btHashInt, 16u>::deallocate(btHashInt*)':
warmUp.cpp:(.text._ZN18btAlignedAllocatorI9btHashIntLj16EE10deallocateEPS0_[btAlignedAllocator<btHashInt, 16u>::deallocate(btHashInt*)]+0x18): undefined reference to `btAlignedFreeInternal(void*)'
/tmp/ccZ7i4Vi.o: In function `btAlignedAllocator<btHashInt, 16u>::allocate(int, btHashInt const**)':
warmUp.cpp:(.text._ZN18btAlignedAllocatorI9btHashIntLj16EE8allocateEiPPKS0_[btAlignedAllocator<btHashInt, 16u>::allocate(int, btHashInt const**)]+0x25): undefined reference to `btAlignedAllocInternal(unsigned long, int)'
/tmp/ccZ7i4Vi.o: In function `btAlignedAllocator<int, 16u>::allocate(int, int const**)':
warmUp.cpp:(.text._ZN18btAlignedAllocatorIiLj16EE8allocateEiPPKi[btAlignedAllocator<int, 16u>::allocate(int, int const**)]+0x25): undefined reference to `btAlignedAllocInternal(unsigned long, int)'
collect2: ld returned 1 exit status


出现链接错误,
btAlignedAllocator
的成员函数没有找到,因为
btAlignedAllocator
的成员函数是在
.cpp
中实现的,所以需要先单独编译,再进行链接:

xusiwei1236@blog.csdn.net:~/data/test/btHashMap$ g++ -c btAlignedAllocator.cpp
xusiwei1236@blog.csdn.net:~/data/test/btHashMap$ ls btAlignedAllocator.*
btAlignedAllocator.cpp  btAlignedAllocator.h  btAlignedAllocator.o
xusiwei1236@blog.csdn.net:~/data/test/btHashMap$ g++ -c warmUp.cpp 
xusiwei1236@blog.csdn.net:~/data/test/btHashMap$ ls warmUp.*
warmUp.cpp  warmUp.o
xusiwei1236@blog.csdn.net:~/data/test/btHashMap$ g++ warmUp.o btAlignedAllocator.o
xusiwei1236@blog.csdn.net:~/data/test/btHashMap$ ls
a.out


生成了
a.out
,运行:

xusiwei1236@blog.csdn.net:~/data/test/btHashMap$ ./a.out 
found key: 1234, value: 5678 in btMap


关于计时

对于程序的时间性能的度量,需要考虑使用实际使用时间(
real time
)还是进程的CPU时间(
process CPU time
)。二者的区别在于,实际时间包括了测试进程所用的CPU时间(还包括测试进程休眠、进程调度等时间)。

从实际用户角度出发的测试场景,需要用实际时间;而对于细节性算法的测量,往往需要用进程的CPU时间来衡量(更能体现算法本身的优劣)。

获得进程所占的CPU时间

C/C++ tip: How to measure CPU time for benchmarking(以下简称HMCT,此文详细介绍了如何在常见的操作系统平台上获得进程的CPU时间,并实现一个跨平台的CPU时间测量函数
getCPUTime
)中说到:

A process’s CPU time accumulates as the process runs and consumes CPU cycles. During I/O operations, thread locks, and other operations that cause the process to pause, CPU time accumulation also pauses until the process can again make headway.

一个进程的CPU时间累计了进程运行所消耗的CPU周期数(PS:周期,这种说法是针对频率固定的CPU的,对当今主流的能够变频的CPU应该说时间)。I/O操作、线程锁住(挂起)、其他引起进程挂起的操作期间CPU时间的累计都会暂停,直到进程再次执行。

这里简单总结一下POSIX平台上都具有的两个计时函数
clock
clock_gettime
:

clock
是ISO C89标准中规定的函数,它的声明位于C标准库的
<time.h>
(对应C++标准库的
<ctime>
)里:

clock_t clock ( void );


主流的几个平台上都有,但在不同的平台上,返回值意义、
clock_t
的实际类型可能略有不同,多数是:从程序启动到调用
clock()
始终走过的滴答数,常量
CLOCKS_PER_SEC
定义了一秒内的滴答数。另外,《HMCT》指出Windows上,
clock()
返回的墙钟时间,而非进程已启动的时间。

clock_gettime
是POSIX规定的,它的声明一般也位于
time.h
里:

int clock_gettime (clockid_t __clock_id, struct timespec *__tp);


它相对
clock
的一个明显的好处是可以得到更高的时间精度。所有POSIX兼容的OS上都有该函数和
struct timesepc
,但在不同的OS上对应的
clockid
参数有所区别,如Linux可用
CLOCK_PROCESS_CPUTIME_ID
clockid
参数获得进程的CPU时间。

clock()
对应的tick数
CLOCKS_PER_SEC
在C89, C99标准都规定为1000,000,在glibc中也是该值, 理论上计时精度应该能够达到
1ms
,而我实际测得的计时精度能只有10ms(Ubuntu 12.04, 内核版本 3.11.0-26,gcc版4.6.3)。

《HMCT》还给出了一份可以兼容不同操作系统
getCPUTime()
的实现,函数接口声明如下:

double getCPUTime();


这里不再列出(可以在原文中找到)。

timer

根据《HMCT》的
getCPUTime()
,包装的一个用于计时的
timer
类(仿照
boost::timer
):

// modify from boost::timer, by xu, http://blog.csdn.net/xusiwei1236 class timer
{
 public:
         timer() { _start_time = getCPUTime(); }
  void   restart() { _start_time = getCPUTime(); }
  double elapsed() const // return elapsed time in seconds
    { return  getCPUTime() - _start_time; }

 private:
  double _start_time;
}; // timer


几种应用场景

下面构造了几个具体的测试场景,并逐步完善。虽然是yy, :-)

benchmark 1 单词统计.

“单词统计”————此测试来源于《编程珠玑》,读取一个文本文件,并对文件中的单词出现的频率进行统计。

这里对其稍作修改,留了统计过程(含插入、查找操作),删除了输出整个map(迭代操作),核心部分伪代码(python-style):

for word in text:
    if dict.find(word):
        dict[word] += 1
    else:
        dict.insert(word, 0)


btHashMap
std::unordered_map
在find和insert的参数和返回值上略有不同:

这是
std::unordered_map
版的:

// C++11 auto key word, to indicates std::unordered_map<std::string, int>::iterator
        auto pos = dict.find(word);
        if(pos != dict.end()) { // found
            pos->second++;
        }
        else { // not found
            dict.insert(std::make_pair(word, 1));
        }


这是
btHashMap
版的:

btHashString key(word); // btHashMap not supprt std::string.
        btHashInt* val = btDict.find(key);
        if(val != NULL) {
            val->setUid1(val->getUid1() + 1);
        }
        else {
            btDict.insert(key, btHashInt(1));
        }


btHashMap
的完整测试:

void btBench(const char* text, int length)
{
    btHashMap<btHashString, btHashInt> btDict;

    int count = 0;
    int cursor = 0;
    char word[256];

    timer t;
    do {
        cursor += took(&text[cursor], word, NULL); // took next word.
        if(!word[0]) break; // no more word.

        count++;

        btHashString key(word);
        btHashInt* val = btDict.find(key); // lookup
        if(val != NULL) { // found
            val->setUid1(val->getUid1() + 1);
        }
        else { // not found
            btDict.insert(key, btHashInt(1));
        }
    }while(cursor < length);
    double timeUsed = t.elapsed();

    printf("%9s: time used: %.3f, word tooks: %d\n", __func__, timeUsed, count);
}


(std的类似,略)

测试程序通过命令行传入一个文本文件名,先将整个文件读入内存,在依次抽取单词进行单词统计,具体代码见benchmark.cpp。

用牛津词典作为输入,测得的一组数据如下:

stdBench: time used: 0.196, word tooks: 695882
  btBench: time used: 0.061, word tooks: 695882


统计了69万多个单词,
btHashMap
明显快于
std::unordered_map


由于
std::map
的接口和
std::unordered_map
相同,换成
std::map
的测试结果:

stdBench: time used: 0.687, word tooks: 695882
  btBench: time used: 0.061, word tooks: 695882


可以发现,
std::map
如预想的比
std::unordered_map
慢,因为
std::map
的底层实现是红黑树,find/insert的平均复杂度都是O(log2 n);而
std::unordered_map
是哈希表,find/insert的平均复杂度都是O(1).

实际上,此例中对于同一单词,在两个版本的find所用的hash算法并不相同(
btHashMap
用的是
btHashString::getHash
,
std::unordered_map
用的是
std::hash<std::string>
)。

benchmark 2 随机数统计

在benchmark 1中,考虑到两个hash map用的字符串hash算法不同,对测得的结果可能略有影响(其实影响很小)。这里索性将
string
换成int做key,这样测出的性能参数就和hash算法无关了。

那么问题来了————数据从哪来,当然可以从命令行读入,但那让人感觉太low了。

干脆用随机数来干,可以用从一个seed开始,生成两次随机序列(rand能够保证生成的是同一个随机序列)。

btHashMap
版(
std::unordered_map
类似,不贴了)

double btBench(int seed, long tests)
{
    btHashMap<btHashInt, btHashInt> dict;

    srand(seed); // setup random seed.

    timer t;
    for(long i = 0; i < tests; ++i) {
        int r = rand(); // generate random int.

        // lookup in the hash map.
        btHashInt* val = dict.find(btHashInt(r));

        if(val != NULL) { // found, update directly.
            val->setUid1(val->getUid1() + 1);
        }
        else { // not found, insert <key, 1>
            dict.insert(key, btHashInt(1));
        }
    }
    return t.elapsed();
}


测试程序通过命令行参数传入测试次数,打印测试次数和时间,具体代码见benchmarkII.cpp.

如下命令,执行多次测试程序,并传入不通测试次数,得到结果:

$ for ((i = 2048; i <= 2**27; i *= 2)); do ./b2 $i; done
       2048  0.001   0.000
       4096  0.004   0.001
       8192  0.005   0.002
      16384  0.006   0.004
      32768  0.022   0.007
      65536  0.023   0.014
     131072  0.054   0.029
     262144  0.126   0.065
     524288  0.289   0.175
    1048576  0.509   0.434
    2097152  1.056   1.038
    4194304  2.182   2.314
    8388608  4.549   4.935
   16777216  9.307   10.411
   33554432  20.002  21.396
   67108864  41.641  47.422
  134217728  90.582  104.393


从这组数据可以看到,i<=2097152(2^21),
btHashMap
都是要比
std::unordered_map
表现的要好的。下面分析为什么当size达到一定数量的时候,
btHashMap
的性能表现就不如
std::unordered_map
了。

btHashMap
std::unordered_map
的rehash成本分析

首先明确一下,rehash并不是所有的hash table都需要的,它只在可以动态增长的hash table上需要,多数语音环境的hashmap是可以动态增长的,也就是需要rehash的。

当hash map的当前所持有的内存不足以放下新的元素时,就需要从新申请更多的内存,并维护好原有的逻辑关系,这就是哈希表的rehash。

从上面的测试结果数据可以看到,当size较大时
btHashMap
的表现要差于
std::unordered_map
。这是因为二者内存布局设计上的差异,导致size较大时二者的rehash开销的不同。

回顾一下,
std::unordered_map
的内存布局是”教科书式”的——一个叫做buckets的表头,每个slot下面挂着哈希值等于其index的所有

benchmark 3 find/insert单独测试

benchmark 2中,测试过程中find/insert是交叉的,且难以预测。所以这里再做一个单纯性的find/insert性能测试:

btHashMap<btHashInt, btHashInt> btDict;

double btInsertBench(int seed, long tests)
{
    srand(seed);

    timer t;
    for(long i = 0; i < tests; ++i) {
        btDict.insert(btHashInt(rand()), btHashInt(1));
    }
    return t.elapsed();
}

double btFindBench(int seed, long tests)
{
    srand(seed);

    timer t;
    for(long i = 0; i < tests; ++i) {
        btDict.find(btHashInt(rand()));
    }
    return t.elapsed();
}


测试时,先用
btInsertBench
向btDict中填充数据,再用
btFindBench
find
性能数据。

测量单位,前面的测试都在用总时间作为“速度”的度量,这里换个更加直观的速率单位: OPS(operation per second):

double stdInsertTime = stdInsertBench(seed, tests);
    double stdFindTime   = stdFindBench(seed, tests);

    double btInsertTime = btInsertBench(seed, tests);
    double btFindTime   = btFindBench(seed, tests);

    // printf("%11d\t% 5.3f\t% 5.3f\t% 5.3f\t% 5.3f\n", tests, stdInsertTime, stdFindTime, btInsertTime, btFindTime); // in total times
    printf("%11ld\t%11ld\t%11d\t%.0f\t%.0f\t%.0f\t%.0f\n", 
        tests, stdDict.size(), btDict.size(), tests/stdInsertTime, tests/stdFindTime, tests/btInsertTime, tests/btFindTime); // in OPS


参考

转载请注明出处(http://blog.csdn.net/xusiwei1236)及原文链接,欢迎评论或email交流观点。

C/C++ tip: How to measure CPU time for benchmarking
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: