这是对Cássio Renan优秀回答的修改。它用标准的C++替换了所有编译器特定的扩展,理论上可在任何符合规范的编译器中移植。此外,它检查参数是否正确对齐,而不是假设它们已经对齐。它优化到相同的代码。
#include <assert.h>
#include <cmath>
#include <stddef.h>
#include <stdint.h>
#define ALIGNMENT alignof(max_align_t)
using std::floor;
void testFunction(const float in[], int32_t out[], const ptrdiff_t length)
{
static_assert(sizeof(float) == sizeof(int32_t), "");
assert((uintptr_t)(void*)in % ALIGNMENT == 0);
assert((uintptr_t)(void*)out % ALIGNMENT == 0);
assert((size_t)length % (ALIGNMENT/sizeof(int32_t)) == 0);
alignas(ALIGNMENT) const float* const input = in;
alignas(ALIGNMENT) int32_t* const output = out;
for (int i = 0; i < length; ++i) {
output[i] = static_cast<int32_t>(floor(input[i]));
}
}
这段代码在GCC上优化效果不如使用非可移植扩展的原始代码。C++标准确实支持
alignas
限定符、对齐数组的引用以及返回缓冲区内对齐范围的
std::align
函数。然而,在我测试过的所有编译器中,这些都不能使编译器生成对齐而不是未对齐的向量加载和存储。
尽管在x86_64上
alignof(max_align_t)
仅为16,并且可以将
ALIGNMENT
定义为常量64,但这并不能帮助任何编译器生成更好的代码,因此我选择了可移植性。最接近强制编译器假定指针对齐的可移植方式是使用
<immintrin.h>
中的类型,这是大多数x86编译器支持的,或者定义一个带有
alignas
说明符的
struct
。通过检查预定义宏,您还可以在Linux编译器上将宏扩展为
__attribute__ ((aligned (ALIGNMENT)))
,或在Windows编译器上扩展为
__declspec (align (ALIGNMENT))
,并在我们不知道的编译器上使用一些安全的东西,但GCC需要在
type上使用属性来实际生成对齐的加载和存储。
此外,原始示例调用了一个内置函数,告诉GCC不可能出现
length
不是32的倍数。如果您使用
assert()
或调用标准函数(如
abort()
),GCC、Clang和ICC都不会做出相同的推断。因此,它们生成的大多数代码将处理
length
不是向量宽度的整数倍的情况。
这样做的一个可能原因是,优化并不能带来太多速度提升:在Intel CPU上,具有对齐地址的非对齐内存指令速度很快,并且处理
length
不是整数倍的情况的代码只需几个字节的长度,在常数时间内运行。
值得一提的是,GCC能够更好地优化
<cmath>
中的内联函数,而不是
<math.c>
中实现的宏。
GCC 9.1需要一组特定的选项来生成AVX512代码。即使使用"-march=cannonlake",默认情况下它也会倾向于256位向量。它需要"-mprefer-vector-width=512"来生成512位代码。(感谢Peter Cordes指出这一点)。它在向量化循环后跟随展开代码,以转换数组中任何剩余的元素。
这是向量化主循环,减去了一些常量时间初始化、错误检查和清理代码,这些代码只会运行一次:
.L7:
vrndscaleps zmm0, ZMMWORD PTR [rdi+rax], 1
vcvttps2dq zmm0, zmm0
vmovdqu32 ZMMWORD PTR [rsi+rax], zmm0
add rax, 64
cmp rax, rcx
jne .L7
机警的读者会注意到Cássio Renan程序生成的代码与此处有两个不同之处:它使用%zmm而不是%ymm寄存器,并且使用不对齐的
vmovdqu32
而不是对齐的
vmovdqa64
来存储结果。
使用相同的标志,Clang 8.0.0在展开循环方面做出了不同选择。每次迭代操作八个512位向量(即128个单精度浮点数),但获取剩余部分的代码没有被展开。如果至少还有64个浮点数,它将使用另外四个AVX512指令处理这些,然后通过非矢量化循环清理任何额外的余数。
如果您在Clang++中编译原始程序,它将不会抱怨并接受它,但不会进行相同的优化:它仍然不会假设
length
是向量宽度的倍数,也不会假设指针是对齐的。
它更喜欢AVX512代码而不是AVX256,即使没有使用
-mprefer-vector-width=512
。
test rdx, rdx
jle .LBB0_14
cmp rdx, 63
ja .LBB0_6
xor eax, eax
jmp .LBB0_13
.LBB0_6:
mov rax, rdx
and rax, -64
lea r9, [rax - 64]
mov r10, r9
shr r10, 6
add r10, 1
mov r8d, r10d
and r8d, 1
test r9, r9
je .LBB0_7
mov ecx, 1
sub rcx, r10
lea r9, [r8 + rcx]
add r9, -1
xor ecx, ecx
.LBB0_9: # =>This Inner Loop Header: Depth=1
vrndscaleps zmm0, zmmword ptr [rdi + 4*rcx], 9
vrndscaleps zmm1, zmmword ptr [rdi + 4*rcx + 64], 9
vrndscaleps zmm2, zmmword ptr [rdi + 4*rcx + 128], 9
vrndscaleps zmm3, zmmword ptr [rdi + 4*rcx + 192], 9
vcvttps2dq zmm0, zmm0
vcvttps2dq zmm1, zmm1
vcvttps2dq zmm2, zmm2
vmovups zmmword ptr [rsi + 4*rcx], zmm0
vmovups zmmword ptr [rsi + 4*rcx + 64], zmm1
vmovups zmmword ptr [rsi + 4*rcx + 128], zmm2
vcvttps2dq zmm0, zmm3
vmovups zmmword ptr [rsi + 4*rcx + 192], zmm0
vrndscaleps zmm0, zmmword ptr [rdi + 4*rcx + 256], 9
vrndscaleps zmm1, zmmword ptr [rdi + 4*rcx + 320], 9
vrndscaleps zmm2, zmmword ptr [rdi + 4*rcx + 384], 9
vrndscaleps zmm3, zmmword ptr [rdi + 4*rcx + 448], 9
vcvttps2dq zmm0, zmm0
vcvttps2dq zmm1, zmm1
vcvttps2dq zmm2, zmm2
vcvttps2dq zmm3, zmm3
vmovups zmmword ptr [rsi + 4*rcx + 256], zmm0
vmovups zmmword ptr [rsi + 4*rcx + 320], zmm1
vmovups zmmword ptr [rsi + 4*rcx + 384], zmm2
vmovups zmmword ptr [rsi + 4*rcx + 448], zmm3
sub rcx, -128
add r9, 2
jne .LBB0_9
test r8, r8
je .LBB0_12
.LBB0_11:
vrndscaleps zmm0, zmmword ptr [rdi + 4*rcx], 9
vrndscaleps zmm1, zmmword ptr [rdi + 4*rcx + 64], 9
vrndscaleps zmm2, zmmword ptr [rdi + 4*rcx + 128], 9
vrndscaleps zmm3, zmmword ptr [rdi + 4*rcx + 192], 9
vcvttps2dq zmm0, zmm0
vcvttps2dq zmm1, zmm1
vcvttps2dq zmm2, zmm2
vcvttps2dq zmm3, zmm3
vmovups zmmword ptr [rsi + 4*rcx], zmm0
vmovups zmmword ptr [rsi + 4*rcx + 64], zmm1
vmovups zmmword ptr [rsi + 4*rcx + 128], zmm2
vmovups zmmword ptr [rsi + 4*rcx + 192], zmm3
.LBB0_12:
cmp rax, rdx
je .LBB0_14
.LBB0_13: # =>This Inner Loop Header: Depth=1
vmovss xmm0, dword ptr [rdi + 4*rax] # xmm0 = mem[0],zero,zero,zero
vroundss xmm0, xmm0, xmm0, 9
vcvttss2si ecx, xmm0
mov dword ptr [rsi + 4*rax], ecx
add rax, 1
cmp rdx, rax
jne .LBB0_13
.LBB0_14:
pop rax
vzeroupper
ret
.LBB0_7:
xor ecx, ecx
test r8, r8
jne .LBB0_11
jmp .LBB0_12
ICC 19同样生成AVX512指令,但与clang
非常不同。它使用了更多的魔法常数进行设置,但不会展开任何循环,而是操作512位向量。
此代码还适用于其他编译器和架构。(尽管MSVC仅支持到AVX2 ISA,并且无法自动向量化循环。)例如,在带有-march=armv8-a+simd
的ARM上,它将生成一个矢量化循环,其中包括frintm v0.4s,v0.4s
和fcvtzs v0.4s,v0.4s
。
请自行尝试。