2009 英特尔® 线程挑战赛—查找
问题描述
问题描述:写一个线程程序在一个线性存储且有序的唯一关键字集合中搜索给定关键字集合的所在位置。关键字是由15个字符组成的字符串,第一个输入文本文件包含有序关键字集合。第二个输入文本文件包含不定数量的关键字,这些关键字将在第一个文件中的关键字集合里进行查询。对于第二个输入文件中的每一个关键字在输出文件中都应该有对应的单行输出,输出信息包括关键字以及其在第一个文件中的下标,下标从0开始。如果待搜索关键字不在第一个文件中,那么将在输出文件中打印没有找到的信息。程序所涉及到的文件名(包括关键字集合文件,待搜索关键字集合文件,输出文件)将在命令行中给出。
文件格式:第一个输入文件的第一行是一个整数,代表文件中有序关键字集合的关键字数量(这里用N表示)。接下来存储的就是N行有序的关键字,每个关键字是15个字符的字符串。第二个文件包括待查询的不定数量的关键字集合,关键字同样是15字符的字符串,每行存储一个关键字。
输出文件应当与第二个输入文件有相同的输出行数。对于第二个文件中列出的每个关键字,在输出文件中都有对应的一行输出信息表明待搜索关键字以及在第一个文件中的下标或者未找到的信息。
输入文件样例 (data.txt):
10
123456789012345
AABBCCDDEEFFGGH
MMNNNNNNNNNNNNN
MMNNNNNNNNNNNNO
NNNNNNNNNN12345
NNNNNNNNNN12346
This is a key22
aabbCCDDEEFFGGh
mMNNNNNNNNNNNNN
not the lastkey
输入文件样例 (keysearch.txt):
MMNNNNNNNNNNNNN
NOT the lastkey
mMNNNNNNNNNNNNN
AABBCCDDEEFFGGH
AABBCCDDEEFFGG0
命令行样例:
> mysearch.exe data.txt keysearch.txt results.txt
输出文件样例 (results.txt):
MMNNNNNNNNNNNNN is found at index 2
NOT the lastkey is NOT FOUND
mMNNNNNNNNNNNNN is found at index 8
AABBCCDDEEFFGGH is found at index 1
AABBCCDDEEFFGG0 in NOT FOUND
限制条件:第一个输入文件中要搜索的数据,必须首先被读入一个一维数组,数组中每个元素包含一个关键字。下面的伪代码说明了一个可能的输入算法。
readfile(inputFD, &N);
for i = 0, N-1 {
A[i] = getkey(inputFD);
}
计时:如果你把计时代码放入你的应用程序中来对第二个文件关键字的输入,搜索和输出过程进行计时,然后打印所花时间,这个时间将用于计分。如果没有添加计时代码,整个执行时间(包括输入和输出的时间)将用于计分。如果没有遵循输入数据和存储的输入限制,整个执行时间将用于计分。
串行算法
本题为查找算法题,可选的查找算法很多,有顺序查找、二分查找、HashMap等等。
顺序查找算法是将待查找的值依次与数组中每一个值比较,相等即查找成功,否则查找失败,顺序查找是O(N)的算法,不需要额外的空间。
二分查找算法只能在已排序的数据中查找,所以首先是将数据排序,然后取中间的值与待查找值进行比较,相等即查找成功。否则根据比较的结果在一半的数据中递归查找。二分查找是O(logN)的算法,不需要额外的空间。
HashMap查找算法通过Hash函数计算Hash值将数据分散到桶中,每个桶里的数据Hash值相同。不同值有相同Hash值我们称为冲突,冲突会导致桶中的数据增加。查找时利用Hash函数得到待查找值的Hash值,在这个Hash值限定桶中进行顺序查找,理论上该算法是O(1)的。如果数据集增大或者Hash函数不够好,冲突会增加,导致桶内数据增加,效率也会随之降低。如果所有数据的Hash值相同,则退化为顺序查找。
由于本题的输入是已经排序的字符串,所以首选当然是二分查找,但它是O(logN)的,理论上不及HashMap查找,但HashMap需要构建时间,性能与数据量也有关系。所以使用什么算法是需要根据数据量来调整的。
每个关键字为15字节,可以放入到__m128i中,于是定义XSearchItem结构表示关键字
// 关键字数据
typedef struct tagXSearchItem
{
union
{
char cVal[16];
uint16 nVal16[8];
uint32 nVal32[4];
uint64 nVal64[2];
__m128i nVal128;
};
tagXSearchItem(__m128i nV128) { nVal128 = nV128; }
}XSearchItem,*PXSEARCHITEM;
实现了三个版本的算法。
二分查找 :见XSearch_Binary.cpp中的XSearch_Binary_Serial函数。
HashMap: 见XSearch_HashMap.cpp中的XSearch_HashMap_Serial函数。
concurrent_hash_map:见XSearch_HashMap_TBB.cpp中的XSearch_HashMap_TBB_
Serial函数。
并行算法
对应的实现了三种算法的并行版本。
二分查找 :
使用多个线程,分别处理一部分待查关键字即可实现二分查找的并行算法。
见XSearch_Binary.cpp中的XSearch_Binary_Parallel函数。
HashMap:
在构造阶段,每个线程分别构造桶数量相同的HashMap,构造完毕后,使用多个线程进行桶的合并。然后使用多个线程,分别处理一部分待查关键字即可实现HashMap查找的并行算法。
见XSearch_HashMap.cpp中的XSearch_HashMap_Parallel函数。
concurrent_hash_map:
concurrent_hash_map本身就是支持并行的数据结构,所以我们只需开启多个线程同时向concurrent_hash_map插入数据,构造完concurrent_hash_map后使用多个线程,分别处理一部分待查关键字即可实现concurrent_hash_map查找的并行算法。
见XSearch_HashMap_TBB.cpp中的XSearch_HashMap_TBB_Parallel函数。
优化工具
Hotspots检测
使用Intel Amplifier的Hotspots检测功能查找二分查找算法的热点函数,结果如下:
二分查找的时间开销基本上都在关键字比较函数XCompareItem中,最初的使用的字符串比较函数替换为汇编版本的比较函数,该函数每次将4个字节的数据通过bswap转化为一个int值后进行比较,大幅度的提高了程序性能。
使用Intel Amplifier的Hotspots检测功能查找HashMap算法的热点函数,结果如下:
主要的时间开销都在函数XSearch_HashMap_Parallel内。
Concurrency检测
使用Intel Amplifier的Concurrency检测功能查找可进行并行优化的代码,结果如下:
根据检测结果可知,二分查找算法几乎实现了完全并行。
检测结果显示在XSearch_HashMap_Parallel内存在较多的串行代码,进入函数内部可以以找到串行执行的代码行如下:
串行执行时间均为分配内存和释放内存开销。由于需要构造很多的HashMap所以内存需求较大,时间消耗也比较多,约为150ms。
Locks and Waits检测
使用Intel Amplifier的Locks and Waits检测功能查找两种算法的锁和同步等待消耗,结果如下:
检测结果显示两种算法不存在较严重的同步和锁消耗。
其他优化
1. 使用内存映射结合OpenMp并行的读取数据。
2. 使用_mm_loadu_si128加载每个关键字。
3. 统计每个分块数据的输出信息需要的空间大小,进而使用内存映射结合OpenMp并行的保存数据。
4. 使用汇编实现二分查找比较函数。
5. 使用XSearchItem结构的nVal64[2]判断关键字是否相同。
6. 根据查找数据和待查找数据的数据量自动选择二分查找和HashMap查找算法。
性能测试
小数据量测试:
操作系统: 32bit的测试在32位XP下完成。
CPU: Intel(R) Core(TM)2 CPU 5270 @ 1.40GHz
内存: 1G
时间单位: 秒
测试数据: DataCount_KeyCount
测试数据 | 二分查找 串行 | 二分查找 并行 | 加速比 | HashMap 串行 | HashMap 并行 | 加速比 |
100k_100k | 0.038339 | 0.024409 | 1.57 | 0.031137 | 0.032812 | 0.95 |
100k_200k | 0.075259 | 0.045982 | 1.64 | 0.055593 | 0.050704 | 1.10 |
100k_1M | 0.362107 | 0.218493 | 1.66 | 0.259891 | 0.167116 | 1.56 |
1M_1M | 0.700737 | 0.427367 | 1.64 | 0.500109 | 0.403246 | 1.24 |
1M_2M | 1.497653 | 0.853737 | 1.75 | 0.857886 | 0.603892 | 1.42 |
1M_5M | 3.437704 | 2.069611 | 1.66 | 1.867009 | 1.189546 | 1.57 |
5M_1M | 1.007964 | 0.616225 | 1.62 | 2.003100 | 1.257592 | 1.59 |
5M_2M | 2.005481 | 1.207397 | 1.66 | 2.989992 | 1.721885 | 1.73 |
5M_5M | 4.876103 | 2.981140 | 1.64 | 5.893172 | 3.201393 | 1.84 |
大数据测试:
操作系统: Red Hat Enterprise Linux AS release 4 (Nahant Update 2)
CPU: Intel(R) Core(TM)2 CPU 6320 @ 1.86GHz
内存: 4G
测试数据 | 10M_10M | 10M_20M | 20M_10M | 20M_20M |
测试结果 | 6.812783 | 13.501797 | 7.963913 | 18.479586 |
编译说明
Windows平台:
使用VS2008和Intel Parallel Studio
1. 用VS2008打开本项目.
2. 选择X64平台Relase编译.
3. 进入Bin目录执行文件为XSearch.exe.
Linux平台:
使用ICC和TBB
1. 上传压缩包种的Src和Linux两个目录到服务器上.
2. 进入XSearch/Linux目录 执行make
3. 进入XSearch/Bin目录 执行文件为XSearch.
其他:
主办方请使用Win32平台Release版本测试,需要使用Linux格式的输入文件。使用默认的Auto Parallel Mode【ap】运行模式,谢谢!
优化结论
通过解决本题进一步学习了查找算法,更深入的了解了二分查找和HashMap在多核系统上的优缺点、性能差异。
学习使用TBB提供的concurrent_hash_map,它是一个高性能的、线程安全的并行HashMap类,由于本题的数据是已经排序的,而且构造好HashMap后再进行查询,不存在并发的、反复的增加数据、查询数据。所以使用它来解答本题虽然很方便,但效率上不占优势。在实际的软件开发环境中强烈推荐使用。
同时在Linux下使用l_cc_p_10.1.015编译XSearch_HashMap_TBB.cpp是报了一个编译错误,时间关系没有继续查原因,不知是否存在Bug。 错误如下:
error: unknown opcode "pause;" -- __asm
致谢
感谢Clay Breshears所做的解答,感谢Mu,Pryce为本文章发表到ISN所做的工作,感谢Xia, JeffX P为我的解决方案进行了认真细致的翻译。