SSE2 按向量移动
SSE2 shift by vector
我一直在尝试在 SSE2 内在函数中通过向量实现移位,但从实验和 the intel intrinsic guide 来看,它似乎只使用向量的最低有效部分。
改写我的问题,给定一个向量 {v1, v2, ..., vn} 和一组位移 {s1, s2, ..., sn},我如何计算结果 {r1, r2, ..., rn} 这样:
r1 = v1 << s1
r2 = v2 << s2
...
rn = vn << sn
因为 _mm_sll_epi* 执行此操作:
r1 = v1 << s1
r2 = v2 << s1
...
rn = vn << s1
提前致谢。
编辑:
这是我的代码:
#include <iostream>
#include <cstdint>
#include <mmintrin.h>
#include <emmintrin.h>
namespace SIMD {
using namespace std;
class SSE2 {
public:
// flipped operands due to function arguments
SSE2(uint64_t a, uint64_t b, uint64_t c, uint64_t d) { low = _mm_set_epi64x(b, a); high = _mm_set_epi64x(d, c); }
uint64_t& operator[](int idx)
{
switch (idx) {
case 0:
_mm_storel_epi64((__m128i*)result, low);
return result[0];
case 1:
_mm_store_si128((__m128i*)result, low);
return result[1];
case 2:
_mm_storel_epi64((__m128i*)result, high);
return result[0];
case 3:
_mm_store_si128((__m128i*)result, high);
return result[1];
}
/* Undefined behaviour */
return 0;
}
SSE2& operator<<=(const SSE2& rhs)
{
low = _mm_sll_epi64(low, rhs.getlow());
high = _mm_sll_epi64(high, rhs.gethigh());
return *this;
}
void print()
{
uint64_t a[2];
_mm_store_si128((__m128i*)a, low);
cout << hex;
cout << a[0] << ' ' << a[1] << ' ';
_mm_store_si128((__m128i*)a, high);
cout << a[0] << ' ' << a[1] << ' ';
cout << dec;
}
__m128i getlow() const
{
return low;
}
__m128i gethigh() const
{
return high;
}
private:
__m128i low, high;
uint64_t result[2];
};
}
int main()
{
cout << "operator<<= test: vector << vector: ";
{
auto x = SIMD::SSE2(7, 8, 15, 10);
auto y = SIMD::SSE2(4, 5, 6, 7);
x.print();
y.print();
x <<= y;
if (x[0] != 112 || x[1] != 256 || x[2] != 960 || x[3] != 1280) {
cout << "FAILED: ";
x.print();
cout << endl;
} else {
cout << "PASSED" << endl;
}
}
return 0;
}
应该发生的结果是 {7 << 4 = 112, 8 << 5 = 256, 15 << 6 = 960, 10 << 7 = 1280}。结果似乎是 {7 << 4 = 112, 8 << 4 = 128, 15 << 6 = 960, 15 << 6 = 640},这不是我想要的。
希望这对你有所帮助,Jens。
如果 AVX2 可用,并且您的元素是 32 位或 64 位,则您的操作需要一个可变移位指令:vpsrlvq
, (__m128i _mm_srlv_epi64 (__m128i a, __m128i count)
)
对于 SSE4.1 的 32 位元素,请参阅 。根据延迟与吞吐量要求,您可以进行单独的移位,然后混合,或使用乘法(通过特殊构造的 2 的幂向量)来获得可变计数的左移,然后进行相同的计数-所有元素右移。
对于您的情况,具有运行时变量移位计数的 64 位元素:
每个 SSE 向量只有两个元素,所以我们只需要两次移位,然后合并结果(我们可以使用 pblendw 或浮点数 movsd
(这可能会导致额外的在某些 CPU 上绕过延迟延迟),或者我们可以使用两次随机播放,或者我们可以执行两个 AND 和一个 OR。
__m128i SSE2_emulated_srlv_epi64(__m128i a, __m128i count)
{
__m128i shift_low = _mm_srl_epi64(a, count); // high 64 is garbage
__m128i count_high = _mm_unpackhi_epi64(count,count); // broadcast the high element
__m128i shift_high = _mm_srl_epi64(a, count_high); // low 64 is garbage
// SSE4.1:
// return _mm_blend_epi16(shift_low, shift_high, 0x0F);
#if 1 // use movsd to blend
__m128d blended = _mm_move_sd( _mm_castsi128_pd(shift_high), _mm_castsi128_pd(shift_low) ); // use movsd as a blend. Faster than multiple instructions on most CPUs, but probably bad on Nehalem.
return _mm_castpd_si128(blended);
#else // SSE2 without using FP instructions:
// if we're going to do it this way, we could have shuffled the input before shifting. Probably not helpful though.
shift_high = _mm_unpackhi_epi64(shift_high, shift_high); // broadcast the high64
return _mm_unpacklo_epi64(shift_high, shift_low); // combine
#endif
}
其他像 pshufd 或 psrldq 这样的洗牌可以工作,但是 punpckhqdq gets the job done without needing an immediate byte, so it's one byte shorter. SSSE3 palignr
可以从一个寄存器中获取高位元素,从另一个寄存器中获取低位元素到一个向量中,但它们会被颠倒(所以我们会需要一个 pshufd
来交换高低两半)。 shufpd
可以混合,但与 movsd
相比没有优势。
有关在两个整数指令之间使用 FP 指令的潜在旁路延迟延迟的详细信息,请参阅 Agner Fog's microarch guide。它在 Intel SnB 系列 CPU 上可能没问题,因为其他 FP 洗牌是。 (是的,movsd xmm1, xmm0
在端口 5 的洗牌单元上运行。如果不需要合并行为,即使是标量,也可以使用 movaps
或 movapd
进行 reg-reg 移动)。
这会编译(在 Godbolt 上使用 gcc5.3 -O3
)到
movdqa xmm2, xmm0 # tmp97, a
psrlq xmm2, xmm1 # tmp97, count
punpckhqdq xmm1, xmm1 # tmp99, count
psrlq xmm0, xmm1 # tmp100, tmp99
movsd xmm0, xmm2 # tmp102, tmp97
ret
我一直在尝试在 SSE2 内在函数中通过向量实现移位,但从实验和 the intel intrinsic guide 来看,它似乎只使用向量的最低有效部分。
改写我的问题,给定一个向量 {v1, v2, ..., vn} 和一组位移 {s1, s2, ..., sn},我如何计算结果 {r1, r2, ..., rn} 这样:
r1 = v1 << s1
r2 = v2 << s2
...
rn = vn << sn
因为 _mm_sll_epi* 执行此操作:
r1 = v1 << s1
r2 = v2 << s1
...
rn = vn << s1
提前致谢。
编辑:
这是我的代码:
#include <iostream>
#include <cstdint>
#include <mmintrin.h>
#include <emmintrin.h>
namespace SIMD {
using namespace std;
class SSE2 {
public:
// flipped operands due to function arguments
SSE2(uint64_t a, uint64_t b, uint64_t c, uint64_t d) { low = _mm_set_epi64x(b, a); high = _mm_set_epi64x(d, c); }
uint64_t& operator[](int idx)
{
switch (idx) {
case 0:
_mm_storel_epi64((__m128i*)result, low);
return result[0];
case 1:
_mm_store_si128((__m128i*)result, low);
return result[1];
case 2:
_mm_storel_epi64((__m128i*)result, high);
return result[0];
case 3:
_mm_store_si128((__m128i*)result, high);
return result[1];
}
/* Undefined behaviour */
return 0;
}
SSE2& operator<<=(const SSE2& rhs)
{
low = _mm_sll_epi64(low, rhs.getlow());
high = _mm_sll_epi64(high, rhs.gethigh());
return *this;
}
void print()
{
uint64_t a[2];
_mm_store_si128((__m128i*)a, low);
cout << hex;
cout << a[0] << ' ' << a[1] << ' ';
_mm_store_si128((__m128i*)a, high);
cout << a[0] << ' ' << a[1] << ' ';
cout << dec;
}
__m128i getlow() const
{
return low;
}
__m128i gethigh() const
{
return high;
}
private:
__m128i low, high;
uint64_t result[2];
};
}
int main()
{
cout << "operator<<= test: vector << vector: ";
{
auto x = SIMD::SSE2(7, 8, 15, 10);
auto y = SIMD::SSE2(4, 5, 6, 7);
x.print();
y.print();
x <<= y;
if (x[0] != 112 || x[1] != 256 || x[2] != 960 || x[3] != 1280) {
cout << "FAILED: ";
x.print();
cout << endl;
} else {
cout << "PASSED" << endl;
}
}
return 0;
}
应该发生的结果是 {7 << 4 = 112, 8 << 5 = 256, 15 << 6 = 960, 10 << 7 = 1280}。结果似乎是 {7 << 4 = 112, 8 << 4 = 128, 15 << 6 = 960, 15 << 6 = 640},这不是我想要的。
希望这对你有所帮助,Jens。
如果 AVX2 可用,并且您的元素是 32 位或 64 位,则您的操作需要一个可变移位指令:vpsrlvq
, (__m128i _mm_srlv_epi64 (__m128i a, __m128i count)
)
对于 SSE4.1 的 32 位元素,请参阅
对于您的情况,具有运行时变量移位计数的 64 位元素:
每个 SSE 向量只有两个元素,所以我们只需要两次移位,然后合并结果(我们可以使用 pblendw 或浮点数 movsd
(这可能会导致额外的在某些 CPU 上绕过延迟延迟),或者我们可以使用两次随机播放,或者我们可以执行两个 AND 和一个 OR。
__m128i SSE2_emulated_srlv_epi64(__m128i a, __m128i count)
{
__m128i shift_low = _mm_srl_epi64(a, count); // high 64 is garbage
__m128i count_high = _mm_unpackhi_epi64(count,count); // broadcast the high element
__m128i shift_high = _mm_srl_epi64(a, count_high); // low 64 is garbage
// SSE4.1:
// return _mm_blend_epi16(shift_low, shift_high, 0x0F);
#if 1 // use movsd to blend
__m128d blended = _mm_move_sd( _mm_castsi128_pd(shift_high), _mm_castsi128_pd(shift_low) ); // use movsd as a blend. Faster than multiple instructions on most CPUs, but probably bad on Nehalem.
return _mm_castpd_si128(blended);
#else // SSE2 without using FP instructions:
// if we're going to do it this way, we could have shuffled the input before shifting. Probably not helpful though.
shift_high = _mm_unpackhi_epi64(shift_high, shift_high); // broadcast the high64
return _mm_unpacklo_epi64(shift_high, shift_low); // combine
#endif
}
其他像 pshufd 或 psrldq 这样的洗牌可以工作,但是 punpckhqdq gets the job done without needing an immediate byte, so it's one byte shorter. SSSE3 palignr
可以从一个寄存器中获取高位元素,从另一个寄存器中获取低位元素到一个向量中,但它们会被颠倒(所以我们会需要一个 pshufd
来交换高低两半)。 shufpd
可以混合,但与 movsd
相比没有优势。
有关在两个整数指令之间使用 FP 指令的潜在旁路延迟延迟的详细信息,请参阅 Agner Fog's microarch guide。它在 Intel SnB 系列 CPU 上可能没问题,因为其他 FP 洗牌是。 (是的,movsd xmm1, xmm0
在端口 5 的洗牌单元上运行。如果不需要合并行为,即使是标量,也可以使用 movaps
或 movapd
进行 reg-reg 移动)。
这会编译(在 Godbolt 上使用 gcc5.3 -O3
)到
movdqa xmm2, xmm0 # tmp97, a
psrlq xmm2, xmm1 # tmp97, count
punpckhqdq xmm1, xmm1 # tmp99, count
psrlq xmm0, xmm1 # tmp100, tmp99
movsd xmm0, xmm2 # tmp102, tmp97
ret