assembly - 有符号或无符号循环计数器
问题描述
在这个简单的例子中,我对使用有符号和无符号循环计数器的区别感到非常惊讶:
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 19.0.0(我展示了循环的展开部分):
..B1.2:
vaddpd zmm7, zmm7, ZMMWORD PTR [rdi+rax*8]
vaddpd zmm6, zmm6, ZMMWORD PTR [64+rdi+rax*8]
vaddpd zmm5, zmm5, ZMMWORD PTR [128+rdi+rax*8]
vaddpd zmm4, zmm4, ZMMWORD PTR [192+rdi+rax*8]
vaddpd zmm3, zmm3, ZMMWORD PTR [256+rdi+rax*8]
vaddpd zmm2, zmm2, ZMMWORD PTR [320+rdi+rax*8]
vaddpd zmm1, zmm1, ZMMWORD PTR [384+rdi+rax*8]
vaddpd zmm0, zmm0, ZMMWORD PTR [448+rdi+rax*8]
add rax, 64
cmp rax, 1048576
jb ..B1.2 # Prob 99%
在无符号情况下,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-usage=high
解决方案
这是 ICC 一个愚蠢的错过优化。它不是 AVX512 特有的;默认/通用架构设置仍然会发生这种情况。
lea ecx, DWORD PTR [16+rax]
作为展开的一部分进行计算i+16
,截断为 32 位(32 位操作数大小),零扩展为 64 位(写入 32 位寄存器时在 x86-64 中隐含)。这在类型宽度上显式地实现了无符号环绕的语义。
gcc 和 clang 证明unsigned i
不会换行没有问题,因此它们可以优化从 32 位无符号到 64 位指针宽度的零扩展,以便在寻址模式下使用,因为循环上限已知1。
回想一下,无符号环绕在 C 和 C++ 中是明确定义的,但有符号溢出是未定义的行为。这意味着可以将带符号的变量提升为指针宽度,并且编译器不必在每次将它们用作数组索引时都将符号扩展重做为指针宽度。(a[i]
等效于*(a+i)
,并且将整数添加到指针的规则意味着对于寄存器的高位可能不匹配的窄值,符号扩展是必要的。)
签名溢出 UB 是 ICC 能够正确优化签名计数器的原因,即使它无法使用范围信息。另请参阅http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html(关于未定义的行为)。add rax, 64
请注意,它使用cmp
64 位操作数大小(RAX 而不是 EAX)
我将您的代码制作成 MCVE 以使用其他编译器进行测试。 __assume_aligned
仅限 ICC,所以我使用了 GNU C __builtin_assume_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;
}
clang 像这样编译你的函数(Godbolt 编译器资源管理器):
# 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
我没有启用 AVX,这不会改变循环结构。请注意,clang 仅使用 2 个向量累加器,因此如果 L1d 缓存中的数据很热,它将成为 FP 的瓶颈,增加最新 CPU 的延迟。Skylake 一次最多可以保持 8addpd
个在运行中(每个时钟吞吐量 2 个,具有 4 个周期延迟)。因此,对于(某些)数据在 L2 或特别是 L1d 缓存中很热的情况,ICC 做得更好。
奇怪的是,clang 没有使用指针增量,如果它无论如何都要添加/cmp。在循环之前只需要几条额外的指令,并且会简化寻址模式,即使在 Sandybridge 上也可以实现负载的微融合。(但它不是 AVX,因此 Haswell 及以后可以保持负载微融合。 微融合和寻址模式)。GCC 会这样做,但根本不会展开,这是 GCC 的默认设置,没有配置文件引导的优化。
无论如何,ICC 的 AVX512 代码将解压成单独的负载并在问题/重命名阶段添加 uops(或者在添加到 IDQ 之前,我不确定)。所以它不使用指针增量来节省前端带宽,为更大的乱序窗口消耗更少的 ROB 空间,并且对超线程更友好,这是非常愚蠢的。
脚注1:
(即使不是,没有像 avolatile
或atomic
访问这样的副作用的无限循环是未定义的行为,所以即使使用i <= n
runtime-variable n
,编译器也可以假设循环不是无限的,因此i
没有' t 换行。 是 while(1); C 中未定义的行为吗? )
在实践中 gcc 和 clang 并没有利用这一点,而是创建一个实际上可能是无限的循环,并且不要因为可能的怪异而自动矢量化。所以避免i <= n
使用运行时变量n
,特别是对于无符号比较。改为使用i < n
。
如果展开,i += 2
可以产生类似的效果。
所以在源代码中做结束指针和指针增量通常是好的,因为这对于 asm 来说通常是最佳的。
推荐阅读
- graphql - 在拼接模式中合并 GraphQL 类型及其参数
- azure - 选择标准负载均衡器
- azure-blob-storage - 保存 Azure blob 连接器的逻辑应用标准问题
- javascript - API JSON 数据导入 Google Sheets (JS)
- php - php isset() 无法与 filter_var() 一起正常工作
- android - 在弹出窗口中定位滚动视图
- javascript - 如何获取位于其他对象内部的对象的值
- assembly - Qualcomm Hexagon:矢量内存负载内在?
- python - ModuleNotFoundError:没有名为“dash.html”的模块
- java - 如何将 HashMap 转换为具有特定格式的字符串?(JAVA)