无法重现不友好的 C++ 缓存代码

Couldn't Reproduce non-friendly C++ cache code

我正在玩一些代码块,它们有意尝试展示有关 CPU 缓存的想法,以及如何从对齐数据中获益等等。

从这篇文章 http://igoro.com/archive/gallery-of-processor-cache-effects/ 中我发现它非常有趣我正在尝试重现这两个循环的行为(示例 1):

int[] arr = new int[64 * 1024 * 1024];

// Loop 1
for (int i = 0; i < arr.Length; i++) arr[i] *= 3;

// Loop 2
for (int i = 0; i < arr.Length; i += 16) arr[i] *= 3;

注意第二个循环中的因子 16,因为在我的机器(Intel i5)中 int 的长度为 4 个字节,第二个循环中的每个元素都将位于不同的缓存行中,因此从理论上讲,根据作者的说法,这两个循环将花费几乎相同的时间来执行,因为这里的速度是由内存访问决定的,而不是由整数乘法指​​令决定的。

所以我尝试重现这种行为,使用两个不同的编译器,MSVC 19.31.31105.0 和 GNU g++ 9.4.0,两种情况下的结果相同,第二个循环大约需要 4%时间相对于第一个循环。这是我用来重现它的实现

#include <cstdlib>
#include <chrono>
#include <vector>
#include <string>

#define SIZE 64 * 1024 * 1024
#define K 16

//Comment this to use C array instead of std vector
//#define USE_C_ARRAY 

int main() {

#if defined(USE_C_ARRAY)
    int* arr = new int[SIZE];
    std::cout << "Testing with C style array\n";
#else 
    std::vector<int> arr(SIZE);
    std::cout << "Using std::vector\n";
#endif

    std::cout << "Step:           " << K << " Elements \n";
    std::cout << "Array Elements: " << SIZE << "\n";
    std::cout << "Array Size:     " << SIZE/(1024*1024) << " MBs \n";
    std::cout << "Int Size:       " << sizeof(int) << "\n";

    auto show_duration = [&](auto title, auto duration){ 
        std::cout << "[" << title << "] Duration: " << 
            std::chrono::duration_cast<std::chrono::milliseconds>(duration).count() <<
            " (milisecs) \n";
    };

    auto start = std::chrono::steady_clock::now();
    // Loop 1
    for (int i = 0; i < SIZE; i++) arr[i]++;
    show_duration("FullArray", 
                (std::chrono::steady_clock::now() - start));


    start = std::chrono::steady_clock::now();
    // Loop 1
    for (int i = 0; i < SIZE; i += K) arr[i]++;

    show_duration(std::string("Array Step ") + std::to_string(K), 
                (std::chrono::steady_clock::now() - start));

#if defined(USE_C_ARRAY)
    delete[] arr;
#endif
    return EXIT_SUCCESS;
}

用g++编译:

 g++ -o for_cache_demo for_cache_demo.cpp -std=c++17 -g3                                                             

注 1:我使用 -g3 明确表明我正在避免任何编译器优化,只是为了查看原始行为。

注2:两次循环生成的汇编代码的区别在于循环计数器add函数的第二个参数,即i加1或K

add     DWORD PTR [rbp-4], 16 # When using a 16 Step 

Link 在 Compiler Explorer 中测试:https://godbolt.org/z/eqWdce35z

如果您对我可能遗漏的内容有任何建议或想法,那就太好了,非常感谢!

你的错误是没有使用任何优化。 non-optimized 代码的开销掩盖了任何缓存效果。

当我不使用任何优化时,我得到以下信息:

$ g++ -O0 cache.cpp -o cache
$ ./cache 
Using std::vector
Step:           16 Elements 
Array Elements: 67108864
Array Size:     64 MBs 
Int Size:       4
[FullArray] Duration: 105 (milisecs) 
[Array Step 16] Duration: 23 (milisecs) 

如果我使用优化,即使是最保守的优化标志,-O1,结果突然改变:

$ g++ -O1 cache.cpp -o cache
$ ./cache 
Using std::vector
Step:           16 Elements 
Array Elements: 67108864
Array Size:     64 MBs 
Int Size:       4
[FullArray] Duration: 26 (milisecs) 
[Array Step 16] Duration: 22 (milisecs)

(可以看到step 16的运行时间变化不大,因为已经被内存访问瓶颈了,而step 1访问的运行时间大幅提升,因为是受执行指令限制,不受内存带宽限制)

进一步提高优化级别并没有改变我的结果。

您可以比较不同优化级别的内部循环。随着 -O0:

vector_step_1(std::vector<int, std::allocator<int> >&):
        push    rbp
        mov     rbp, rsp
        sub     rsp, 32
        mov     QWORD PTR [rbp-24], rdi
        mov     DWORD PTR [rbp-4], 0
.L7:
        cmp     DWORD PTR [rbp-4], 67108863
        jg      .L8
        mov     eax, DWORD PTR [rbp-4]
        movsx   rdx, eax
        mov     rax, QWORD PTR [rbp-24]
        mov     rsi, rdx
        mov     rdi, rax
        call    std::vector<int, std::allocator<int> >::operator[](unsigned long)
        mov     rdx, rax
        mov     ecx, DWORD PTR [rdx]
        mov     eax, ecx
        add     eax, eax
        add     eax, ecx
        mov     DWORD PTR [rdx], eax
        add     DWORD PTR [rbp-4], 1
        jmp     .L7
.L8:
        nop
        leave
        ret

您可以看到它在每次迭代时都会调用整个函数。

-O1:

vector_step_1(std::vector<int, std::allocator<int> >&):
        mov     eax, 0
.L5:
        mov     rdx, rax
        add     rdx, QWORD PTR [rdi]
        mov     ecx, DWORD PTR [rdx]
        lea     ecx, [rcx+rcx*2]
        mov     DWORD PTR [rdx], ecx
        add     rax, 4
        cmp     rax, 268435456
        jne     .L5
        ret

现在,循环中只有 8 条指令。在 -O2 时,每个循环变成 6 条指令。