Assembly 有符号或无符号循环计数器
在这个简单的示例中,使用有符号和无符号循环计数器之间的差异让我非常惊讶:Assembly 有符号或无符号循环计数器,assembly,optimization,compiler-optimization,icc,unsigned-integer,Assembly,Optimization,Compiler Optimization,Icc,Unsigned Integer,在这个简单的示例中,使用有符号和无符号循环计数器之间的差异让我非常惊讶: double const* a; __assume_aligned(a, 64); double s = 0.0; //for ( unsigned int i = 0; i < 1024*1024; i++ ) for ( int i = 0; i < 1024*1024; i++ ) { s += a[i]; } 在无符号情况下,icc使用额外的寄存器来寻址内存,相应的LEAs: ..B1.2:
double const* a;
__assume_aligned(a, 64);
double s = 0.0;
//for ( unsigned int i = 0; i < 1024*1024; i++ )
for ( int i = 0; i < 1024*1024; i++ )
{
s += a[i];
}
在无符号情况下,icc使用额外的寄存器来寻址内存,相应的LEA
s:
..B1.2:
lea edx, DWORD PTR [8+rax]
vaddpd zmm6, zmm6, ZMMWORD PTR [rdi+rdx*8]
lea ecx, DWORD PTR [16+rax]
vaddpd zmm5, zmm5, ZMMWORD PTR [rdi+rcx*8]
vaddpd zmm7, zmm7, ZMMWORD PTR [rdi+rax*8]
lea esi, DWORD PTR [24+rax]
vaddpd zmm4, zmm4, ZMMWORD PTR [rdi+rsi*8]
lea r8d, DWORD PTR [32+rax]
vaddpd zmm3, zmm3, ZMMWORD PTR [rdi+r8*8]
lea r9d, DWORD PTR [40+rax]
vaddpd zmm2, zmm2, ZMMWORD PTR [rdi+r9*8]
lea r10d, DWORD PTR [48+rax]
vaddpd zmm1, zmm1, ZMMWORD PTR [rdi+r10*8]
lea r11d, DWORD PTR [56+rax]
add eax, 64
vaddpd zmm0, zmm0, ZMMWORD PTR [rdi+r11*8]
cmp eax, 1048576
jb ..B1.2 # Prob 99%
对我来说,令人惊讶的是,它没有生成相同的代码(给定编译时循环计数)。这是一个编译器优化问题吗
编译选项:
-O3-march=skylake-avx512-mtune=skylake-avx512-qopt-zmm使用率=高
这是ICC愚蠢的遗漏优化。它不是特定于AVX512的;默认/常规拱门设置仍会发生这种情况
lea ecx,DWORD PTR[16+rax]
正在计算i+16
作为展开的一部分,截断为32位(32位操作数大小),零扩展为64位(写入32位寄存器时在x86-64中隐式)。这在类型宽度处显式实现了无符号环绕的语义
gcc和clang在证明无符号i
不会换行方面没有问题,因此他们可以优化零扩展,从32位无符号到64位指针宽度,以便在寻址模式中使用,因为循环上限是已知的1
记得在C和C++中,未签名的环绕是很好定义的,但是签名溢出是未定义的行为。这意味着有符号变量可以提升为指针宽度,并且编译器不必每次将它们用作数组索引时都将符号扩展重做为指针宽度。(
a[i]
相当于*(a+i)
,向指针添加整数的规则意味着,对于寄存器高位可能不匹配的窄值,符号扩展是必要的。)
签名溢出UB是ICC能够正确优化签名计数器的原因,即使它无法使用范围信息。另请参见(关于未定义的行为)。请注意,它使用的是64位操作数大小的addrax、64
和cmp
(rax而不是EAX)
我将您的代码制作成MCVE,以便与其他编译器进行测试
\uuuuu-assemption\uu-aligned
仅为ICC,因此我使用了GNU C\uuuuuuuuu-builtin\uu-assemption\uu-aligned
#define COUNTER_TYPE unsigned
double sum(const double *a) {
a = __builtin_assume_aligned(a, 64);
double s = 0.0;
for ( COUNTER_TYPE i = 0; i < 1024*1024; i++ )
s += a[i];
return s;
}
我没有启用AVX,这不会改变循环结构。请注意,clang只使用2个向量累加器,所以如果L1d缓存中的数据是热的,它将在最新的CPU上增加延迟。Skylake一次最多可保持8个addpd
(2个时钟吞吐量,4个周期延迟)。因此,对于二级缓存(特别是L1d缓存)中的(部分)数据很热的情况,ICC的表现要好得多
奇怪的是,如果clang打算添加/cmp,那么它没有使用指针增量。它只需要在循环之前执行两条额外的指令,并且可以简化寻址模式,即使在Sandybridge上也可以实现负载的微融合。(但它不是AVX,因此Haswell和更高版本可以保持负载微熔合。)。GCC做到了这一点,但根本不展开,这是GCC在没有配置文件引导优化的情况下的默认设置
无论如何,ICC的AVX512代码将在发布/重命名阶段(或者在添加到IDQ之前,我不确定)将分层为单独的加载并添加UOP。因此,它不使用指针增量来节省前端带宽、为更大的无序窗口消耗更少的ROB空间以及更友好的阅读,这是非常愚蠢的
脚注1:
(即使不是,没有副作用的无限循环(如
volatile
或原子访问)是未定义的行为,因此即使使用i,编译器似乎也无法推断截断到32位实际上是不必要的。它如何与size\u t
一起工作?这是一个遗漏的优化,因为它可以证明那i
不会换行,但会失败。但是签名溢出是UB,它可以将32位循环计数器提升到64位,而不必像每次在这里重复零扩展一样,每次都重复符号扩展。不是每次,而是每8次。@zch你是对的,size\u t
生成与签名int相同的代码
#define COUNTER_TYPE unsigned
double sum(const double *a) {
a = __builtin_assume_aligned(a, 64);
double s = 0.0;
for ( COUNTER_TYPE i = 0; i < 1024*1024; i++ )
s += a[i];
return s;
}
# clang 7.0 -O3
sum: # @sum
xorpd xmm0, xmm0
xor eax, eax
xorpd xmm1, xmm1
.LBB0_1: # =>This Inner Loop Header: Depth=1
addpd xmm0, xmmword ptr [rdi + 8*rax]
addpd xmm1, xmmword ptr [rdi + 8*rax + 16]
addpd xmm0, xmmword ptr [rdi + 8*rax + 32]
addpd xmm1, xmmword ptr [rdi + 8*rax + 48]
addpd xmm0, xmmword ptr [rdi + 8*rax + 64]
addpd xmm1, xmmword ptr [rdi + 8*rax + 80]
addpd xmm0, xmmword ptr [rdi + 8*rax + 96]
addpd xmm1, xmmword ptr [rdi + 8*rax + 112]
add rax, 16 # 64-bit loop counter
cmp rax, 1048576
jne .LBB0_1
addpd xmm1, xmm0
movapd xmm0, xmm1 # horizontal sum
movhlps xmm0, xmm1 # xmm0 = xmm1[1],xmm0[1]
addpd xmm0, xmm1
ret