为什么我的SIMD向量4长度函数比朴素的向量长度方法慢3倍?
SIMD向量4长度函数:
__extern_always_inline float vec4_len(const float *v) {
__m128 vec1 = _mm_load_ps(v);
__m128 xmm1 = _mm_mul_ps(vec1, vec1);
__m128 xmm2 = _mm_hadd_ps(xmm1, xmm1);
__m128 xmm3 = _mm_hadd_ps(xmm2, xmm2);
return sqrtf(_mm_cvtss_f32(xmm3));
}
天真的实现:
sqrtf(V[0] * V[0] + V[1] * V[1] + V[2] * V[2] + V[3] * V[3])
SIMD版本迭代1000000000次需要16110毫秒。相比之下,朴素版本快了约3倍,只需4746毫秒。
#include <math.h>
#include <time.h>
#include <stdint.h>
#include <stdio.h>
#include <x86intrin.h>
static float vec4_len(const float *v) {
__m128 vec1 = _mm_load_ps(v);
__m128 xmm1 = _mm_mul_ps(vec1, vec1);
__m128 xmm2 = _mm_hadd_ps(xmm1, xmm1);
__m128 xmm3 = _mm_hadd_ps(xmm2, xmm2);
return sqrtf(_mm_cvtss_f32(xmm3));
}
int main() {
float A[4] __attribute__((aligned(16))) = {3, 4, 0, 0};
struct timespec t0 = {};
clock_gettime(CLOCK_MONOTONIC, &t0);
double sum_len = 0;
for (uint64_t k = 0; k < 1000000000; ++k) {
A[3] = k;
sum_len += vec4_len(A);
// sum_len += sqrtf(A[0] * A[0] + A[1] * A[1] + A[2] * A[2] + A[3] * A[3]);
}
struct timespec t1 = {};
clock_gettime(CLOCK_MONOTONIC, &t1);
fprintf(stdout, "%f\n", sum_len);
fprintf(stdout, "%ldms\n", (((t1.tv_sec - t0.tv_sec) * 1000000000) + (t1.tv_nsec - t0.tv_nsec)) / 1000000);
return 0;
}
我在一台搭载Intel(R) Core(TM) i7-8550U处理器的计算机上,使用以下命令运行。首先是使用vec4_len
版本,然后是使用普通的C语言。
我使用GCC (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0进行编译:
Original Answer翻译成"最初的回答"
gcc -Wall -Wextra -O3 -msse -msse3 sse.c -lm && ./a.out
SSE版本输出:
499999999500000128.000000
13458ms
普通C版本输出:
最初的回答:
499999999500000128.000000
4441ms
__m128i
中,可以通过使用_mm_sqrt_ss
避免sqrtf()
的愚蠢的设置NaN时的errno行为,因此您无需使用-fno-math-errno
编译。 - undefined__attribute__((__noinline__))
,就像这个例子中的做法一样。 - undefinedhaddps
,这个指令很糟糕,尤其对于Sandybridge系列来说。它需要2个洗牌微操作和1个垂直加法微操作,而SnB系列每个时钟周期只能执行1个洗牌微操作(在端口5上)。直到IceLake在另一个端口上添加第二个洗牌单元之前,这种情况下较短的汇编代码并不总是更好的选择。 - undefined