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

时间:2012-11-08 00:01:43

标签: c++ caching optimization

我正在解析大小约1MB的文件,读取前300KB并搜索许多特定签名。我的策略是,对于每个字节,查看字节是否在map / vector /我知道可能在签名开头的任何字节中,如果是,则查找完整签名 - 对于此示例,假设那些领先字节是x37,x50和x52。处理总共90个文件(实际上9个文件10次),以下代码在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; 
                    }
                }
            }
        }

但是,我的初始实现在一个缓慢的84秒内结束。唯一的区别与上面标有“//比较行”的行有关:

findItr = mapSigs.find(*bp);
if (findItr != mapSigs.end())
...

使用包含3个值的向量的非常类似的实现也会导致处理速度极慢(190秒):

if (find(vecFirstChars.begin(), vecFirstChars.end(), *bp) != vecFirstChars.end())
{
    findItr = mapSigs.find(*bp);
    ...

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

if (vecFirstChars[0] == *bp || vecFirstChars[1] == *bp || vecFirstChars[2] == *bp)
{
    findItr = mapSigs.find(*bp);
    ...

到目前为止最快的实现(受以下组件10的启发)如下所示,大约在2.0秒内完成:

bool validSigs[256] = {0};
validSigs[0x37] = true;
validSigs[0x50] = true;
validSigs[0x52] = true;

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

将其扩展为使用2个validSigs来查看第二个字符是否有效,将总运行时间缩短为0.4秒。

我觉得其他实现应该表现得更好。特别是地图,它应该按照更多的签名前缀进行扩展,并且搜索是O(log(n))vs O(n)。我错过了什么?我唯一的黑暗猜测是静态比较和(对较小的现存)矢量索引,我得到用于比较的值缓存在寄存器或其他位置,这使得它明显快于读取从记忆里。如果这是真的,我是否能够明确地告诉编译器将经常使用特定值?是否有任何其他优化我可以利用以下代码不明显?

我正在使用Visual Studio 2008进行编译。

2 个答案:

答案 0 :(得分:1)

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

非常简单,与矢量中的每个值进行比较依次需要3次比较。该映射是O(log N),但由于导航链接的数据结构,系数(由big-O表示法忽略)很大。查找表是O(1),系数很小,因为可以通过单个机器指令完成对结构的访问,然后剩下的就是与零进行一次比较。

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

答案 1 :(得分:0)

“与常数比较”将3个内存地址与3个常量进行比较。如果编译器感觉如此,这种情况将非常容易地执行诸如展开或执行位优化之类的操作。书面ASM将在这里拥有的唯一分支将是高度可预测的。

对于文字3元素向量查找,需要解除引用向量值地址的额外成本。

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

要验证该理论,请尝试使分支更可预测 - 一次搜索最多100个不同输入字节的每个元素,然后搜索下一个。这将使天真的分支预测工作在98%的时间,而不是代码中的33%。即,为签名0扫描100(或其他)字符,然后扫描签名1的100(或其他)字符,直到签名用完为止。然后继续下一个100个字符的块来扫描签名。我之所以选择100,是因为我试图避免分支预测失败,而且我认为百分之几的分支预测失败并不是那么糟糕。 :)

对于map解决方案,井map具有很高的常量开销,因此它很慢是可预测的。 map的主要用途是处理大量的n次查找,以及它们非常容易编码的事实。