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

Why are vector and map searches so much slower than static comparisons

本文关键字:比较 静态 向量 地图 图搜索 为什么      更新时间:2023-10-16

我正在大小约1MB,读取第一个300KB并搜索许多特定签名。我的策略是,对于每个字节,查看字节是否在地图/向量/我知道可能是签名开始时的字节中字节是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个有效的符号来查看第二个字符是否有效,还将总运行时间降低到0.4秒。

我觉得其他实现应该会表现更好。尤其是地图,该地图应扩展为添加更多签名前缀,并且搜索为O(log(n))与O(n)。我想念什么?我唯一的黑暗猜测是,通过静态比较,并且(在现存较少)矢量索引时,我得到了用于比较寄存器或其他位置的比较值,这使其比阅读速度要快得多从记忆里。如果这是真的,我是否可以明确地告诉编译器经常使用特定值?我还可以利用以下代码的其他优化?

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

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

非常简单,与向量中的每个值进行比较,需要3个比较。该地图为O(log n),但是由于导航链接的数据结构,系数(被BIG-O符号忽略)很大。查找表为O(1),带有一个小系数,因为可以通过单个机器指令完成对结构的访问,然后所有保留的内容都是与零的比较。

分析性能的最佳方法是使用诸如Valgrind/kcachegrind的探测器工具。

"与常数比较"比较3个内存地址与3个常数。如果编译器感觉像它,则这种情况将非常容易完成诸如揭开或进行一些优化之类的事情。书面ASM将在这里拥有的唯一分支将是高度可预测的。

对于文字3个元素向量查找,还需要取消矢量值地址的额外成本。

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

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

至于map解决方案,map s的开销很高,因此慢速是可以预见的。map的主要用途是处理大型N查找,并且它们真的很容易对付。