我已经编写了以下代码,用于对缓存未命中对性能的影响进行基准测试:
#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_rows
和sum_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
答案 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_lo
和count
分流到内存中,因此展开是一种悲观。展开因子为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
数组上执行单个组合循环。
那么 在这里出错了什么?嗯,现代优化器是复杂的动物,充满了启发式,缺乏洞察力,我们大多只能推测。
然而,一个简化的模型是它们被构造成从高级表示开始的传递,并逐渐将变换应用到较低级别的形式,希望结果更高效。令人遗憾的是,当发生诸如展开之类的相对高级别的转换时,可能还没有发生低级别的寄存器分配,因此很大程度上它被迫猜测一个合适的展开因子。
根据我的经验,模拟的宽整数算法很少得到最多的爱,并且经常被分流到通用回退并且与其他变换很难集成。
此外,矢量化尤其是一个困难的过程,通常在代码落入编译器已知的模式之一时应用。
在实践中,相当于最后一根稻草效应的后果,对先前有效代码的微小改变可能会超出优化器的复杂性范围。在通过之间无限期地往返,直到达到最佳结果为止,编译器不得不依赖于猜测而出于性能原因而最终会出现问题,此时卡片房屋可能会崩溃。
因此,如果您的项目依赖于代码生成,那么您可以通过定期查看输出和测试回归来执行尽职调查,并且在关键内部循环的情况下,通常建议明确键入更接近于预期的最终结果,而不是依赖于易犯错误的转变。