是否每个元素都在std :: vector中访问缓存?

时间:2016-07-21 09:50:46

标签: c++ arrays performance caching vector

已知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;
    }
}

在循环中,我访问一个数组和一个堆栈变量,它们在内存中彼此远离。这是否意味着我每次迭代都会访问不同的内存并错过缓存?

2 个答案:

答案 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的内存位置。因此,它也不会成为缓存未命中的问题。