在这种特殊情况下,为什么数据类型会影响性能?
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_rows
和 sum_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_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
数组进行单个组合循环。
那么哪里出了问题?好吧,现代优化器是复杂的野兽,充满了启发式方法,缺乏我们只能推测的洞察力。
然而,一个简化的模型是,它们被结构化为从高级表示开始的传递,并逐渐将转换应用到较低级别的形式,这有望变得更有效率。遗憾的是,当发生诸如展开等相对较高级别的转换时,低级别的寄存器分配可能尚未发生,因此很大程度上被迫猜测合适的展开因子。
根据我的经验,模拟的宽整数运算很少受到最多的喜爱,并且经常被分流到通用回退并且与其他转换的集成很差。
此外,矢量化尤其是一个困难的过程,通常在代码属于编译器已知的模式之一时应用。
在实践中,对先前有效代码的微小改动可能会超出优化器的复杂性范围,这相当于最后一根稻草效应。编译器不会在两次传递之间无限期地来回切换直到达到最佳结果,而是出于性能原因被迫依赖猜测并最终会逃避,此时纸牌屋可能会倒塌。
因此,如果您的项目依赖于代码生成,那么您需要通过定期审查输出和测试回归来执行尽职调查,并且在关键的内部循环的情况下,通常建议明确输入更接近的内容到预期的最终结果,而不是依赖容易出错的转换。
我编写了以下代码来对缓存未命中对性能的影响进行基准测试:
#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
,它们接受一个数组并添加元素,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_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
数组进行单个组合循环。
那么哪里出了问题?好吧,现代优化器是复杂的野兽,充满了启发式方法,缺乏我们只能推测的洞察力。
然而,一个简化的模型是,它们被结构化为从高级表示开始的传递,并逐渐将转换应用到较低级别的形式,这有望变得更有效率。遗憾的是,当发生诸如展开等相对较高级别的转换时,低级别的寄存器分配可能尚未发生,因此很大程度上被迫猜测合适的展开因子。
根据我的经验,模拟的宽整数运算很少受到最多的喜爱,并且经常被分流到通用回退并且与其他转换的集成很差。
此外,矢量化尤其是一个困难的过程,通常在代码属于编译器已知的模式之一时应用。
在实践中,对先前有效代码的微小改动可能会超出优化器的复杂性范围,这相当于最后一根稻草效应。编译器不会在两次传递之间无限期地来回切换直到达到最佳结果,而是出于性能原因被迫依赖猜测并最终会逃避,此时纸牌屋可能会倒塌。
因此,如果您的项目依赖于代码生成,那么您需要通过定期审查输出和测试回归来执行尽职调查,并且在关键的内部循环的情况下,通常建议明确输入更接近的内容到预期的最终结果,而不是依赖容易出错的转换。