对于具有所有相同组件的 SSE 向量,是动态生成还是预先计算?
For for an SSE vector that has all the same components, generate on the fly or precompute?
当我需要做一个向量运算,它的操作数只是一个广播到每个组件的浮点数时,我是否应该预先计算 __m256
或 __m128
,并在需要时加载它,或者每次我需要向量时使用 _mm_set1_ps
将浮点数广播到寄存器?
我一直在预先计算非常重要且使用频繁的向量,并即时生成不太重要的向量。但是我真的通过预计算获得了任何速度吗?值得这么麻烦吗?
_mm_set1_ps
是用一条指令实现的吗?这可能会回答我的问题。
我认为通常最好从代码(例如循环)中分离出 SSE 向量,并在需要时使用它,假设你注意不要意外地强制它进入记忆。 (例如,如果你获取它的地址或通过引用将它传递给另一个函数,那么它可能会被强制进入内存并且你可能会得到奇怪的行为。)
这个想法是 通常 最好避免将值传入和传出 SSE 寄存器,如果碰巧在您的特定情况下不是这种情况,编译器已经知道如何该值是构造出来的,通常可以rematerialize it if need be. I think this is a lot easier than loop-invariant code motion,这是反向优化(即编译器为您分解出来的地方)并且需要编译器证明代码确实是循环不变的。
当然这在很大程度上取决于您的代码,但我已经使用这两种方法实现了两个简单的功能。 See code
__m128 calc_set1(float num1, float num2)
{
__m128 num1_4 = _mm_set1_ps(num1);
__m128 num2_4 = _mm_set1_ps(num2);
__m128 result4 = _mm_mul_ps(num1_4, num2_4);
return result4;
}
__m128 calc_mov(float* num1_4_addr, float* num2_4_addr)
{
__m128 num1_4 = _mm_load_ps(num1_4_addr);
__m128 num2_4 = _mm_load_ps(num2_4_addr);
__m128 result4 = _mm_mul_ps(num1_4, num2_4);
return result4;
}
和组装
calc_set1(float, float):
shufps [=11=], %xmm0, %xmm0
shufps [=11=], %xmm1, %xmm1
mulps %xmm1, %xmm0
ret
calc_mov(float*, float*):
movaps (%rdi), %xmm0
mulps (%rsi), %xmm0
ret
您可以看到 calc_mov()
的表现符合您的预期,而 calc_set1()
使用单个随机播放指令。
一个movps
指令可能需要大约四个周期来生成地址+如果L1缓存的加载端口繁忙+更多在缓存未命中的罕见事件中。
shufps
将在任何最新的英特尔微体系结构上占用一个周期。我相信无论是 SSE128 还是 AVX256 都是如此。因此我建议使用 mm_set1_ps
方法。
当然,洗牌指令假定浮点数已经在 SSE/AVX 寄存器中。如果您从内存中加载它,那么广播会更好,因为它会在一条指令中捕获 movps
和 shufps
的精华。
我正在玩广播来寻找 fastest way to fill a vector (SSE2) with a certain value. Templates friendly 的答案。看看一些广播的 asm 转储。
set1
每次使用应该不会有太大区别,只要编译器知道要广播的值不会别名任何东西。 (如果编译器不能假设它没有别名,它将不得不在每次写入可能有别名的数组或指针后重做广播。)
将 set1
结果存储在命名变量中通常是一种很好的风格。如果编译器用完向量寄存器,它可能会将向量溢出到堆栈,稍后重新加载,或者它可能会重新广播。我不确定编码风格是否会影响这个决定。
我不会使用 static const
变量在函数调用之间缓存它。 (这可能导致编译器生成代码来检查变量是否已在每次调用时初始化。)
编译时常量的广播有时会导致编译时广播,因此您的代码在内存中只有 16B 的常量数据。
AVX1 广播寄存器中已有的值是最坏的情况。 AVX1 仅提供内存源 vbroadcastps
(仅使用加载端口)。一次广播需要 shufps / vinsertf128
.
vbroadcastps ymm, xmm
(使用 shuffle 端口)需要 AVX2。
当我需要做一个向量运算,它的操作数只是一个广播到每个组件的浮点数时,我是否应该预先计算 __m256
或 __m128
,并在需要时加载它,或者每次我需要向量时使用 _mm_set1_ps
将浮点数广播到寄存器?
我一直在预先计算非常重要且使用频繁的向量,并即时生成不太重要的向量。但是我真的通过预计算获得了任何速度吗?值得这么麻烦吗?
_mm_set1_ps
是用一条指令实现的吗?这可能会回答我的问题。
我认为通常最好从代码(例如循环)中分离出 SSE 向量,并在需要时使用它,假设你注意不要意外地强制它进入记忆。 (例如,如果你获取它的地址或通过引用将它传递给另一个函数,那么它可能会被强制进入内存并且你可能会得到奇怪的行为。)
这个想法是 通常 最好避免将值传入和传出 SSE 寄存器,如果碰巧在您的特定情况下不是这种情况,编译器已经知道如何该值是构造出来的,通常可以rematerialize it if need be. I think this is a lot easier than loop-invariant code motion,这是反向优化(即编译器为您分解出来的地方)并且需要编译器证明代码确实是循环不变的。
当然这在很大程度上取决于您的代码,但我已经使用这两种方法实现了两个简单的功能。 See code
__m128 calc_set1(float num1, float num2)
{
__m128 num1_4 = _mm_set1_ps(num1);
__m128 num2_4 = _mm_set1_ps(num2);
__m128 result4 = _mm_mul_ps(num1_4, num2_4);
return result4;
}
__m128 calc_mov(float* num1_4_addr, float* num2_4_addr)
{
__m128 num1_4 = _mm_load_ps(num1_4_addr);
__m128 num2_4 = _mm_load_ps(num2_4_addr);
__m128 result4 = _mm_mul_ps(num1_4, num2_4);
return result4;
}
和组装
calc_set1(float, float):
shufps [=11=], %xmm0, %xmm0
shufps [=11=], %xmm1, %xmm1
mulps %xmm1, %xmm0
ret
calc_mov(float*, float*):
movaps (%rdi), %xmm0
mulps (%rsi), %xmm0
ret
您可以看到 calc_mov()
的表现符合您的预期,而 calc_set1()
使用单个随机播放指令。
一个movps
指令可能需要大约四个周期来生成地址+如果L1缓存的加载端口繁忙+更多在缓存未命中的罕见事件中。
shufps
将在任何最新的英特尔微体系结构上占用一个周期。我相信无论是 SSE128 还是 AVX256 都是如此。因此我建议使用 mm_set1_ps
方法。
当然,洗牌指令假定浮点数已经在 SSE/AVX 寄存器中。如果您从内存中加载它,那么广播会更好,因为它会在一条指令中捕获 movps
和 shufps
的精华。
我正在玩广播来寻找 fastest way to fill a vector (SSE2) with a certain value. Templates friendly 的答案。看看一些广播的 asm 转储。
set1
每次使用应该不会有太大区别,只要编译器知道要广播的值不会别名任何东西。 (如果编译器不能假设它没有别名,它将不得不在每次写入可能有别名的数组或指针后重做广播。)
将 set1
结果存储在命名变量中通常是一种很好的风格。如果编译器用完向量寄存器,它可能会将向量溢出到堆栈,稍后重新加载,或者它可能会重新广播。我不确定编码风格是否会影响这个决定。
我不会使用 static const
变量在函数调用之间缓存它。 (这可能导致编译器生成代码来检查变量是否已在每次调用时初始化。)
编译时常量的广播有时会导致编译时广播,因此您的代码在内存中只有 16B 的常量数据。
AVX1 广播寄存器中已有的值是最坏的情况。 AVX1 仅提供内存源 vbroadcastps
(仅使用加载端口)。一次广播需要 shufps / vinsertf128
.
vbroadcastps ymm, xmm
(使用 shuffle 端口)需要 AVX2。