在这种特殊情况下,为什么数据类型会影响性能?

Why is data type having an effect on performance in this particular case?

我编写了以下代码来对缓存未命中对性能的影响进行基准测试:

#include <chrono>
#include <cstdint>
#include <cstring>
#include <iostream>

// Avoiding using power of 2 because of possible performance degradation due to cache associativity?
static const size_t ROW_SIZE   = 600;
static const size_t COL_SIZE   = 600;
static const size_t TEST_COUNT = 50;

#define SAME_TYPES     1
#define INIT_TO_ONE    0

#if SAME_TYPES
#define ARY_TYPE uint64_t
#define RET_TYPE uint64_t
#else
#define ARY_TYPE uint32_t
#define RET_TYPE uint64_t
#endif

RET_TYPE sum_array_rows(const ARY_TYPE *a, const size_t step)
{
    RET_TYPE sum = 0;
    for (size_t row = 0; row < ROW_SIZE; row++)
        for (size_t col = 0; col < COL_SIZE; col++)
            sum += static_cast<RET_TYPE>(a[row * step + col]);

    return sum;
}

RET_TYPE sum_array_cols(const ARY_TYPE *a, const size_t step)
{
    RET_TYPE sum = 0;
    for (size_t col = 0; col < COL_SIZE; col++)
        for (size_t row = 0; row < ROW_SIZE; row++)
            sum += static_cast<RET_TYPE>(a[row * step + col]);

    return sum;
}

int main()
{
#if INIT_TO_ONE
    ARY_TYPE *a = new ARY_TYPE[ROW_SIZE * COL_SIZE];
    for (int i = 0; i < ROW_SIZE * COL_SIZE; i++) a[i] = 1;
#else
    ARY_TYPE *a = new ARY_TYPE[ROW_SIZE * COL_SIZE]();
#endif

    std::chrono::high_resolution_clock hrc;

    std::cout << "SAME_TYPES:  " << SAME_TYPES << "\n";
    std::cout << "INIT_TO_ONE: " << INIT_TO_ONE << "\n";
    std::cout << "ROW_SIZE:    " << ROW_SIZE << "\n";
    std::cout << "COL_SIZE:    " << COL_SIZE << "\n\n";

    {
        RET_TYPE sum = 0;
        auto start = hrc.now();

        for (int i = 0; i < TEST_COUNT; i++) sum = sum_array_rows(a, COL_SIZE);

        auto end = hrc.now();
        std::cout << "Time taken: " << (end - start).count() / TEST_COUNT << "\n";
        std::cout << "Sum:        " << sum << "\n";
    }

    // I've added this because I want to trash the cache
    // I'm not sure if this is necessary or if it's doing any good...
    ARY_TYPE *other = new ARY_TYPE[ROW_SIZE * COL_SIZE];
    for (int i = 0; i < ROW_SIZE * COL_SIZE; i++) other[i] = 1;

    {
        RET_TYPE sum = other[ROW_SIZE] - 1;
        auto start = hrc.now();

        for (int i = 0; i < TEST_COUNT; i++) sum = sum_array_cols(a, COL_SIZE);

        auto end = hrc.now();
        std::cout << "Time taken: " << (end - start).count() / TEST_COUNT << "\n";
        std::cout << "Sum:        " << sum << "\n";
    }

    return 0;
}

我有两个函数 sum_array_rowssum_array_cols,它们接受一个数组并添加元素,return 求和。区别在于我们访问元素的顺序。两个函数的 return 类型总是 uint64_t.

我在文件顶部附近有一个名为 SAME_TYPES 的定义。如果 SAME_TYPES 那么 return 类型和元素类型都是 uint64_t。如果不是 SAME_TYPES 则元素类型为 uint32_t.

所以 运行 这个代码给了我...

SAME_TYPES 设置为 1:

SAME_TYPES:  1
INIT_TO_ONE: 0
ROW_SIZE:    600
COL_SIZE:    600

Time taken: 80948  (element type is uint64_t, ROW first)
Sum:        0
Time taken: 566241 (element type is uint64_t, COL first)
Sum:        0

SAME_TYPES 设置为 0:

SAME_TYPES:  0
INIT_TO_ONE: 0
ROW_SIZE:    600
COL_SIZE:    600

Time taken: 283348 (element type is uint32_t, ROW first)
Sum:        0
Time taken: 369653 (element type is uint32_t, COL first)
Sum:        0

我很困惑为什么这会对性能产生任何影响。当数据类型相同时,结果似乎是预期的,row-col 比 col-row 快。但是,为什么当类型不同时,我会看到 row-col 的减少和 col-row 的 增加。我知道如果元素的数据类型更大,那么我可以将更少的内容放入缓存,但是为什么在外循环中迭代列时我会看到性能提高。有人可以给我解释一下吗?

以防万一,我正在使用 VS 2015 进行编译,我的 CPU 是 i5-4460 (运行 Windows 10)。


编辑:我想我不应该盲目地相信编译器。我最初使用默认设置 /02 进行编译。当我在没有优化的情况下编译时,我得到了预期的行为:

SAME_TYPES 设置为 1:

SAME_TYPES:  1
INIT_TO_ONE: 0
ROW_SIZE:    600
COL_SIZE:    600

Time taken: 1009518 (element type is uint64_t, ROW first)
Sum:        0
Time taken: 1504863 (element type is uint64_t, COL first)
Sum:        0

SAME_TYPES 设置为 0:

SAME_TYPES:  0
INIT_TO_ONE: 0
ROW_SIZE:    600
COL_SIZE:    600

Time taken: 909479  (element type is uint32_t, ROW first)
Sum:        0
Time taken: 1244492 (element type is uint32_t, COL first)
Sum:        0

数据类型仍然有效,但现在看来是合理的。这是编译优化时的程序集:

Optimisation ... /O2
SAME_TYPE ...... 1

    RET_TYPE sum_array_rows(const ARY_TYPE *a, const size_t step)
    {
        00FD10C0  xorps       xmm2,xmm2  
            RET_TYPE sum = 0;
        00FD10C3  lea         eax,[ecx+10h]  
        00FD10C6  movaps      xmm1,xmm2  
        00FD10C9  mov         edx,258h  
        00FD10CE  xchg        ax,ax  
                for (size_t col = 0; col < COL_SIZE; col++)
        00FD10D0  mov         ecx,96h  
                    sum += static_cast<RET_TYPE>(a[row * step + col]);
        00FD10D5  movups      xmm0,xmmword ptr [eax-10h]  
        00FD10D9  paddq       xmm2,xmm0  
        00FD10DD  movups      xmm0,xmmword ptr [eax]  
        00FD10E0  add         eax,20h  
        00FD10E3  paddq       xmm1,xmm0  
        00FD10E7  sub         ecx,1  
        00FD10EA  jne         sum_array_rows+15h (0FD10D5h)  
            for (size_t row = 0; row < ROW_SIZE; row++)
        00FD10EC  sub         edx,1  
            for (size_t row = 0; row < ROW_SIZE; row++)
        00FD10EF  jne         sum_array_rows+10h (0FD10D0h)  

            return sum;
        }
        00FD10F1  paddq       xmm1,xmm2  
        00FD10F5  movaps      xmm0,xmm1  
        00FD10F8  psrldq      xmm0,8  
        00FD10FD  paddq       xmm1,xmm0  
        00FD1101  movd        eax,xmm1  
        00FD1105  psrldq      xmm1,4  
        00FD110A  movd        edx,xmm1  
        00FD110E  ret  


    RET_TYPE sum_array_cols(const ARY_TYPE *a, const size_t step)
    {
        00FD1110  push        ebp  
        00FD1111  mov         ebp,esp  
        00FD1113  sub         esp,24h  
        00FD1116  push        ebx  
        00FD1117  xorps       xmm0,xmm0  
            RET_TYPE sum = 0;
        00FD111A  mov         dword ptr [ebp-10h],258h  
        00FD1121  push        esi  
        00FD1122  movlpd      qword ptr [ebp-18h],xmm0  
        00FD1127  lea         eax,[ecx+2580h]  
        00FD112D  mov         edx,dword ptr [ebp-14h]  
        00FD1130  push        edi  
        00FD1131  mov         edi,dword ptr [sum]  
        00FD1134  mov         dword ptr [ebp-0Ch],eax  
        00FD1137  nop         word ptr [eax+eax]  
                for (size_t row = 0; row < ROW_SIZE; row++)
        00FD1140  xorps       xmm0,xmm0  
        00FD1143  mov         dword ptr [ebp-8],0C8h  
        00FD114A  movlpd      qword ptr [sum],xmm0  
        00FD114F  mov         ecx,dword ptr [ebp-18h]  
        00FD1152  mov         ebx,dword ptr [ebp-14h]  
        00FD1155  movlpd      qword ptr [ebp-20h],xmm0  
        00FD115A  mov         esi,dword ptr [ebp-20h]  
        00FD115D  mov         dword ptr [ebp-4],ecx  
        00FD1160  mov         ecx,dword ptr [ebp-1Ch]  
        00FD1163  nop         dword ptr [eax]  
        00FD1167  nop         word ptr [eax+eax]  
                    sum += static_cast<RET_TYPE>(a[row * step + col]);
        00FD1170  add         edi,dword ptr [eax-2580h]  
        00FD1176  mov         dword ptr [sum],edi  
        00FD1179  adc         edx,dword ptr [eax-257Ch]  
        00FD117F  mov         edi,dword ptr [ebp-4]  
        00FD1182  add         edi,dword ptr [eax-12C0h]  
        00FD1188  mov         dword ptr [ebp-4],edi  
        00FD118B  adc         ebx,dword ptr [eax-12BCh]  
        00FD1191  add         esi,dword ptr [eax]  
        00FD1193  mov         edi,dword ptr [sum]  
        00FD1196  adc         ecx,dword ptr [eax+4]  
        00FD1199  lea         eax,[eax+3840h]  
        00FD119F  sub         dword ptr [ebp-8],1  
        00FD11A3  jne         sum_array_cols+60h (0FD1170h)  
            for (size_t col = 0; col < COL_SIZE; col++)
        00FD11A5  add         esi,dword ptr [ebp-4]  
        00FD11A8  mov         eax,dword ptr [ebp-0Ch]  
        00FD11AB  adc         ecx,ebx  
        00FD11AD  add         edi,esi  
        00FD11AF  adc         edx,ecx  
        00FD11B1  add         eax,8  
        00FD11B4  sub         dword ptr [ebp-10h],1  
        00FD11B8  mov         dword ptr [ebp-0Ch],eax  
        00FD11BB  jne         sum_array_cols+30h (0FD1140h)  

            return sum;
        00FD11BD  mov         eax,edi  
        }
        00FD11BF  pop         edi  
        00FD11C0  pop         esi  
        00FD11C1  pop         ebx  
        00FD11C2  mov         esp,ebp  
        00FD11C4  pop         ebp  
        00FD11C5  ret  


================

Optimisation ... /O2
SAME_TYPE ...... 0

    RET_TYPE sum_array_rows(const ARY_TYPE *a, const size_t step)
    {
        00A110C0  push        ebp  
        00A110C1  mov         ebp,esp  
        00A110C3  sub         esp,24h  
        00A110C6  push        ebx  
        00A110C7  xorps       xmm0,xmm0  
            RET_TYPE sum = 0;
        00A110CA  mov         dword ptr [ebp-0Ch],258h  
        00A110D1  movlpd      qword ptr [ebp-18h],xmm0  
        00A110D6  mov         edx,dword ptr [ebp-14h]  
        00A110D9  mov         eax,dword ptr [sum]  
        00A110DC  push        esi  
        00A110DD  push        edi  
        00A110DE  xchg        ax,ax  
                for (size_t col = 0; col < COL_SIZE; col++)
        00A110E0  xorps       xmm0,xmm0  
        00A110E3  mov         dword ptr [ebp-8],0C8h  
        00A110EA  movlpd      qword ptr [sum],xmm0  
        00A110EF  mov         esi,dword ptr [ebp-18h]  
        00A110F2  mov         ebx,dword ptr [ebp-14h]  
                for (size_t col = 0; col < COL_SIZE; col++)
        00A110F5  movlpd      qword ptr [ebp-20h],xmm0  
        00A110FA  mov         edi,dword ptr [ebp-20h]  
        00A110FD  mov         dword ptr [ebp-4],esi  
        00A11100  mov         esi,dword ptr [ebp-1Ch]  
                    sum += static_cast<RET_TYPE>(a[row * step + col]);
        00A11103  add         eax,dword ptr [ecx]  
        00A11105  adc         edx,0  
        00A11108  mov         dword ptr [sum],edx  
        00A1110B  mov         edx,dword ptr [ebp-4]  
        00A1110E  add         edx,dword ptr [ecx+4]  
        00A11111  mov         dword ptr [ebp-4],edx  
        00A11114  mov         edx,dword ptr [sum]  
        00A11117  adc         ebx,0  
        00A1111A  add         edi,dword ptr [ecx+8]  
        00A1111D  adc         esi,0  
        00A11120  add         ecx,0Ch  
        00A11123  sub         dword ptr [ebp-8],1  
        00A11127  jne         sum_array_rows+43h (0A11103h)  
            for (size_t row = 0; row < ROW_SIZE; row++)
        00A11129  add         edi,dword ptr [ebp-4]  
        00A1112C  adc         esi,ebx  
        00A1112E  add         eax,edi  
        00A11130  adc         edx,esi  
        00A11132  sub         dword ptr [ebp-0Ch],1  
        00A11136  jne         sum_array_rows+20h (0A110E0h)  

            return sum;
        }
        00A11138  pop         edi  
        00A11139  pop         esi  
        00A1113A  pop         ebx  
        00A1113B  mov         esp,ebp  
        00A1113D  pop         ebp  
        00A1113E  ret  


    RET_TYPE sum_array_cols(const ARY_TYPE *a, const size_t step)
    {
        00A11140  push        ebp  
        00A11141  mov         ebp,esp  
        00A11143  sub         esp,24h  
        00A11146  push        ebx  
        00A11147  xorps       xmm0,xmm0  
            RET_TYPE sum = 0;
        00A1114A  mov         dword ptr [ebp-10h],258h  
        00A11151  push        esi  
        00A11152  movlpd      qword ptr [ebp-18h],xmm0  
        00A11157  lea         eax,[ecx+12C0h]  
        00A1115D  mov         edx,dword ptr [ebp-14h]  
        00A11160  push        edi  
        00A11161  mov         edi,dword ptr [sum]  
        00A11164  mov         dword ptr [ebp-0Ch],eax  
        00A11167  nop         word ptr [eax+eax]  
                for (size_t row = 0; row < ROW_SIZE; row++)
        00A11170  xorps       xmm0,xmm0  
        00A11173  mov         dword ptr [ebp-8],0C8h  
        00A1117A  movlpd      qword ptr [sum],xmm0  
        00A1117F  mov         ecx,dword ptr [ebp-18h]  
        00A11182  mov         ebx,dword ptr [ebp-14h]  
        00A11185  movlpd      qword ptr [ebp-20h],xmm0  
        00A1118A  mov         esi,dword ptr [ebp-20h]  
        00A1118D  mov         dword ptr [ebp-4],ecx  
        00A11190  mov         ecx,dword ptr [ebp-1Ch]  
        00A11193  nop         dword ptr [eax]  
        00A11197  nop         word ptr [eax+eax]  
                    sum += static_cast<RET_TYPE>(a[row * step + col]);
        00A111A0  add         edi,dword ptr [eax-12C0h]  
        00A111A6  lea         eax,[eax+1C20h]  
        00A111AC  adc         edx,0  
        00A111AF  mov         dword ptr [sum],edx  
        00A111B2  mov         edx,dword ptr [ebp-4]  
        00A111B5  add         edx,dword ptr [eax-2580h]  
        00A111BB  mov         dword ptr [ebp-4],edx  
        00A111BE  mov         edx,dword ptr [sum]  
        00A111C1  adc         ebx,0  
        00A111C4  add         esi,dword ptr [eax-1C20h]  
        00A111CA  adc         ecx,0  
        00A111CD  sub         dword ptr [ebp-8],1  
        00A111D1  jne         sum_array_cols+60h (0A111A0h)  
            for (size_t col = 0; col < COL_SIZE; col++)
        00A111D3  add         esi,dword ptr [ebp-4]  
        00A111D6  mov         eax,dword ptr [ebp-0Ch]  
        00A111D9  adc         ecx,ebx  
        00A111DB  add         edi,esi  
        00A111DD  adc         edx,ecx  
        00A111DF  add         eax,4  
        00A111E2  sub         dword ptr [ebp-10h],1  
        00A111E6  mov         dword ptr [ebp-0Ch],eax  
        00A111E9  jne         sum_array_cols+30h (0A11170h)  

            return sum;
        00A111EB  mov         eax,edi  
        }
        00A111ED  pop         edi  
        }
        00A111EE  pop         esi  
        00A111EF  pop         ebx  
        00A111F0  mov         esp,ebp  
        00A111F2  pop         ebp  
        00A111F3  ret  

简短的回答是,优化器试图变得聪明,但它的启发式方法在类型大小不同的情况下失败了,最终会减慢代码速度。

让我们首先查看使用 64 位源数据为 sum_array_rows 的内循环生成的代码。

innerloop:
    movups xmm0,[eax-0x10]
    paddq xmm2,xmm0
    movups xmm0,[eax]
    add eax,0x20
    paddq xmm1,xmm0
    sub ecx,1
    jne innerloop

这大致等同于以下使用内部函数的 C 代码。

do {
    sum1 = _mm_add_epi64(sum1, _mm_loadu_si128(&ptr[0]));
    sum2 = _mm_add_epi64(sum2, _mm_loadu_si128(&ptr[1]));
    ptr += 2;
} while(--count);

我们在这里看到的是,优化器已确定加法是关联的,因此将循环展开到四个并行累加器上,最终在循环结束时将它们加在一起。这允许 CPU 并行执行独立计算,更重要的是允许使用 SSE2 指令集在单个指令中添加 64 位整数对来进行矢量化。

这是一个的结果。


在频谱的另一边,我们有 32 位源数据的 64 位累积版本:

innerloop:
    add eax,[ecx]
    adc edx,0
    mov [sum],edx
    mov edx,[ebp-4]
    add edx,[ecx+4]
    mov [ebp-4],edx
    mov edx,[sum]
    adc ebx,0
    add edi,[ecx+8]
    adc esi,0
    add ecx,12
    sub [ebp-8],1
    jne innerloop

请注意,32 位 x86 目标缺乏对使用普通(非向量)指令集的 64 位算术的本机支持。因此,每个累加器被分成单独的高位字变量和低位字变量,低位字的进位手动传播到高位字。

此外,循环展开了三次而不是四次。

伪C版大概是这样写的:

do {
    sum1_lo += ptr[0]; if(carry) ++sum1_hi;
    sum2_lo += ptr[1]; if(carry) ++sum2_hi;
    sum3_lo += ptr[2]; if(carry) ++sum3_hi;
    ptr += 3;
} while(--count);

遗憾的是,这里的展开是悲观的,因为处理器 运行 超出了寄存器,被迫将 sum1_lo/sum2_locount 分流到内存中。展开因子为 2 是正确的选择,甚至没有展开更快。

在本机 64 位整数上使用并行加法的矢量化版本仍然是可能的。但是,这需要先解压缩源数据。沿着这些线的东西:

_m128i zero = __mm_setzero_epi128();
do {
    _m128i data = _mm_loadu_si128(*ptr++);
    sum1 = _mm_add_epi64(sum1, _mm_unpacklo_epi64(data, zero));
    sum2 = _mm_add_epi64(sum2, _mm_unpackhi_epi64(data, zero));
} while(--count);

我省略了代码,但中间结果的折叠也在外循环中不必要地计算,而不是等待函数结束。或者更好的是对原始 COL_SIZE*ROW_SIZE 数组进行单个组合循环。


那么哪里出了问题?好吧,现代优化器是复杂的野兽,充满了启发式方法,缺乏我们只能推测的洞察力。

然而,一个简化的模型是,它们被结构化为从高级表示开始的传递,并逐渐将转换应用到较低级别的形式,这有望变得更有效率。遗憾的是,当发生诸如展开等相对较高级别的转换时,低级别的寄存器分配可能尚未发生,因此很大程度上被迫猜测合适的展开因子。

根据我的经验,模拟的宽整数运算很少受到最多的喜爱,并且经常被分流到通用回退并且与其他转换的集成很差。

此外,矢量化尤其是一个困难的过程,通常在代码属于编译器已知的模式之一时应用。

在实践中,对先前有效代码的微小改动可能会超出优化器的复杂性范围,这相当于最后一根稻草效应。编译器不会在两次传递之间无限期地来回切换直到达到最佳结果,而是出于性能原因被迫依赖猜测并最终会逃避,此时纸牌屋可能会倒塌。

因此,如果您的项目依赖于代码生成,那么您需要通过定期审查输出和测试回归来执行尽职调查,并且在关键的内部循环的情况下,通常建议明确输入更接近的内容到预期的最终结果,而不是依赖容易出错的转换。