我需要每秒处理大约2000个100元素的数组。这些数组以shorts的形式传递给我,数据存在高位中,需要进行移位和强制转换成chars类型。这是我能得到的最高效的方法,还是有更快的方式来执行此操作?(我需要跳过其中2个值)
for(int i = 0; i < 48; i++)
{
a[i] = (char)(b[i] >> 8);
a[i+48] = (char)(b[i+50] >> 8);
}
我需要每秒处理大约2000个100元素的数组。这些数组以shorts的形式传递给我,数据存在高位中,需要进行移位和强制转换成chars类型。这是我能得到的最高效的方法,还是有更快的方式来执行此操作?(我需要跳过其中2个值)
for(int i = 0; i < 48; i++)
{
a[i] = (char)(b[i] >> 8);
a[i+48] = (char)(b[i+50] >> 8);
}
bool isBigEndian() {
short i = 1; // sets only lowest order bit
char *ix = reinterpret_cast<char *>(&i);
return (*ix == 0); // will be 1 if little endian
}
int shft = isBigEndian()? 0 : 1;
char * pb = reinterpret_cast<char *>(b);
for(int i = 0; i < 48; i++)
{
a[i] = pt[2 * i + shft];
a[i+48] = pt[2 * i + 50 + shft];
}
但是像所有低级优化一样,这必须与将在生产代码中使用的编译器和编译器选项进行基准测试。
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
,例如。或者在这种情况下,使用一个变体来替代 isBigEndian
的 ==
表达式,不需要 #if
。不过遗憾的是,这并不符合 ISO 标准的 C 或 C++。 - Peter Cordes有一组short
数组将占用两倍的内存,因此缓存效应可能很重要。
所有这些都取决于您需要对字节数组执行什么操作。
对于x86目标,您可以使用SIMD向量而不是逐个字符循环来获得大幅加速。对于其他您关心的编译目标,您可以编写类似的特殊版本。例如,我假设ARM NEON具有类似的洗牌功能。
在编写特定于平台的版本时,您还可以做出所有在该平台上为真的字节序和未对齐访问的假设。
#ifdef __SSE2__ // will be true for all x86-64 builds and most i386 builds
#include <immintrin.h>
static __m128i pack2(const short *p) {
__m128i lo = _mm_loadu_si128((__m128i*)p);
__m128i hi = _mm_loadu_si128((__m128i*)(p + 8));
lo = _mm_srli_epi16(lo, 8); // logical shift, not arithmetic, because we need the high byte to be zero
hi = _mm_srli_epi16(hi, 8);
return _mm_packus_epi16(lo, hi); // treats input as signed, saturates to unsigned 0x0 .. 0xff range
}
#endif // SSE2
void conv(char *a, const short *b) {
#ifdef __SSE2__
for(int i = 0; i < 48; i+=16) {
__m128i low = pack2(b+i);
_mm_storeu_si128((__m128i *)(a+i), low);
__m128i high = pack2(b+i + 50);
_mm_storeu_si128((__m128i *)(a+i + 48), high);
}
#else
/******* Fallback C version *******/
for(int i = 0; i < 48; i++) {
a[i] = (char)(b[i] >> 8);
a[i+48] = (char)(b[i+50] >> 8);
}
#endif
}
正如您在Godbolt Compiler Explorer上看到的那样, 当每次存储16B时,gcc完全展开了循环,因为只有几次迭代。
这应该可以正常运行,但在Skylake之前的处理器上,将两个short
向量进行移位后再存储会出现瓶颈。Haswell每个时钟周期只能维持一个psrli
。(当移位计数是立即数时,Skylake可以维持每0.5个时钟周期一个。请参见Agner Fog的指南和insn表,链接在x86标签wiki中。)(__m128i*)(1 + (char*)p)
加载可能会获得更好的结果,因此我们想要的字节已经在每个16位元素的低半部分中。我们仍然需要使用_mm_and_si128
屏蔽掉每个元素的高半部分,而不是移位,但PAND
可以在任何向量执行端口上运行,因此它具有每个时钟周期的三倍吞吐量。vpand xmm0, xmm5, [rsi]
,其中xmm5是_mm_set1_epi16(0x00ff)
的掩码,[rsi]
保存2*i + 1 + (char*)b
。融合域uop吞吐量可能会成为一个问题,就像常见于具有大量加载/存储和计算的代码一样。b
是16B对齐的,则测试掩码版本与移位版本将很值得。b
后面一字节的内容。如果您确保b
有某种填充以使其不会到达内存页的末尾,那么这是可以接受的。
在AVX2中,vpackuswb ymm
在两个独立的通道中操作。不知道是否有必要对256b向量进行加载和掩码处理(或移位),然后使用vextracti128
并在256b向量的两个半部分上进行128b打包。
或者将两个向量之间进行256b打包,然后使用vpermq
(_mm256_permute4x64_epi64
)进行排序:
lo = _mm256_loadu(b..); // { b[15..8] | b[7..0] }
hi = // { b[31..24] | b[23..16] }
// mask or shift
__m256i packed = _mm256_packus_epi16(lo, hi); // [ a31..24 a15..8 | a23..16 a7..0 ]
packed = _mm256_permute4x64_epi64(packed, _MM_SHUFFLE(3, 1, 2, 0));
(char)(b[i] >> 8);
。 - SergeyA