编程中的常识是,由于缓存命中,内存局部性可以大大提高性能。我最近发现了boost::flat_map
哪个是基于矢量的地图实现。它似乎不像您典型的map
/ 那样流行,unordered_map
因此我无法找到任何性能比较。它如何比较?最佳的用例是什么?
谢谢!
编程中的常识是,由于缓存命中,内存局部性可以大大提高性能。我最近发现了boost::flat_map
哪个是基于矢量的地图实现。它似乎不像您典型的map
/ 那样流行,unordered_map
因此我无法找到任何性能比较。它如何比较?最佳的用例是什么?
谢谢!
Answers:
我最近在公司中针对不同的数据结构进行了基准测试,因此我需要一言以蔽之。正确地进行基准测试非常复杂。
在网上,我们很少(如果有的话)找到精心设计的基准。直到今天,我只发现了以新闻工作者的方式完成的基准测试(相当快,并且涵盖了很多变量)。
1)您需要考虑缓存预热
大多数运行基准测试的人都害怕计时器的差异,因此他们将自己的东西运行数千次并占用全部时间,只是小心地将每次操作重复相同的数千次,然后才认为具有可比性。
事实是,在现实世界中这没有什么意义,因为您的缓存不会变热,并且您的操作可能只会被调用一次。因此,您需要使用RDTSC进行基准测试,并花一些时间调用它们一次。英特尔发表了一篇论文,描述了如何使用RDTSC(使用cpuid指令刷新管道,并在程序开始时对其进行至少3次调用以使其稳定)。
2) RDTSC精度测量
我也建议这样做:
u64 g_correctionFactor; // number of clocks to offset after each measurement to remove the overhead of the measurer itself.
u64 g_accuracy;
static u64 const errormeasure = ~((u64)0);
#ifdef _MSC_VER
#pragma intrinsic(__rdtsc)
inline u64 GetRDTSC()
{
int a[4];
__cpuid(a, 0x80000000); // flush OOO instruction pipeline
return __rdtsc();
}
inline void WarmupRDTSC()
{
int a[4];
__cpuid(a, 0x80000000); // warmup cpuid.
__cpuid(a, 0x80000000);
__cpuid(a, 0x80000000);
// measure the measurer overhead with the measurer (crazy he..)
u64 minDiff = LLONG_MAX;
u64 maxDiff = 0; // this is going to help calculate our PRECISION ERROR MARGIN
for (int i = 0; i < 80; ++i)
{
u64 tick1 = GetRDTSC();
u64 tick2 = GetRDTSC();
minDiff = std::min(minDiff, tick2 - tick1); // make many takes, take the smallest that ever come.
maxDiff = std::max(maxDiff, tick2 - tick1);
}
g_correctionFactor = minDiff;
printf("Correction factor %llu clocks\n", g_correctionFactor);
g_accuracy = maxDiff - minDiff;
printf("Measurement Accuracy (in clocks) : %llu\n", g_accuracy);
}
#endif
这是一个差异度量工具,它将采用所有测量值中的最小值,以避免不时获得-10 ** 18(64位第一个负值)。
注意使用内在函数而不是内联汇编。如今,编译器很少支持首次内联汇编,但更糟糕的是,编译器围绕内联汇编创建了一个完整的排序障碍,因为它无法静态分析内部,因此这是对真实世界中的东西进行基准测试的问题,尤其是在仅调用东西时一旦。因此,内在函数适用于此,因为它不会破坏编译器对指令的自由重排序。
3)参数
最后一个问题是人们通常会测试场景的变化是否太少。容器性能受以下因素影响:
要点1很重要,因为容器确实会不时分配,并且如果它们使用CRT“ new”或某些用户定义的操作(例如池分配或自由列表或其他)进行分配,这将很重要。
(对于对pt 1感兴趣的人,请加入有关系统分配器性能影响的gamedev神秘线程)
第二点是因为某些容器(例如A)会浪费时间在周围复制东西,而类型越大,开销也越大。问题在于,当与另一个容器B进行比较时,对于小型容器,A可能胜过B;对于大型容器,A可能会输。
点3与点2相同,不同之处在于它将成本乘以某个加权因子。
第4点是一个大O问题,也有缓存问题。对于少数类型,某些复杂性较差的容器可以大大优于低复杂度的容器(例如map
vs. vector
,因为它们的缓存局部性很好,但会map
分散内存)。然后在某个交叉点,它们将丢失,因为所包含的整体大小开始“泄漏”到主内存并导致高速缓存未命中,再加上渐近复杂性可以开始显现的事实。
第5点是关于编译器能够在编译时清除空的或琐碎的内容。这可以极大地优化某些操作,因为容器是模板化的,因此每种类型都有自己的性能概况。
第6点与第5点相同,POD可以受益于以下事实:复制构造只是一个简单的过程,对于某些情况,某些容器可以使用部分模板专门化或SFINAE根据T的特征选择算法,从而具有特定的实现。
显然,平面图是一个排序的矢量包装器,例如Loki AssocVector,但是C ++ 11附带了一些补充的现代化功能,它们利用移动语义来加速单个元素的插入和删除。
这仍然是有序的容器。大多数人通常不需要订购部分,因此存在unordered..
。
您是否考虑过可能需要flat_unorderedmap
?一个google::sparse_map
或多个类似的东西-一个开放地址哈希图。
开放地址哈希映射的问题在于,rehash
它们必须将周围的所有内容复制到新的扩展平面上,而标准的无序映射只需要重新创建哈希索引,而分配的数据则保留在原处。当然,缺点是内存像地狱一样分散。
在开放地址哈希图中重新哈希的标准是当容量超过存储桶矢量的大小乘以负载因子时。
典型的负载系数为0.8
; 因此,您需要注意的是,如果可以在填充哈希映射之前对其进行预调整大小,请始终将其预调整为:intended_filling * (1/0.8) + epsilon
这将为您提供一个保证,即在填充过程中,您无需虚假地重新哈希和重新复制所有内容。
封闭地址映射(std::unordered..
)的优点是您不必关心那些参数。
但是boost::flat_map
是有序向量;因此,它将始终具有log(N)渐近复杂度,它不如开放地址哈希图(摊销的恒定时间)好。您也应该考虑这一点。
此测试涉及不同的映射(int
键和__int64
/ somestruct
为值)和std::vector
。
测试类型信息:
typeid=__int64 . sizeof=8 . ispod=yes
typeid=struct MediumTypePod . sizeof=184 . ispod=yes
插入
编辑:
我以前的结果包含一个错误:他们实际测试了有序插入,这对于平面图表现出非常快的行为。
我将这些结果留在本页的后面,因为它们很有趣。
这是正确的测试:
我已经检查了实现,这里的平面图中没有实现延迟排序之类的东西。每个插入都是即时进行排序的,因此此基准展现了渐近趋势:
map:O(N * log(N))
hashmaps:O(N)
vector and flatmaps:O(N * N)
警告:此后,的2个测试std::map
和flat_map
s都存在错误,并且实际上测试了有序插入(相对于其他容器的随机插入。是的,这很令人困惑,抱歉):
我们可以看到有序插入会导致后推,而且速度非常快。但是,从我的基准测试的非图表结果中,我也可以说,这对于反向插入而言并不是绝对的最佳。在10k个元素上,可以在预先保留的向量上获得完美的反向插入最优性。这给了我们300万个周期;我们在此处观察到4.8M的有序插入flat_map
(因此为最佳的160%)。
分析:请记住,这是向量的“随机插入”,因此,每次插入都要向上(一个元素一个元素)移动一半(平均)数据,因此产生了10亿个庞大的周期。
随机搜索3个元素(时钟重新归一化为1)
大小= 100
大小= 10000
迭代
尺寸超过100(仅限MediumPod类型)
超过10000(仅限MediumPod类型)
最终盐粒
最后,我想回到“基准测试§3Pt1”(系统分配器)上。在最近的一项实验中,我围绕着我开发的开放地址哈希图的性能进行了研究,在某些std::unordered_map
用例上(在此讨论),我测量出Windows 7和Windows 8之间的性能差距超过3000%。
这使我想警告读者有关以上结果(它们是在Win7上完成的):您的行驶里程可能会有所不同。
最好的祝福
flat_map
比std::map
-是任何人都无法解释这一结果呢?
flat_map
as作为容器的固有特性。因为Aska::
版本比std::map
查找要快。证明还有优化的空间。预期的性能在渐近上是相同的,但是由于缓存的局部性,可能会稍微好一些。使用高尺寸集时,它们应会聚。
从文档看来,这类似于Loki::AssocVector
我是一个相当沉重的用户。由于它基于向量,因此具有向量的特征,即:
size
增长超过,迭代器就会失效capacity
。capacity
,需要重新分配和移动对象,即,除非end
在capacity > size
std::map
由于缓存局部性而更快,这是一种二进制搜索,其性能与std::map
其他地方相同最好的用法是当您事先知道元素的数量(以便可以reserve
提前)时,或者插入/删除很少但查找频繁时。迭代器失效使它在某些用例中有点麻烦,因此就程序正确性而言它们是不可互换的。