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

时间:2017-08-14 18:41:01

标签: c++ optimization sse simd cpu-cache

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

#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,它们接受一个数组并添加元素,并返回总和。不同之处在于我们访问元素的顺序。两个函数的返回类型始终为uint64_t

我在名为SAME_TYPES的文件顶部附近有一个定义。如果SAME_TYPES,则返回类型和元素类型均为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快。但是,当类型不同时,为什么我看到col-row中的row-col和增加减少了。我知道如果元素的数据类型较大,那么我可以更少地放入我的缓存中,但是为什么我在迭代外部循环中的列时看到性能的提高。有人可以向我解释一下吗?

如果重要,我使用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  

1 个答案:

答案 0 :(得分:4)

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

首先回顾一下使用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数组上执行单个组合循环。

那么 在这里出错了什么?嗯,现代优化器是复杂的动物,充满了启发式,缺乏洞察力,我们大多只能推测。

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

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

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

在实践中,相当于最后一根稻草效应的后果,对先前有效代码的微小改变可能会超出优化器的复杂性范围。在通过之间无限期地往返,直到达到最佳结果为止,编译器不得不依赖于猜测而出于性能原因而最终会出现问题,此时卡片房屋可能会崩溃。

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