为什么向量和地图搜索比静态比较要慢得多
Why are vector and map searches so much slower than static comparisons
我正在大小约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查找,并且它们真的很容易对付。
- 比较并显示使用最小值(a,b)和最大值(a、b)升序排列的4个数字
- 将值与 C++ 中的静态和全局表进行比较
- 将非静态数据成员与常量成员进行比较
- 使用自定义比较功能对静态多维数组进行排序
- std::具有自定义比较函数结果的排序函数错误:必须调用对非静态成员函数的引用
- 为什么向量和地图搜索比静态比较要慢得多
- C++:将静态类成员与静态类成员的传递版本进行比较
- Clang和GCC与MSVC和ICC的比较:如果复制/移动省略也适用,那么在复制/移动构造函数中是否需要静态断言
- 如何为地图提供需要静态数据的比较器
- 功能比较器可以是静态功能
- 比较非静态函数中的静态整数和非静态整数
- 产量和静态局部变量的比较
- 为什么比较 constexpr 函数的两个参数不是静态断言的常量条件
- 可以strcmp()比较动态字符数组和静态字符数组
- 比较两个静态数组
- 如何比较两个充满类的静态列表
- 在编译时比较静态字段指针
- 类型比较的Boost静态断言
- 正在初始化非静态成员:C++和Java比较
- 在比较 Windows、控制台、静态库和 DLL C++时,后两者的用途是什么?