g++ -O3 为 loop 创建了奇怪的指令 - 两个具有相同 asm 的版本
g++ -O3 creates strange instructions for loop - two versions with the same asm
我正在使用c ++编写一些用于数值计算的代码。我需要非常仔细地编写代码,以帮助编译器生成良好的指令。然后,我发现带有 -O3 标志的 g++ 9.2 有些奇怪。我不是组装专家,所以我需要有人帮助我或指出我错在哪里。
完整的代码可以在这里找到 https://godbolt.org/z/fyuYtq .我在此处复制并粘贴关键片段
void sum_twopointer(Elem *p1, Elem *p2, ptrdiff_t stride, ptrdiff_t start, ptrdiff_t end) {
Elem sm = 0;
for(auto i = start;i != end; ++i) {
p1[0] = p2[0] + p2[0];
p1 += stride;
p2 += stride;
}
}
它是用g++ -O3
编译的。g++ 的版本是 9.2。汇编代码是
sum_twopointer(double*, double*, long, long, long):
cmp rcx, r8
je .L32
lea r9, [0+rdx*8]
xor eax, eax
cmp rdx, 1
jne .L36
.L34:
movsd xmm0, QWORD PTR [rsi+rax]
add rcx, 1
addsd xmm0, xmm0
movsd QWORD PTR [rdi+rax], xmm0
add rax, r9
cmp r8, rcx
jne .L34
.L32:
ret
.L36:
movsd xmm0, QWORD PTR [rsi+rax]
add rcx, 1
addsd xmm0, xmm0
movsd QWORD PTR [rdi+rax], xmm0
add rax, r9
cmp r8, rcx
jne .L36
ret
据我了解,编译器正在尝试对步幅仅为 1 的特殊情况进行一些优化,因此它为 stride==1 的情况创建了一个新分支,但它没有做任何进一步的事情。请注意,.L34 与以下那些相同。L36.
我为此做了一些基准测试。步幅=1 和步幅=2 的性能如下。 代码在那里 https://gist.github.com/lhprojects/dac3a9fcf15bd5b1ec365ba6a87c679d
g++ -O2
---------------------------------------------------------------
Benchmark Time CPU Iterations
---------------------------------------------------------------
BM_twopointer/8192/1 3743 ns 3742 ns 185062 stride=1
BM_twopointer/8192/2 1980 ns 1980 ns 328523 stride=2
g++ -O3
---------------------------------------------------------------
Benchmark Time CPU Iterations
---------------------------------------------------------------
BM_twopointer/8192/1 5006 ns 5001 ns 120725 stride=1
BM_twopointer/8192/2 2043 ns 2041 ns 333914 stride=2
无论如何,对于步幅=1,与-O2相比,-O3的性能会变差。我想知道我的代码发生了什么。我是否在 c++ 中触发了一些未定义的行为?或者简单地说,g++ 中的代码优化存在缺陷。(如果我的英文写作让你感到非常困惑,我很抱歉。
我相信编译器需要知道 p1 和 p2 不重叠......将它们声明为指针__restrict应该允许编译器实际使用 SIMD 指令。 对我来说,它会为 stride==1 创建一个特殊情况,但随后不会对该知识做任何事情,这对我来说确实很奇怪。
据我了解,编译器正在尝试对步幅仅为 1 的特殊情况进行一些优化,因此它为 stride==1 的情况创建了一个新分支,但它没有做任何进一步的事情。
重新测试表明该问题自GCC 13.1以来已修复。因此,这可能是以前的 GCC 版本中的错误或至少是技术限制。
使用以下代码作为测试:
#include <stddef.h>
#include <stdint.h>
typedef double Elem;
void sum_twopointer(Elem * __restrict p1, Elem *__restrict p2, ptrdiff_t stride, ptrdiff_t start, ptrdiff_t end) {
Elem sm = 0;
for(auto i = start;i < end; ++i) {
p1[0] = p2[0] + p2[0];
p1 += stride;
p2 += stride;
}
}
在 GCC 12.3 中,-fversion-loops-for-strides
生成以下代码。GCC 尝试检测阵列是否具有 stride-1 访问并生成单独的循环,但此"快速路径"循环与回退循环相同,并且不进行任何优化。
SSE 和 AVX(使用-march=native
启用(都是这种情况,即使在-Ofast
下也是如此。
sum_twopointer(double*, double*, long, long, long):
cmp rcx, r8
jge .L1
lea r9, [0+rdx*8]
xor eax, eax
cmp rdx, 1
jne .L4
.L3:
movsd xmm0, QWORD PTR [rsi+rax]
add rcx, 1
addsd xmm0, xmm0
movsd QWORD PTR [rdi+rax], xmm0
add rax, r9
cmp r8, rcx
jne .L3
.L1:
ret
.L4:
movsd xmm0, QWORD PTR [rsi+rax]
add rcx, 1
addsd xmm0, xmm0
movsd QWORD PTR [rdi+rax], xmm0
add rax, r9
cmp r8, rcx
jne .L4
ret
但是,在 GCC 13.1 中,stride-1 循环现在使用硬编码的偏移8
,如下所示:
sum_twopointer(double*, double*, long, long, long):
cmp rcx, r8
jge .L1
xor eax, eax
cmp rdx, 1
jne .L12
.L3:
movsd xmm0, QWORD PTR [rsi+rax]
add rcx, 1
addsd xmm0, xmm0
movsd QWORD PTR [rdi+rax], xmm0
add rax, 8
cmp r8, rcx
jne .L3
.L1:
ret
.L12:
sal rdx, 3
.L4:
movsd xmm0, QWORD PTR [rsi+rax]
add rcx, 1
addsd xmm0, xmm0
movsd QWORD PTR [rdi+rax], xmm0
add rax, rdx
cmp r8, rcx
jne .L4
ret
- 为什么这两个版本的代码给出不同的输出
- 如何在两个版本之间从perforce差异获取C/C 函数名称
- 我真的需要两个版本的==操作员超载吗?
- 这两个版本的代码有什么区别
- 同一个应用程序中的同一库的两个版本
- 基于布尔标志有效地派生公共函数的两个版本
- 特征 'sum()' 函数在两个版本的程序中给出不同的相同向量
- c++ 编译器优化是否可以针对布尔参数生成两个版本的函数
- 为什么这两个版本的快速排序在进行比较的次数上存在巨大差异
- 调试:跟踪(和diffing)同一程序的两个版本的函数调用树
- C++堆栈实现 - 两个版本的 top 方法
- 如何静态链接到两个版本的 xerces-c(或任何与此相关的库)
- C++检查两个版本的函数
- 为什么同一服务器的两个版本的行为不同?
- 两个版本的[]操作符用于右值和左值情况
- 用于比较给定两个源代码文件的同一c++函数的两个版本的工具
- c++两个版本的重载下标操作符
- 当我包含一个共享库时,我的可执行文件正在寻找它的两个版本
- 为什么矢量模板函数赋值有两个版本可以区分
- 避免使用"const X&"和"X&"的两个版本的函数