C++ 为什么矢量和地图搜索比静态比较慢得多

C++ 为什么矢量和地图搜索比静态比较慢得多,c++,caching,optimization,C++,Caching,Optimization,我正在解析大约1MB大小的文件,读取前300KB并搜索一些特定的签名。我的策略是,对于每个字节,查看该字节是否位于映射/向量/我知道可能位于签名开头的任何字节中,如果是,则查找完整签名-在本例中,假设这些前导字节是x37、x50和x52。处理总共90个文件(实际上是10次处理9个文件),以下代码在2.122秒内执行: byte * bp = &buffer[1]; const byte * endp = buffer + bytesRead - 30; /

我正在解析大约1MB大小的文件,读取前300KB并搜索一些特定的签名。我的策略是,对于每个字节,查看该字节是否位于映射/向量/我知道可能位于签名开头的任何字节中,如果是,则查找完整签名-在本例中,假设这些前导字节是x37、x50和x52。处理总共90个文件(实际上是10次处理9个文件),以下代码在2.122秒内执行:

        byte * bp = &buffer[1];
        const byte * endp = buffer + bytesRead - 30; // a little buffer for optimization - no signature is that long
        //multimap<byte, vector<FileSignature> >::iterator lb, ub;
        map<byte, vector<FileSignature> >::iterator findItr;
        vector<FileSignature>::iterator intItr;
        while (++bp != endp)
        {
            if (*bp == 0x50 || *bp == 0x52 || *bp == 0x37)  // Comparison line
            {
                findItr = mapSigs.find(*bp);
                for (intItr = findItr->second.begin(); intItr != findItr->second.begin(); intItr++)
                {
                    bool bMatch = true;
                    for (UINT i = 1; i < intItr->mSignature.size(); ++i)
                    {
                        if (intItr->mSignature[i] != bp[i])
                        {
                            bMatch = false;
                            break;
                        }
                    }

                    if (bMatch)
                    {
                        CloseHandle(fileHandle);
                        return true; 
                    }
                }
            }
        }
使用包含3个值的向量的非常类似的实现也会导致极慢的处理(190秒):

但是直接访问向量元素的实现执行得相当好(8.1秒)。虽然不如静态比较好,但仍远远优于其他选项:

if (vecFirstChars[0] == *bp || vecFirstChars[1] == *bp || vecFirstChars[2] == *bp)
{
    findItr = mapSigs.find(*bp);
    ...
迄今为止最快的实现(受下面组件10的启发)如下所示,以大约2.0秒的时间计时:

if (find(vecFirstChars.begin(), vecFirstChars.end(), *bp) != vecFirstChars.end())
{
    findItr = mapSigs.find(*bp);
    ...
bool validSigs[256] = {0};
validSigs[0x37] = true;
validSigs[0x50] = true;
validSigs[0x52] = true;

while (++bp != endp)
{
    if (validSigs[*bp])
    {
    ...
将此扩展为使用2个ValidSig来查看第2个字符是否有效,从而使总运行时间降低到0.4秒

我觉得其他的实现应该表现得更好。特别是map,它应该随着更多签名前缀的添加而缩放,搜索是O(log(n))vs O(n)。我错过了什么?我唯一的猜测是,通过静态比较和向量索引,我得到了缓存在寄存器或其他位置的用于比较的值,这使得它比从内存读取要快得多。如果这是真的,我能显式地告诉编译器特定的值将被经常使用吗?对于下面的代码,我是否可以利用任何其他不明显的优化


我使用Visual Studio 2008进行编译。

这非常简单,可以归结为执行的指令数。向量、映射或查找表将完全驻留在CPU级别1的数据缓存中,因此内存访问不会占用时间。对于查找表,只要大多数字节与签名前缀不匹配,分支预测器就会停止流控制占用时间。(但其他结构确实会产生流量控制开销。)

所以很简单,依次比较向量中的每个值需要3次比较。映射为O(logn),但由于在链接数据结构中导航,系数(被big-O符号忽略)较大。查找表是O(1),系数很小,因为对结构的访问可以通过一条机器指令完成,然后剩下的就是一次与零的比较

分析性能的最佳方法是使用探查器工具,如valgrind/kcachegrind。

与常量比较“与常量比较”将3个内存地址与3个常量进行比较。如果编译器愿意的话,这种情况将非常容易进行展开或位优化。这里编写的ASM将具有的唯一分支是高度可预测的

对于文字3元素向量查找,取消引用向量值的地址会带来额外的成本

对于向量循环,编译器此时不知道向量有多大。所以它必须编写一个通用循环。这个循环中有一个分支,一个分支朝一个方向走两次,然后朝另一个方向走。如果计算机使用启发式“分支按上次的方式进行”,这将导致大量分支预测失败

为了验证这一理论,请尝试使分支更可预测——一次搜索每个元素最多100个不同的输入字节,然后搜索下一个。这将使朴素的分支预测在98%的时间内工作,而不是代码中的33%。也就是说,扫描签名0的100(或任意)个字符,然后扫描签名1的100(或任意)个字符,直到签名用完为止。然后转到下一个100个字符的块以扫描签名。我之所以选择100,是因为我试图避免分支预测失败,我认为少数分支预测失败并不那么糟糕。:)


至于
map
解决方案,那么
map
s有很高的固定开销,因此速度慢是可以预测的。
map
的主要用途是处理大量的n次查找,事实上它们很容易编码。

只对三个值进行预筛选肯定会加快速度,因为它有效地排除了其他可能的253个已知未命中条件。地图上只有三个值,我真的不明白为什么你有一个。对这三个值(您已经在做)进行内联测试并点击相应的后续列表也会更快。如果这都是关于搜索子字符串(字节等)的,我建议您阅读算法,比如您是否考虑过使用稀疏向量作为签名值?听起来在这种情况下这可能是一个不错的优化,特别是如果你的签名范围是一个字节的话。我没有读过关于Knuth Morris Pratt的书-读起来很有趣,但不太适用。假设字节的分布是均匀的,我将在每256次比较中只看到3(或7)个字符的匹配。当我得到匹配时,后续每个字符匹配的可能性更低(1/256)。我认为这里没有什么好处@组件10我不知道稀疏向量会有帮助,但连续数组确实会加快速度-另一个案例更新了问题。它是调试构建还是优化构建?如果您只想使用哈希功能,而不关心它在
映射中的顺序,您应该使用
哈希映射。虽然仍然有一些复杂的过程,比如碰撞,但是对于每个查找,它应该比基于树的映射和
O(logN)
要好得多。很好的建议,但我不认为这解释了查找的原因
bool validSigs[256] = {0};
validSigs[0x37] = true;
validSigs[0x50] = true;
validSigs[0x52] = true;

while (++bp != endp)
{
    if (validSigs[*bp])
    {
    ...