btHashMap vs std::unodered_map ——两种hashmap的性能对比测试
2015-07-25 11:41
926 查看
本篇补上《bullet HashMap 内存紧密的哈希表》欠下的债(下面简称《btHashMap》)。
《btHashMap》一文只是从理论上分析了bullet hash map(
bullet源码最近也迁移到github了,github连接:https://github.com/bulletphysics/bullet3
本文的全部测试代码:
github.com: https://github.com/xusiwei/HashMapBenchmark
(备用https://code.csdn.net/xusiwei1236/bthashmapbenchmark)
查看
有了这些基础,可以轻松的写出一个demo,如下
现在,当前目录下已有6个文件:
尝试编译
出现链接错误,
生成了
从实际用户角度出发的测试场景,需要用实际时间;而对于细节性算法的测量,往往需要用进程的CPU时间来衡量(更能体现算法本身的优劣)。
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平台上都具有的两个计时函数
主流的几个平台上都有,但在不同的平台上,返回值意义、
它相对
《HMCT》还给出了一份可以兼容不同操作系统
这里不再列出(可以在原文中找到)。
这里对其稍作修改,留了统计过程(含插入、查找操作),删除了输出整个map(迭代操作),核心部分伪代码(python-style):
这是
这是
(std的类似,略)
测试程序通过命令行传入一个文本文件名,先将整个文件读入内存,在依次抽取单词进行单词统计,具体代码见benchmark.cpp。
用牛津词典作为输入,测得的一组数据如下:
统计了69万多个单词,
由于
可以发现,
实际上,此例中对于同一单词,在两个版本的find所用的hash算法并不相同(
那么问题来了————数据从哪来,当然可以从命令行读入,但那让人感觉太low了。
干脆用随机数来干,可以用从一个seed开始,生成两次随机序列(rand能够保证生成的是同一个随机序列)。
测试程序通过命令行参数传入测试次数,打印测试次数和时间,具体代码见benchmarkII.cpp.
如下命令,执行多次测试程序,并传入不通测试次数,得到结果:
从这组数据可以看到,i<=2097152(2^21),
首先明确一下,rehash并不是所有的hash table都需要的,它只在可以动态增长的hash table上需要,多数语音环境的hashmap是可以动态增长的,也就是需要rehash的。
当hash map的当前所持有的内存不足以放下新的元素时,就需要从新申请更多的内存,并维护好原有的逻辑关系,这就是哈希表的rehash。
从上面的测试结果数据可以看到,当size较大时
回顾一下,
测试时,先用
测量单位,前面的测试都在用总时间作为“速度”的度量,这里换个更加直观的速率单位: OPS(operation per second):
C/C++ tip: How to measure CPU time for benchmarking
《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
相关文章推荐
- (解决问题)hadoop Live Nodes :0
- nodeJs 脚本 WatchPost.js 本地开发过程中直接与服务器进行文件同步
- CEAN.js (Couchbase + Express + AngularJS + Node.js示例)
- Delete Node in a Linked List
- LeetCode237:Delete Node in a Linked List
- Node系列——express项目搭建
- Node.js的函数返回值
- Swap Nodes in Pairs(C语言)
- [leetcode-]Remove Nth Node From End of List(C)
- [LeetCode] Delete Node in a Linked List
- NTVS:把Visual Studio变成Node.js IDE的工具 搜集资料。
- [LeetCode][Java] Populating Next Right Pointers in Each Node II
- Node:二叉树搜索
- Algorithms—25.Reverse Nodes in k-Group
- 【cheerio】nodejs的抓取页面模块
- LeetCode || Delete Node in a Linked List
- nodejs this
- 【LeetCode-面试算法经典-Java实现】【025-Reverse Nodes in k-Group(单链表中k个结点一组进行反转)】
- 【LeetCode-面试算法经典-Java实现】【024-Swap Nodes in Pairs(成对交换单链表的结点)】
- Mac上使用brew安装nvm来支持多版本的Nodejs