C++缓存性能奇怪的行为

5

我阅读了一篇文章(1.5年前的:http://www.drdobbs.com/parallel/cache-friendly-code-solving-manycores-ne/240012736),该文章讨论了缓存性能和数据大小。他们展示了下面的代码,声称在i7(Sandy Bridge)上运行:

static volatile int array[Size];
static void test_function(void)
{
    for (int i = 0; i < Iterations; i++)
        for (int x = 0; x < Size; x++)
          array[x]++;
}

他们声称,如果他们保持Size*Iterations不变,并且增加Size,当数组在内存中的大小超过L2缓存大小时,观察到执行时间急剧增加(10倍)。
作为自己的一项练习,我想尝试这个以查看是否能够重现他们的结果。我的机器是i7 3770k、win7、visual c++ 2012编译器、Win32 debug模式,没有启用任何优化。但是令人惊讶的是,即使超出了L3缓存大小,我也没有看到执行时间的增加,这让我认为编译器在某种程度上对这段代码进行了优化。但是我也没有看到任何优化。我看到的唯一速度变化是在我的机器字大小以下,需要稍微更长的时间。下面是我的定时、代码列表和相关汇编。
有人知道为什么吗:
1) 为什么无论数组的大小如何,所需的时间都不会增加?或者我该如何找出原因?
2) 为什么所需的时间开始很高,然后降低直到达到缓存线大小,如果数据小于行大小,更多的迭代应该在不读取缓存的情况下被处理吗?
Size=1,Iterations=1073741824, Time=3829
Size=2,Iterations=536870912, Time=2625
Size=4,Iterations=268435456, Time=2563
Size=16,Iterations=67108864, Time=2906
Size=32,Iterations=33554432, Time=3469
Size=64,Iterations=16777216, Time=3250
Size=256,Iterations=4194304, Time=3140
Size=1024,Iterations=1048576, Time=3110
Size=2048,Iterations=524288, Time=3187
Size=4096,Iterations=262144, Time=3078
Size=8192,Iterations=131072, Time=3125
Size=16384,Iterations=65536, Time=3109
Size=32768,Iterations=32768, Time=3078
Size=65536,Iterations=16384, Time=3078
Size=262144,Iterations=4096, Time=3172
Size=524288,Iterations=2048, Time=3109
Size=1048576,Iterations=1024, Time=3094
Size=2097152,Iterations=512, Time=3313
Size=4194304,Iterations=256, Time=3391
Size=8388608,Iterations=128, Time=3312
Size=33554432,Iterations=32, Time=3109
Size=134217728,Iterations=8, Time=3515
Size=536870912,Iterations=2, Time=3532

code:

#include <string>
#include <cassert>
#include <windows.h>

template <unsigned int SIZE, unsigned int ITERATIONS>
static void test_body(volatile char* array)
{
     for (unsigned int i = 0; i < ITERATIONS; i++)
    {
        for (unsigned int  x = 0; x < SIZE; x++)
        {
            array[x]++;
        }
    }
}

template <unsigned int SIZE, unsigned int ITERATIONS>
static void test_function()
{
    assert(SIZE*ITERATIONS == 1024*1024*1024);
    static volatile char array[SIZE];

    test_body<SIZE, 1>(array); //warmup

    DWORD beginTime = GetTickCount();
    test_body<SIZE, ITERATIONS>(array); 
    DWORD endTime= GetTickCount();
    printf("Size=%u,Iterations=%u, Time=%d\n", SIZE,ITERATIONS, endTime-beginTime);
}

int main()
{
    enum { eIterations= 1024*1024*1024};
    test_function<1, eIterations>();
    test_function<2, eIterations/2>();
    test_function<4, eIterations/4>();
    test_function<16, eIterations/16>();
    test_function<32, eIterations/ 32>();
    test_function<64, eIterations/ 64>();
    test_function<256, eIterations/ 256>();
    test_function<1024, eIterations/ 1024>();
    test_function<2048, eIterations/ 2048>();
    test_function<4096, eIterations/ 4096>();
    test_function<8192, eIterations/ 8192>();
    test_function<16384, eIterations/ 16384>();
    test_function<32768, eIterations/ 32768>();
    test_function<65536, eIterations/ 65536>();
    test_function<262144, eIterations/ 262144>();
    test_function<524288, eIterations/ 524288>();
    test_function<1048576, eIterations/ 1048576>();
    test_function<2097152, eIterations/ 2097152>();
    test_function<4194304, eIterations/ 4194304>();
    test_function<8388608, eIterations/ 8388608>();
    test_function<33554432, eIterations/ 33554432>();
    test_function<134217728, eIterations/ 134217728>();
    test_function<536870912, eIterations/ 536870912>();
}

反汇编

    for (unsigned int i = 0; i < ITERATIONS; i++)
00281A59  mov         dword ptr [ebp-4],0  
00281A60  jmp         test_body<536870912,2>+1Bh (0281A6Bh)  
00281A62  mov         eax,dword ptr [ebp-4]  
00281A65  add         eax,1  
00281A68  mov         dword ptr [ebp-4],eax  
00281A6B  cmp         dword ptr [ebp-4],2  
00281A6F  jae         test_body<536870912,2>+53h (0281AA3h)  
    {
        for (unsigned int  x = 0; x < SIZE; x++)
00281A71  mov         dword ptr [ebp-8],0  
00281A78  jmp         test_body<536870912,2>+33h (0281A83h)  
00281A7A  mov         eax,dword ptr [ebp-8]  
    {
        for (unsigned int  x = 0; x < SIZE; x++)
00281A7D  add         eax,1  
00281A80  mov         dword ptr [ebp-8],eax  
00281A83  cmp         dword ptr [ebp-8],20000000h  
00281A8A  jae         test_body<536870912,2>+51h (0281AA1h)  
        {
            array[x]++;
00281A8C  mov         eax,dword ptr [array]  
00281A8F  add         eax,dword ptr [ebp-8]  
00281A92  mov         cl,byte ptr [eax]  
00281A94  add         cl,1  
00281A97  mov         edx,dword ptr [array]  
00281A9A  add         edx,dword ptr [ebp-8]  
00281A9D  mov         byte ptr [edx],cl  
        }
00281A9F  jmp         test_body<536870912,2>+2Ah (0281A7Ah)  
    }
00281AA1  jmp         test_body<536870912,2>+12h (0281A62h)  

Skimon,请在开始计时每个大小之前更新您的代码以执行完整的单次迭代(像Joky的答案中的预热一样)。 - osgx
如果你想要测量缓存大小和延迟,可以使用经典的缓存测试工具skimon。在链表上遍历时,改变元素之间的内存距离(stride),这就是lmbench中的lat_mem_rd测试,CPU-Z也使用了该测试。 - osgx
@osgx 好的,更新代码以进行预热和重新发布时间和反汇编。 - skimon
@osgx在发布原始问题之前,我运行了CPU-Z来检查缓存大小。 我并不真正关心时序,而是想了解我提到的两个点。谢谢 - skimon
skimon,谢谢。在英特尔芯片的L1和L2附近有硬件预取引擎,每个引擎都可以检测线性访问模式并提前加载数据。但总时间(如果是ms=毫秒)太长了(即使对于较小的缓存,访问速度也接近内存速度)。我将尝试探索hw perfcounters。您能否发布几个Windows二进制文件,每个文件仅适用于一个大小(32 1024 32768 1048576 33554432)? - osgx
显示剩余2条评论
4个回答

8
TL;DR:你的测试不是用于测量缓存延迟或速度的正确测试。相反,它测量了通过OoO CPU管道剖分复杂代码的一些问题。
使用正确的测试来测量缓存和内存延迟:lmbench中的lat_mem_rd;以及用于速度(带宽)测量的正确测试:内存速度的STREAM基准测试memtest86中的测试,用于带有rep movsl主要操作的缓存速度。
此外,在现代(2010年及以后的)台式机/服务器CPU中,靠近L1和L2缓存有硬件预取逻辑,可以检测线性访问模式,并在您请求数据之前从外部缓存预加载数据到内部:Intel Optimization Manual - 7.2 Hardware prefetching of data,第365页;intel.com blog, 2009。很难禁用所有硬件预取(SO Q/A 1SO Q/A 2)。
长话短说:
我将尝试使用Linux中的perf性能监测工具(又称perf_events)对类似测试进行多次测量。该代码基于Joky的程序(32位整数数组,而不是字符数组),并被分成了几个二进制文件: a5 用于大小为2 ^ 5 = 32; a10 => 2 ^ 10 = 1024(4 KB); a15 => 2 ^ 15 = 32768; a20(100万个整数= 4 MB)和a25(3200万个整数= 128MB)。CPU是i7-2600四核Sandy Bridge 3.4 GHz。

让我们从默认事件集的基本perf stat开始(跳过了一些行)。我选择了2 ^ 10(4 KB)和2 ^ 20(4 MB)。

$ perf stat ./a10
Size=1024 ITERATIONS=1048576, TIME=2372.09 ms

 Performance counter stats for './a10':

               276 page-faults               #    0,000 M/sec
     8 238 473 169 cycles                    #    3,499 GHz
     4 936 244 310 stalled-cycles-frontend   #   59,92% frontend cycles idle
       415 849 629 stalled-cycles-backend    #    5,05% backend  cycles idle
    11 832 421 238 instructions              #    1,44  insns per cycle
                                             #    0,42  stalled cycles per insn
     1 078 974 782 branches                  #  458,274 M/sec
         1 080 091 branch-misses             #    0,10% of all branches

$ perf stat ./a20
Size=1048576 ITERATIONS=1024, TIME=2432.4 ms

 Performance counter stats for './a20':

             2 321 page-faults               #    0,001 M/sec
     8 487 656 735 cycles                    #    3,499 GHz
     5 184 295 720 stalled-cycles-frontend   #   61,08% frontend cycles idle
       663 245 253 stalled-cycles-backend    #    7,81% backend  cycles idle
    11 836 712 988 instructions              #    1,39  insns per cycle
                                             #    0,44  stalled cycles per insn
     1 077 257 745 branches                  #  444,104 M/sec
            30 601 branch-misses             #    0,00% of all branches

我们可以看到什么?指令计数非常接近(因为Size*Iterations是恒定的),周期计数和时间也很接近。两个示例都有10亿个分支,预测准确率达到99%。但前端有非常高的60%停顿计数,后端为5-8%。前端停顿是指指令获取和解码中的停顿,可能很难确定原因,但对于你的代码,前端无法每个时钟周期解码4条指令(见Intel optimisation manual B-41页,B.3节 - “用于... Sandy Bridge”的性能调整技术,子段B.3.2 分层自上而下的性能表现特征化...)。
$ perf record -e stalled-cycles-frontend ./a20
Size=1048576 ITERATIONS=1024, TIME=2477.65 ms
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.097 MB perf.data (~4245 samples) ]
$ perf annotate -d a20|cat
 Percent |      Source code & Disassembly of a20
------------------------------------------------
         :      08048e6f <void test_body<1048576u, 1024u>(int volatile*)>:

   10.43 :       8048e87:       mov    -0x8(%ebp),%eax
    1.10 :       8048e8a:       lea    0x0(,%eax,4),%edx
    0.16 :       8048e91:       mov    0x8(%ebp),%eax
    0.78 :       8048e94:       add    %edx,%eax
    6.87 :       8048e96:       mov    (%eax),%edx
   52.53 :       8048e98:       add    $0x1,%edx
    9.89 :       8048e9b:       mov    %edx,(%eax)
   14.15 :       8048e9d:       addl   $0x1,-0x8(%ebp)
    2.66 :       8048ea1:       mov    -0x8(%ebp),%eax
    1.39 :       8048ea4:       cmp    $0xfffff,%eax

或者使用原始操作码(objdump -d),有些操作码的索引比较复杂,可能无法被三个简单的解码器处理,需要等待唯一的复杂解码器(图片在这里:http://www.realworldtech.com/sandy-bridge/4/)。

 8048e87:       8b 45 f8                mov    -0x8(%ebp),%eax
 8048e8a:       8d 14 85 00 00 00 00    lea    0x0(,%eax,4),%edx
 8048e91:       8b 45 08                mov    0x8(%ebp),%eax
 8048e94:       01 d0                   add    %edx,%eax
 8048e96:       8b 10                   mov    (%eax),%edx
 8048e98:       83 c2 01                add    $0x1,%edx
 8048e9b:       89 10                   mov    %edx,(%eax)
 8048e9d:       83 45 f8 01             addl   $0x1,-0x8(%ebp)
 8048ea1:       8b 45 f8                mov    -0x8(%ebp),%eax
 8048ea4:       3d ff ff 0f 00          cmp    $0xfffff,%eax

后端停顿是由于等待内存或缓存(在测量缓存时感兴趣的东西)以及内部执行核心停顿而创建的停顿:

$ perf record -e stalled-cycles-backend ./a20
Size=1048576 ITERATIONS=1024, TIME=2480.09 ms
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.095 MB perf.data (~4149 samples) ]
$ perf annotate -d a20|cat
    4.25 :       8048e96:       mov    (%eax),%edx
   58.68 :       8048e98:       add    $0x1,%edx
    8.86 :       8048e9b:       mov    %edx,(%eax)
    3.94 :       8048e9d:       addl   $0x1,-0x8(%ebp)
    7.66 :       8048ea1:       mov    -0x8(%ebp),%eax
    7.40 :       8048ea4:       cmp    $0xfffff,%eax

大多数后端停顿都是由于add 0x1,%edx报告的,因为它是前一个命令中从数组加载的数据的消费者。通过对数组进行存储,它们占据了后端停顿的70%,或者如果我们将其乘以程序中总后端停顿比例(7%),则占所有停顿的5%。换句话说,缓存比你的程序更快。现在我们可以回答你的第一个问题:

无论我的数组大小如何,所需时间为什么完全不增加?

你的测试很糟糕(未经优化),你试图测量缓存,但它们只会使总运行时间减慢5%。你的测试非常不稳定(嘈杂),你看不到这个5%的影响。
使用自定义的perf stat运行,我们也可以测量缓存请求到未命中的比率。对于4 KB程序,L1数据缓存提供了99.99%的所有加载和99.999%的所有存储服务。我们可以注意到,您不正确的测试生成了比步进数组并递增每个元素所需的请求多几倍的缓存请求(10亿次加载+ 10亿次存储)。额外的访问是用于处理本地变量,如x,它们始终由缓存提供服务,因为它们的地址是固定的。
$ perf stat -e 'L1-dcache-loads,L1-dcache-load-misses,L1-dcache-stores,L1-dcache-store-misses' ./a10
Size=1024 ITERATIONS=1048576, TIME=2412.25 ms

 Performance counter stats for './a10':

     5 375 195 765 L1-dcache-loads
           364 140 L1-dcache-load-misses     #    0,01% of all L1-dcache hits
     2 151 408 053 L1-dcache-stores
            13 350 L1-dcache-store-misses

对于4 MB程序,命中率要差很多。错过的次数增加了100倍!现在有1.2%的所有内存请求不是由L1而是由L2提供服务。
$ perf stat -e 'L1-dcache-loads,L1-dcache-load-misses,L1-dcache-stores,L1-dcache-store-misses' ./a20
Size=1048576 ITERATIONS=1024, TIME=2443.92 ms

 Performance counter stats for './a20':

     5 378 035 007 L1-dcache-loads
        67 725 008 L1-dcache-load-misses     #    1,26% of all L1-dcache hits
     2 152 183 588 L1-dcache-stores
        67 266 426 L1-dcache-store-misses

我們是否經常想要注意到快取延遲時間如何增加,從4 CPU時鐘增加到12(時間增加了3倍),而這種變化只影響了1.2%的快取請求,而我們的程序對快取延遲時間敏感度僅下降了7%?

如果我們使用更大的數據集會怎樣呢?好的,這裡是a25(4字節整數的2 ^ 25 = 128 MB,比快取大小大幾倍):

$ perf stat   ./a25
Size=134217728 ITERATIONS=8, TIME=2437.25 ms

 Performance counter stats for './a25':

           262 417 page-faults               #    0,090 M/sec
    10 214 588 827 cycles                    #    3,499 GHz
     6 272 114 853 stalled-cycles-frontend   #   61,40% frontend cycles idle
     1 098 632 880 stalled-cycles-backend    #   10,76% backend  cycles idle
    13 683 671 982 instructions              #    1,34  insns per cycle
                                             #    0,46  stalled cycles per insn
     1 274 410 549 branches                  #  436,519 M/sec
           315 656 branch-misses             #    0,02% of all branches

$ perf stat -e 'L1-dcache-loads,L1-dcache-load-misses,L1-dcache-stores,L1-dcache-store-misses' ./a25
Size=134217728 ITERATIONS=8, TIME=2444.13 ms

 Performance counter stats for './a25':

     6 138 410 226 L1-dcache-loads
        77 025 747 L1-dcache-load-misses     #    1,25% of all L1-dcache hits
     2 515 141 824 L1-dcache-stores
        76 320 695 L1-dcache-store-misses

几乎相同的L1缺失率,但后端停顿更多。我能够获取“cache-references,cache-misses”事件的统计数据,并建议它们与L3缓存有关(对L2的请求次数多几倍)。
$ perf stat -e 'cache-references,cache-misses' ./a25
Size=134217728 ITERATIONS=8, TIME=2440.71 ms

 Performance counter stats for './a25':

        17 053 482 cache-references
        11 829 118 cache-misses              #   69,365 % of all cache refs

因此,缺失率很高,但测试执行了10亿个(有用的)负载,只有0.08亿个未命中L1。其中0.01亿个请求由内存服务。内存延迟大约为230 cpu clocks,而不是4个时钟周期的L1延迟。这个测试能否看到这一点?也许,如果噪音很小。

我可能会有更多问题,但现在困扰我的是:原作者在原帖中如何进行这个测试(链接中),或者那里的代码不足以知道?它完全依赖于他们使用的编译器/优化吗?我有点生气他们没有明确说明如何重现这个问题,但我也很高兴他们没有,因为现在我可以从你的帖子中学到东西! - skimon
我的单核 Pentium 4 2.5GHz,512KB L2 的测试数据:a5 5.7 秒,a10 5.7 秒,a15 6.8 秒,a20 7 秒,a25 7 秒。即使在您的测试中,缓存效应也是可见的。 - osgx
我没有阅读所有细节,但这个测试之所以不好的主要原因是它没有测量缓存读取带宽,而是混合了读/写带宽。执行array[x]++会引入一个读-修改-写序列,这将使该行在缓存中变脏,并迫使您将其全部驱逐到内存中,占用昂贵的缓存/总线带宽。@skimon,为什么不只是读取值(并在某个累加器中对它们求和以避免优化它们)? - Leeor
Leeor,在这里感谢你提供的所有答案。测试的问题在于它不是针对某个特定的东西进行测试(没有测量任何特定的内容)。这只是试图从链接的DrDobbs文章中复制代码的第一次错误尝试。RMW可能会很糟糕,并且会消耗可用的BW,但它对这个测试的影响非常小,因为测试生成的BW太低了:(对于最大的测试a25,使用128MB的数组)每秒10亿个4字节线性存储=每秒4GB(或每秒64百万行被驱逐)。i7-2600具有20 GB/s的内存BW,因此驱逐不是测试的限制因素。 - osgx
1
@HCSF,我认为这是基于Joky在这里的答案编写的代码:https://dev59.com/Nn_aa4cB1Zd3GeqPyRBE#23283006。 - osgx
显示剩余9条评论

1
一些结果(OSX,Sandy Bridge):

GCC -O0

Size=1 ITERATIONS=1073741824, TIME=2416.06 ms
Size=2 ITERATIONS=536870912, TIME=1885.46 ms
Size=4 ITERATIONS=268435456, TIME=1782.92 ms
Size=16 ITERATIONS=67108864, TIME=2023.71 ms
Size=32 ITERATIONS=33554432, TIME=2184.99 ms
Size=64 ITERATIONS=16777216, TIME=2464.09 ms
Size=256 ITERATIONS=4194304, TIME=2358.31 ms
Size=1024 ITERATIONS=1048576, TIME=2333.77 ms
Size=2048 ITERATIONS=524288, TIME=2340.16 ms
Size=4096 ITERATIONS=262144, TIME=2349.97 ms
Size=8192 ITERATIONS=131072, TIME=2346.96 ms
Size=16384 ITERATIONS=65536, TIME=2350.3 ms
Size=32768 ITERATIONS=32768, TIME=2348.71 ms
Size=65536 ITERATIONS=16384, TIME=2355.28 ms
Size=262144 ITERATIONS=4096, TIME=2358.97 ms
Size=524288 ITERATIONS=2048, TIME=2476.46 ms
Size=1048576 ITERATIONS=1024, TIME=2429.07 ms
Size=2097152 ITERATIONS=512, TIME=2427.09 ms
Size=4194304 ITERATIONS=256, TIME=2443.42 ms
Size=8388608 ITERATIONS=128, TIME=2435.54 ms
Size=33554432 ITERATIONS=32, TIME=2389.08 ms
Size=134217728 ITERATIONS=8, TIME=2444.43 ms
Size=536870912 ITERATIONS=2, TIME=2600.91 ms

GCC -O3

Size=1 ITERATIONS=1073741824, TIME=2197.12 ms
Size=2 ITERATIONS=536870912, TIME=996.409 ms
Size=4 ITERATIONS=268435456, TIME=606.252 ms
Size=16 ITERATIONS=67108864, TIME=306.904 ms
Size=32 ITERATIONS=33554432, TIME=897.692 ms
Size=64 ITERATIONS=16777216, TIME=847.794 ms
Size=256 ITERATIONS=4194304, TIME=802.136 ms
Size=1024 ITERATIONS=1048576, TIME=761.971 ms
Size=2048 ITERATIONS=524288, TIME=760.136 ms
Size=4096 ITERATIONS=262144, TIME=759.149 ms
Size=8192 ITERATIONS=131072, TIME=749.881 ms
Size=16384 ITERATIONS=65536, TIME=756.672 ms
Size=32768 ITERATIONS=32768, TIME=759.565 ms
Size=65536 ITERATIONS=16384, TIME=754.81 ms
Size=262144 ITERATIONS=4096, TIME=745.899 ms
Size=524288 ITERATIONS=2048, TIME=749.527 ms
Size=1048576 ITERATIONS=1024, TIME=758.009 ms
Size=2097152 ITERATIONS=512, TIME=776.671 ms
Size=4194304 ITERATIONS=256, TIME=778.963 ms
Size=8388608 ITERATIONS=128, TIME=783.191 ms
Size=33554432 ITERATIONS=32, TIME=770.603 ms
Size=134217728 ITERATIONS=8, TIME=785.703 ms
Size=536870912 ITERATIONS=2, TIME=911.875 ms

请注意第一个的速度较慢,我感觉可能在负载存储转发方面存在错误估计...

有趣的是,开启优化并删除volatile后,曲线变得更加平滑:

Size=1 ITERATIONS=1073741824, TIME=0 ms
Size=2 ITERATIONS=536870912, TIME=0 ms
Size=4 ITERATIONS=268435456, TIME=0 ms
Size=16 ITERATIONS=67108864, TIME=0.001 ms
Size=32 ITERATIONS=33554432, TIME=125.581 ms
Size=64 ITERATIONS=16777216, TIME=140.654 ms
Size=256 ITERATIONS=4194304, TIME=217.559 ms
Size=1024 ITERATIONS=1048576, TIME=168.155 ms
Size=2048 ITERATIONS=524288, TIME=159.031 ms
Size=4096 ITERATIONS=262144, TIME=154.373 ms
Size=8192 ITERATIONS=131072, TIME=153.858 ms
Size=16384 ITERATIONS=65536, TIME=156.819 ms
Size=32768 ITERATIONS=32768, TIME=156.505 ms
Size=65536 ITERATIONS=16384, TIME=156.921 ms
Size=262144 ITERATIONS=4096, TIME=215.911 ms
Size=524288 ITERATIONS=2048, TIME=220.298 ms
Size=1048576 ITERATIONS=1024, TIME=235.648 ms
Size=2097152 ITERATIONS=512, TIME=320.284 ms
Size=4194304 ITERATIONS=256, TIME=409.433 ms
Size=8388608 ITERATIONS=128, TIME=431.743 ms
Size=33554432 ITERATIONS=32, TIME=429.436 ms
Size=134217728 ITERATIONS=8, TIME=430.052 ms
Size=536870912 ITERATIONS=2, TIME=535.773 ms

为了帮助任何人复现“问题”,这里提供一些标准(我希望如此)的C++代码:
#include <string>
#include <iostream>
#include <chrono>
#include <cstdlib>
#include <memory>

template <unsigned int SIZE, unsigned int ITERATIONS>
void test_body(volatile int *array) {
    for (int i = 0; i < ITERATIONS; i++)
    {
        for (int  x = 0; x < SIZE; x++)
        {
            array[x]++;
        }
    }

}


template <unsigned int SIZE, unsigned int ITERATIONS>
static void test_function()
{
    static_assert(SIZE*ITERATIONS == 1024*1024*1024, "SIZE MISMATCH");
    std::unique_ptr<volatile int[]> array { new int[SIZE] };

    // Warmup
    test_body<SIZE, 1>(array.get());

    auto start = std::chrono::steady_clock::now();

    test_body<SIZE, ITERATIONS>(array.get());

    auto end = std::chrono::steady_clock::now();
    auto diff = end - start;
    std::cout << "Size=" << SIZE << " ITERATIONS=" << ITERATIONS << ", TIME=" << std::chrono::duration <double, std::milli> (diff).count() << " ms" << std::endl;
}

int main()
{
    enum { eIterations= 1024*1024*1024};
    test_function<1, eIterations>();
    test_function<2, eIterations/2>();
    test_function<4, eIterations/4>();
    test_function<16, eIterations/16>();
    test_function<32, eIterations/ 32>();
    test_function<64, eIterations/ 64>();
    test_function<256, eIterations/ 256>();
    test_function<1024, eIterations/ 1024>();
    test_function<2048, eIterations/ 2048>();
    test_function<4096, eIterations/ 4096>();
    test_function<8192, eIterations/ 8192>();
    test_function<16384, eIterations/ 16384>();
    test_function<32768, eIterations/ 32768>();
    test_function<65536, eIterations/ 65536>();
    test_function<262144, eIterations/ 262144>();
    test_function<524288, eIterations/ 524288>();
    test_function<1048576, eIterations/ 1048576>();
    test_function<2097152, eIterations/ 2097152>();
    test_function<4194304, eIterations/ 4194304>();
    test_function<8388608, eIterations/ 8388608>();
    test_function<33554432, eIterations/ 33554432>();
    test_function<134217728, eIterations/ 134217728>();
    test_function<536870912, eIterations/ 536870912>();
}

谢谢您做这件事。我怀疑如果您启用优化并删除volatile,我预计编译器会完全优化掉该数组,因为它没有被使用? - skimon
编译器不够智能 :) 但它可以保存显式刷新并允许重新排序指令。 - Joky
2
它还可以进行向量化,并且会搞乱多个事情。我只是把它放在这里作为信息,它并不代表... - Joky

1
看起来很明显,常数时间意味着恒定的指令执行速率。为了衡量缓存/RAM速度,数据传输指令应占主导地位,并且结果需要比运行时间更进一步的澄清,例如MB/秒和每秒指令数。您需要像我的BusSpeed基准测试这样的东西(在Google上搜索Roy BusSpeed基准测试或BusSpd2k获取源代码和Windows、Linux和Android版本的结果)。原始代码使用类似以下指令的汇编代码:
   "add     edx,ecx"     \
   "mov     ebx,[edi]"   \
   "mov     ecx,ebx"     \
"lp: and     ebx,[edx]"   \
   "and     ecx,[edx+4]"   \
   "and     ebx,[edx+8]"   \
   "and     ecx,[edx+12]"   \
   "and     ebx,[edx+16]"   \
   "and     ecx,[edx+20]"   \
   "and     ebx,[edx+24]"   \
   "and     ecx,[edx+28]"   \
   "and     ebx,[edx+32]"   \
   "and     ecx,[edx+36]"   \
   "and     ebx,[edx+40]"   \

 To

   "and     ecx,[edx+236]"   \
   "and     ebx,[edx+240]"   \
   "and     ecx,[edx+244]"   \
   "and     ebx,[edx+248]"   \
   "and     ecx,[edx+252]"   \
   "add     edx,256"     \
   "dec     eax"         \
   "jnz     lp"          \
   "and     ebx,ecx"     \
   "mov     [edi],ebx"     \             

后续版本使用C语言,如下所示。
void inc1word()
{
   int i, j;

   for(j=0; j<passes1; j++)
   {
       for (i=0; i<wordsToTest; i=i+64)
       {
           andsum1 = andsum1 & array[i   ] & array[i+1 ] & array[i+2 ] & array[i+3 ]
                             & array[i+4 ] & array[i+5 ] & array[i+6 ] & array[i+7 ]
                             & array[i+8 ] & array[i+9 ] & array[i+10] & array[i+11]
                             & array[i+12] & array[i+13] & array[i+14] & array[i+15]
                             & array[i+16] & array[i+17] & array[i+18] & array[i+19]
                             & array[i+20] & array[i+21] & array[i+22] & array[i+23]
                             & array[i+24] & array[i+25] & array[i+26] & array[i+27]
                             & array[i+28] & array[i+29] & array[i+30] & array[i+31]
                             & array[i+32] & array[i+33] & array[i+34] & array[i+35]
                             & array[i+36] & array[i+37] & array[i+38] & array[i+39]
                             & array[i+40] & array[i+41] & array[i+42] & array[i+43]
                             & array[i+44] & array[i+45] & array[i+46] & array[i+47]
                             & array[i+48] & array[i+49] & array[i+50] & array[i+51]
                             & array[i+52] & array[i+53] & array[i+54] & array[i+55]
                             & array[i+56] & array[i+57] & array[i+58] & array[i+59]
                             & array[i+60] & array[i+61] & array[i+62] & array[i+63];
       }
   }
}

该基准测试衡量缓存和RAM的MB/秒,包括跳过顺序寻址以查看数据在突发读取中的读取位置。以下是示例结果。请注意突发读取效果以及读取到两个不同寄存器(来自汇编代码版本的Reg2)可能比1个寄存器更快。然后,在这种情况下,将每个字加载到1个寄存器(AndI、Reg1、Inc4字节)会产生几乎恒定的速度(约为1400 MIPS)。因此,即使是一长串指令也可能不适合特定的流水线)。找出方法是运行更广泛的测试变化。

######################################################################### Intel(R) Core(TM) i7 CPU 930 @ 2.80GHz 测量值 2807 MHz

         Windows Bus Speed Test Version 2.2 by Roy Longbottom

  Minimum      0.100 seconds per test, Start Fri Jul 30 16:43:56 2010

          MovI  MovI  MovI  MovI  MovI  MovI  AndI  AndI  MovM  MovM
  Memory  Reg2  Reg2  Reg2  Reg2  Reg1  Reg2  Reg1  Reg2  Reg1  Reg8
  KBytes Inc64 Inc32 Inc16  Inc8  Inc4  Inc4  Inc4  Inc4  Inc8  Inc8
   Used   MB/S  MB/S  MB/S  MB/S  MB/S  MB/S  MB/S  MB/S  MB/S  MB/S

      4  10025 10800 11262 11498 11612 11634  5850 11635 23093 23090
      8  10807 11267 11505 11627 11694 11694  5871 11694 23299 23297
     16  11251 11488 11620 11614 11712 11719  5873 11718 23391 23398
     32   9893  9853 10890 11170 11558 11492  5872 11466 21032 21025
     64   3219  4620  7289  9479 10805 10805  5875 10797 14426 14426
    128   3213  4805  7305  9467 10811 10810  5875 10805 14442 14408
    256   3144  4592  7231  9445 10759 10733  5870 10743 14336 14337
    512   2005  3497  5980  9056 10466 10467  5871 10441 13906 13905
   1024   2003  3482  5974  9017 10468 10466  5874 10467 13896 13818
   2048   2004  3497  5958  9088 10447 10448  5870 10447 13857 13857
   4096   1963  3398  5778  8870 10328 10328  5851 10328 13591 13630
   8192   1729  3045  5322  8270  9977  9963  5728  9965 12923 12892
  16384    692  1402  2495  4593  7811  7782  5406  7848  8335  8337
  32768    695  1406  2492  4584  7820  7826  5401  7792  8317  8322
  65536    695  1414  2488  4584  7823  7826  5403  7800  8321  8321
 131072    696  1402  2491  4575  7827  7824  5411  7846  8322  8323
 262144    696  1413  2498  4594  7791  7826  5409  7829  8333  8334
 524288    693  1416  2498  4595  7841  7842  5411  7847  8319  8285
1048576    704  1415  2478  4591  7845  7840  5410  7853  8290  8283

                  End of test Fri Jul 30 16:44:29 2010

MM使用1和8个MMX寄存器,后续版本使用SSE。
源代码和执行文件可供任何人自由使用。文件位于以下位置,其中显示了数组声明:

Windows http://www.roylongbottom.org.uk/busspd2k.zip

 xx = (int *)VirtualAlloc(NULL, useMemK*1024+256, MEM_COMMIT, PAGE_READWRITE);

Linux http://www.roylongbottom.org.uk/memory_benchmarks.tar.gz

#ifdef Bits64
   array = (long long *)_mm_malloc(memoryKBytes[ipass-1]*1024, 16);
#else
   array = (int *)_mm_malloc(memoryKBytes[ipass-1]*1024, 16);

结果和其他链接(MP版本,Android)在以下位置:

http://www.roylongbottom.org.uk/busspd2k%20results.htm


Roy,感谢您提供的主循环代码。您如何声明“array”变量?您的基准测试是否开源? - osgx
1
请查看上面的评论。 - Roy Longbottom

0

我不明白什么是常数时间。我稍微修改了你的代码,使它更简单。我的时间比你的短得多,但我不确定为什么。一开始的大时间很有道理,因为只有少数值需要写入,所以存在依赖链。L2缓存在256k/4=64k处结束。请注意,在size=32768和65536之间,值开始上升。

//GCC -O3 Intel(R) Xeon(R) CPU E5-1620 0 @ 3.60GHz
Size=1, Iterations=1073741824, Time=187.18 ms
Size=2, Iterations=536870912, Time=113.47 ms
Size=4, Iterations=268435456, Time=50.53 ms
Size=8, Iterations=134217728, Time=25.02 ms
Size=16, Iterations=67108864, Time=25.61 ms
Size=32, Iterations=33554432, Time=24.08 ms
Size=64, Iterations=16777216, Time=22.69 ms
Size=128, Iterations=8388608, Time=22.03 ms
Size=256, Iterations=4194304, Time=19.98 ms
Size=512, Iterations=2097152, Time=17.09 ms
Size=1024, Iterations=1048576, Time=15.66 ms
Size=2048, Iterations=524288, Time=14.94 ms
Size=4096, Iterations=262144, Time=14.58 ms
Size=8192, Iterations=131072, Time=14.40 ms
Size=16384, Iterations=65536, Time=14.63 ms
Size=32768, Iterations=32768, Time=14.75 ms
Size=65536, Iterations=16384, Time=18.58 ms
Size=131072, Iterations=8192, Time=20.51 ms
Size=262144, Iterations=4096, Time=21.18 ms
Size=524288, Iterations=2048, Time=21.26 ms
Size=1048576, Iterations=1024, Time=21.22 ms
Size=2097152, Iterations=512, Time=22.17 ms
Size=4194304, Iterations=256, Time=38.01 ms
Size=8388608, Iterations=128, Time=38.63 ms
Size=16777216, Iterations=64, Time=38.09 ms
Size=33554432, Iterations=32, Time=38.54 ms
Size=67108864, Iterations=16, Time=39.11 ms
Size=134217728, Iterations=8, Time=39.96 ms
Size=268435456, Iterations=4, Time=42.15 ms
Size=536870912, Iterations=2, Time=46.39 ms

代码:

#include <stdio.h>
#include <omp.h>

static void test_function(int n, int iterations)
{
    int *array = new int[n];
    for (int i = 0; i < iterations; i++)
        for (int x = 0; x < n; x++)
          array[x]++;
    delete[] array;
}

int main() {        
    for(int i=0, n=1, iterations=1073741824; i<30; i++, n*=2, iterations/=2) {
        double dtime;
        dtime = omp_get_wtime();
        test_function(n, iterations);
        dtime = omp_get_wtime() - dtime;
        printf("Size=%d, Iterations=%d, Time=%.3f\n", n, iterations, dtime);
    }
}

Z玻色子,您更改了数组的类型,声明时没有使用volatile。启用优化后,编译器可能会将test_function中的循环优化为类似于for (int x = 0; x < n; x++) array[x] += iterations的形式。请检查您的汇编代码或在此处发布。 - osgx

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接