为啥 std::vector 的速度是原始数组的两倍?包含完整代码

Posted

技术标签:

【中文标题】为啥 std::vector 的速度是原始数组的两倍?包含完整代码【英文标题】:Why is std::vector twice as fast as raw array? Complete code included为什么 std::vector 的速度是原始数组的两倍?包含完整代码 【发布时间】:2014-04-06 22:36:13 【问题描述】:

结果:

向量时间:7051

阵列时间:18944

我为此使用了 MSVC 发布模式,编译为 32 位。

在此测试之前,我查看了向量的 GCC 源代码并感到惊讶,因为我认为 operator[] 检查了数组越界,但事实并非如此。不过,没想到向量这么快?!

完整代码:

#include <iostream>
#include <vector>

int main()
    const int size = 10000;
    unsigned long long my_array[size];
    std::vector<unsigned long long> my_vec;
    
    my_vec.resize(size);

    //Populate containers
    for(int i=0; i<size; i++)
        my_vec[i] = i;
        my_array[i] = i;
    

    //Initialise test variables
    unsigned long long sum = 0;
    unsigned long long time = 0;
    unsigned long long start = 0;
    unsigned long long finish = 0;

    //Time the vector
    start = __rdtsc();
    for(int i=0; i<size; i++)
        sum += my_vec[i];
    
    finish = __rdtsc();


    time = finish - start;
    std::cout << "Vector time: " << time << "     " << sum << std::endl;


    sum = 0;

    //Time the array
    start = __rdtsc();
    for(int i=0; i<size; i++)
        sum += my_array[i];
    
    finish = __rdtsc();

    time = finish - start;
    std::cout << "Array time: " << time << "     " << sum << std::endl;

    int t = 8;
    std::cin >> t;
    return 0;

【问题讨论】:

当我测试它时,数组总是更快。向量时间:83755,数组时间:69753。当然,执行之间的值会有所不同,但不会太大。 尝试恢复这些测试。惊喜! @user997112:在向量的情况下,您在填充后立即读取它。在数组的情况下,在数组填充和数组读取之间有一个完整的向量读取循环。 也许比另一种更快的测试更好的方法是创建两个单独的程序,其中一个严格来说是standard C array,另一个严格来说是std::vector。从算法上讲,这两个程序都必须运行完全相同的测试。 除非您将进程固定到特定内核,否则您不能信任 TSC 测量结果,即使这样您也需要确保您使用的内在函数也省略了阻止指令的指令(例如 CPUID)在执行管道中重新排序(因此您尝试测量的代码相对于 TSC 读取进行序列化)。这个基准是不可信的。 【参考方案1】:

以下使用的是 MSVC 2013。

对于向量:

0019138E  mov         edi,edi  
  for (int i = 0; i<size; i++)
00191390  lea         ecx,[ecx+20h]  
    sum += my_vec[i];
00191393  movdqu      xmm0,xmmword ptr [ecx-20h]  
00191398  paddq       xmm1,xmm0  
0019139C  movdqu      xmm0,xmmword ptr [ecx-10h]  
001913A1  paddq       xmm2,xmm0  
001913A5  dec         esi  
001913A6  jne         main+0F0h (0191390h)  
  

对于数组:

0019142D  lea         ecx,[ecx]  
  for (int i = 0; i<size; i++)
00191430  lea         ecx,[ecx+20h]  
    sum += my_array[i];
00191433  movdqu      xmm0,xmmword ptr [ecx-30h]  
00191438  paddq       xmm1,xmm0  
0019143C  movdqu      xmm0,xmmword ptr [ecx-20h]  
00191441  paddq       xmm2,xmm0  
00191445  dec         esi  
00191446  jne         main+190h (0191430h)  
  

如您所见,内部循环是相同的。实际上,我怀疑这是一个硬件问题,我交换了两个循环,并且数组的输出速度更快到相同的幅度(所以实际上,在现实世界中,两者都没有比另一个更快或更慢)。

我预测这是某种 CPU 缓存行为: https://en.wikipedia.org/wiki/CPU_cache

【讨论】:

缓存将充满第一个数据结构的内容 - 所以当第二个执行时它会遇到缓存未命中的负载 第一个数据结构也会经历缺失(来自其他用户空间程序或其他),所以这不是直接的结果。确认后,我会立即跟进一些更多信息。 我认为这可能与my_vec[i] = i; 之类的行以及它们对缓存的排序影响有关,但我无法确认。我也无法确认这是 CPU 节点/缓存模式(或类似)问题,因为两个页面都是读/写的(我在程序集中确认了)。 我刚刚尝试了向量、数组、向量,我得到了:9028、13222、11502(当我重复时,我得到了类似的结果,第一个总是比第二个快得多 2)【参考方案2】:

我们有两个数组,每个数组 80,000 字节。首先,160,000 字节并行填充数据。然后读取其中的 80,000 个,然后读取其他 80,000 个。假设缓存为 128,000 字节:

在读取向量的前 32,000 个字节时,数据不会被缓存。接下来的 48,000 个字节被缓存。现在缓存包含所有向量和数组的最后 48,000 字节。但是数组中的字节是最旧的,所以当数组从头开始读取时,它末尾的数据会被丢弃。所以从数组中读取的所有内容都是未缓存的。

因此,对于向量,我们有 32,000 字节的未缓存读取和 48,000 字节的缓存读取,而数组有 80,000 字节的未缓存读取。

这是针对 128,000 字节的缓存大小。其他尺寸会有所不同。 但随后可能会发生完全不同的事情。您的代码可以在运行时切换到不同的处理器,此时一个处理器可能必须将数据写入主存储器,而另一个处理器读取它。在另一个方向上,操作系统可能刚刚意识到正在进行一些操作,并将处理器从省电模式切换到某种加速模式。

进行一次测量并从中得出结论,这些都没有考虑在内。

【讨论】:

但是向量在堆上,所以数组和向量不共享同一个页面。因此;它们之间没有缓存共享。像这样修改代码:unsigned long long *my_array = new unsigned long long[size]; NOT 会改变性能。 @kvanberendonck:gnasher729 没有声称有任何缓存共享,除了所有进程当然共享相同的完整处理器缓存这一事实。 我现在意识到我误解了,但我的解释是他现在原因缓存在进程开始时是“无丢失”/新鲜的。是这样吗?缓存不会从计算机上运行的所有其他内容中填充,因此在第一个和第二个循环中都会有相同的未命中率? 好吧,他的(隐含的)假设是在整个代码块的运行期间没有中断(因此中间的缓存不会受到任何干扰),并且他也忽略了中间的输出调用。但他不需要在一开始就假设一个干净的缓存,因为填充数组和向量的循环(在计时开始之前运行)会将任何已经被到达代码块时在那里。 令人费解的是,删除顶部填充数组的循环(将它们带入缓存)对计时没有影响。还尝试在两个循环之间添加std::this_thread::yield(); 以排除操作系统因素的成本,但这似乎也没有任何区别。

以上是关于为啥 std::vector 的速度是原始数组的两倍?包含完整代码的主要内容,如果未能解决你的问题,请参考以下文章

std::vector 与 C++ 中的原始数组有多相似?

为啥我们不能通过值传递数组来函数?

将 std::vector<int> 从原始内存转换为数组[重复]

为啥使用 std::vector 而不是 realloc? [关闭]

为啥 std::vector 没有 append 方法? [关闭]

C++ std::map 和 std::vector 的优点? [关闭]