已知std::vector
将其数据保存在堆上,因此向量本身的实例和第一个元素具有不同的地址。另一方面,std::array
是一个围绕原始数组的轻量级包装器,它的地址等于第一个元素的地址。
假设集合的大小足以容纳int32
的一个缓存行。在我的具有384kB L1缓存的机器上,它有98304个数字。
如果我迭代std::vector
,事实证明我总是首先访问矢量本身的地址和下一个访问元素的地址。可能这些地址不在同一个缓存行中。因此,每个元素访问都是缓存未命中。
但如果我迭代std::array
地址在同一个缓存行中,那么它应该更快。
我使用VS2013进行了全面优化测试,std::array
快了大约20%。
我的假设是对的吗?
更新:,以便不创建第二个类似主题。在这段代码中,我有一个数组和一些局部变量:
void test(array<int, 10>& arr)
{
int m{ 42 };
for (int i{ 0 }; i < arr.size(); ++i)
{
arr[i] = i * m;
}
}
在循环中,我访问一个数组和一个堆栈变量,它们在内存中彼此远离。这是否意味着我每次迭代都会访问不同的内存并错过缓存?
答案 0 :(得分:12)
你说的许多事情都是正确的,但我不相信你会以你认为的速度看到缓存未命中。相反,我认为您正在看到编译器优化的其他影响。
你是正确的,当你在std::vector
中查找一个元素时,有两个内存读取:首先,读取指向元素的指针的内存;第二,读取元素本身的内存。但是,如果您在std::vector
上执行多个顺序读取,则可能是您执行的第一次读取将在元素上出现缓存未命中,但所有连续读取将在缓存中或不可避免。内存缓存针对引用的局部性进行了优化,因此每当将单个地址拉入缓存时,大量相邻的内存地址也会被拉入缓存。因此,如果迭代std::vector
的元素,大多数情况下根本不会有任何缓存未命中。性能应该与常规数组非常相似。还值得记住的是,缓存存储了多个不同的内存位置,而不仅仅是一个,因此您正在读取堆栈上的内容(std::vector
内部指针)和堆中的某些内容(元素),或堆栈上的两个不同元素,不会立即导致缓存未命中。
需要记住的是,与缓存命中相比,缓存未命中非常 - 通常要慢10倍 - 所以如果确实在std::vector
的每个元素上看到缓存未命中你不会看到性能差距只有20%。你会看到更接近2倍或更大性能差距的东西。
那么,为什么你会看到性能上的差异?你尚未考虑的一个重要因素是,如果你使用std::array<int, 10>
,那么编译器可以在编译时告诉数组中有十个元素,并且可以展开或以其他方式优化循环你必须消除不必要的检查。事实上,编译器原则上可以用10个连续的代码块替换循环,这些代码块都写入特定的数组元素,这可能比在循环中反向分支反向快得多。另一方面,使用std::vector
的等效代码,编译器不能总是事先知道循环运行的次数,因此很可能无法生成与其生成的代码一样好的代码。对于阵列。
然后有一个事实,你在这里写的代码是如此之小,以至于任何计时的尝试都会产生大量的噪音。很难评估它的可靠速度,因为与方法的“冷”运行相比,将其放入for循环这样简单的操作会使缓存行为陷入混乱。
总的来说,我不会将此归因于缓存未命中,因为我怀疑它们的数量明显不同。相反,我认为这是阵列上的编译器优化,其大小是静态已知的,而std::vector
上的优化只能动态地知道。
答案 1 :(得分:3)
我认为它与缓存未命中无关。
您可以将std::array
作为原始数组的包装,即int arr[10]
,而vector
作为动态数组的包装,即new int[10]
。它们应具有相同的性能。但是,当您访问vector
时,您可以通过指针操作动态数组。通常,编译器可能比使用指针的代码更好地优化代码。这可能是您获得测试结果的原因:std::array
更快。
您可以进行测试,将std::array
替换为int arr[10]
。虽然std::array
只是int arr[10]
的包装,但您可能会获得更好的性能(在某些情况下,编译器可以使用原始数组进行更好的优化)。您还可以进行另一项将vector
替换为new int[10]
的测试,它们应具有相同的性能。
对于第二个问题,局部变量,即m
,将保存在寄存器中(如果正确优化),并且在for循环期间将无法访问m
的内存位置。因此,它也不会成为缓存未命中的问题。