为什么我的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])
#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;
}
我用GCC(Ubuntu7.4.0-1Ubuntu1~18.04.1)7.4.0:
gcc -Wall -Wextra -O3 -msse -msse3 sse.c -lm && ./a.out
SSE版本输出:
499999999500000128.000000
13458ms
纯C版本输出:
499999999500000128.000000
4441ms
最明显的问题是使用效率低下的点积(使用haddps
,其成本为2倍shuffle uops+1x add uop),而不是shuffle+add。关于在_mm_mul_ps
之后要做什么,请参见在x86上做水平浮点向量和的最快方法,这并不太费劲。但这仍然不是x86能够非常有效地完成的事情。
但无论如何,真正的问题是您的基准循环。
A[3]=K;
然后使用_mm_load_ps(A)
创建一个存储转发延迟,如果它是简单编译而不是向量洗牌的话。如果加载只从单个存储指令加载数据,而没有其他数据,则存储+重新加载可以以大约5个延迟周期有效地转发。否则,它必须对整个存储缓冲区进行较慢的扫描来组装字节。这会给存储转发增加大约10个延迟周期。
参见Agner Fog的microarch指南和/或优化手册。
另外,通过让编译器将v[0]*v[0]+v[1]*v[1]+v[2]*v[2]
的计算从循环中移出,您对SSE的影响更大。
GCC9.1来自Kamil的Godbolt链接的内部循环看起来很糟糕,似乎包括了一个循环携带的存储/重新加载,以将新的a[3]
合并到8字节的a[2..3]
对中,进一步限制了CPU重叠多个迭代的能力。
我不知道为什么海合会认为这是个好主意。对于将向量加载分成8字节的CPU(如Pentium M或Bobcat)可能会有所帮助,以避免存储转发停滞。但对于“通用”的现代x86-64 CPU来说,这不是一个合理的调优。
.L18:
pxor xmm4, xmm4
mov rdx, QWORD PTR [rsp+8] ; reload A[2..3]
cvtsi2ss xmm4, rbx
mov edx, edx ; truncate RDX to 32-bit
movd eax, xmm4 ; float bit-pattern of (float)k
sal rax, 32
or rdx, rax ; merge the float bit-pattern into A[3]
mov QWORD PTR [rsp+8], rdx ; store A[2..3] again
movaps xmm0, XMMWORD PTR [rsp] ; vector load: store-forwarding stall
mulps xmm0, xmm0
haddps xmm0, xmm0
haddps xmm0, xmm0
ucomiss xmm3, xmm0
movaps xmm1, xmm0
sqrtss xmm1, xmm1
ja .L21 ; call sqrtf to set errno if needed; flags set by ucomiss.
.L17:
add rbx, 1
cvtss2sd xmm1, xmm1
addsd xmm2, xmm1 ; total += (double)sqrtf
cmp rbx, 1000000000
jne .L18 ; }while(k<1000000000);
这种疯狂在标量版本中是不存在的。
不管怎样,gcc都避免了完整的UINT64_T
->float
转换的低效(在AVX512之前,x86在硬件中没有这种转换)。它可以证明使用有符号的64位->浮点转换总是有效的,因为不能设置高位。
脚注1:但是sqrtps
与scalar具有相同的每3个循环1的吞吐量,因此通过一次水平执行1个向量,而不是并行执行4个向量的4个长度,您只能获得CPU sqrt吞吐量能力的1/4。
我最近用Java写了一个计算密集型算法,然后把它翻译成C++。令我吃惊的是,C++的执行速度要慢得多。我现在已经编写了一个更短的Java测试程序,以及一个相应的C++程序-参见下面。我的原始代码具有大量的数组访问功能,测试代码也是如此。C++的执行时间要长5.5倍(请参阅每个程序末尾的注释)。 以下1st21条评论后的结论... null null Java代码: C++代码:
问题内容: 以下是C 中的一个简单循环。计时器正在使用QueryPerformanceCounter(),并且非常准确。我发现Java占用了C 60%的时间,这不是吗?我在这里做错了什么?即使是严格的别名(此处未包含在代码中)也完全没有帮助… 此C ++的运行时间约为9.5秒。我正在将Intel Compiler 12.1用于主机处理器优化(专门针对我的处理器),并将所有功能都最大化。所以这是最好
问题内容: 我想迭代地构建稀疏矩阵,并注意到根据SciPy文档,有两种合适的选择: LiL矩阵: 类scipy.sparse.lil_matrix(arg1,shape = None,dtype = None,copy = False)[源]基于行的链表稀疏矩阵 这是用于增量构造稀疏矩阵的有效结构。 DoK矩阵: 类scipy.sparse.dok_matrix(arg1,shape = None
问题内容: 考虑下面的go代码: : 我不明白的是,为什么taste_fruits的容量为3,直觉上我希望为2,因为这是切片的长度? 而且,如果tasty_fruits的容量为3,那么为什么: 造成: 问题答案: 这行: 创建一个 数组 ,而不是一个切片。即使您仅提供了3个元素,它也有4个元素。输出: 切片: 结果是: 长度:明显2.容量? 的 容量 是…的片的长度和超过所述切片中的[基本]阵列的
我一直在研究一个深度学习库,自己写作。在矩阵运算中,获得最佳性能对我来说是一个关键。我一直在研究编程语言及其对数字运算的性能。过了一段时间,我发现C#SIMD具有与C++SIMD非常相似的性能。所以,我决定用C#编写这个库。 首先,我测试了C#SIMD(我测试了很多东西,但是这里不写了)。我注意到,当使用较小的数组时,它的工作效果要好得多。当使用较大的数组时,效率不高。我觉得很可笑。通常情况下,当
本文向大家介绍唯一索引比普通索引快吗, 为什么?相关面试题,主要包含被问及唯一索引比普通索引快吗, 为什么?时的应答技巧和注意事项,需要的朋友参考一下 唯一索引不一定比普通索引快, 还可能慢. 查询时, 在未使用limit 1的情况下, 在匹配到一条数据后, 唯一索引即返回, 普通索引会继续匹配下一条数据, 发现不匹配后返回. 如此看来唯一索引少了一次匹配, 但实际上这个消耗微乎其微. 更新时,