此函数是英特尔SIMD的理想候选函数吗

Is this function a good candidate for SIMD on Intel?

本文关键字:理想 候选函数 SIMD 函数 英特尔      更新时间:2023-10-16

我正在尝试优化以下函数(稍微简化了一点,但这是我的程序花费大量时间的循环):

int f(int len, unsigned char *p) {
int i = 0;
while (i < len && p[i] >= 32 && p[i] <= 127) {
i++;
}
return i;
}

我认为它可以用矢量指令进行优化,但从一点研究来看,SSE似乎不适合在字节级别工作。此程序仅针对OSX上的64位Intel CPU。有没有一个我看不到的聪明的小把戏可以让我一次处理64位?带有-O3的llvm没有做任何巧妙的优化。

更新:

SIMD代码在我的基准测试中通常是最快的(取决于输入的大小),但出于某种原因,使用SIMD的应用程序总体上比使用天真的代码或比特游戏慢。对于上下文,应用程序是在终端模拟器的输入流中查找ASCII字符串的子序列的长度。ASCII字符串得到特殊的"快速路径"处理。我只能把一个答案标记为正确,但两个都很棒。我确实做了一个小的改进,通过这样做删除了if语句:

while (i < len - 8) {
uint64_t bytes = *(uint64_t *)(p + i);
uint64_t middleBits = bytes & 0x6060606060606060;
uint64_t highBits = bytes & 0x8080808080808080;
middleBits |= (middleBits >> 1);
middleBits &= ~(highBits >> 2);
if ((middleBits & 0x2020202020202020) != 0x2020202020202020) {
break;
}
i += 8;
}

我不确定这是否是你的问题的答案,也不知道这是否会大大加快你的代码,但这是我脑海中的一个想法。由于32等于2^5,如果一个字节在32和128之间,则必须设置第6或第7位,并清除第8位。你可以将测试扩展到64位整数,给我一个代码,比如:

// check whether each byte is in range 32 - 128.
unsigned bytesInRange(unsigned long long x) {
unsigned long long y, z;
if ((x & 0x8080808080808080LL) != 0) return(0);
y = x >> 1;
z = x | y;
if ((z & 0x2020202020202020LL) == 0x2020202020202020LL) return(1);
return(0);
}
int f(int len, unsigned char *p) {
int i = 0;
int len8 = len / 8;
unsigned long long *q = (unsigned long long *) p;
while (i < len8 && bytesInRange(q[i])) {
i++;
}
i = i * 8;
while (i < len && p[i] >= 32 && p[i] <= 127) {
i++;
}
return i;
}

对于需要对齐的体系结构,需要在第一次循环之前进行检查。

您可以使用_mm_cmplt_epi8和_mm_cmpgt_epi7(msvc内部函数)对比较进行矢量化。

然后,您可以对比较结果进行"与"运算的结果使用movemask。如果movemask的结果是0xFFFF,则所有比较都通过。否则,您需要运行尾循环来找出未通过测试的正确位置。你可以从面具中看出这一点,但根据"len"的值,这可能不值得付出努力。

如果"len"不是16的倍数,则还需要尾部的原始未乘循环。它可能更快,也可能不更快——你需要对它进行分析才能确定。

废弃它-比较对有符号的值进行操作,它不起作用。。

下面是工作版本。

union UmmU8 {
__m128i mm_;
struct {
unsigned char u8_;
};
};
int f(int len, unsigned char *p) {
int i = 0;
__m128i A;
__m128i B;
__m128i C;
UmmU8* pu = (UmmU8*)p;    
int const len16 = len / 16;
while (i < len16) {
A = pu[i].mm_;
B = _mm_slli_epi32(A, 1);
C = _mm_slli_epi32(A, 2);
B = _mm_or_si128(B, C);
A = _mm_andnot_si128(A, B);
int mask = _mm_movemask_epi8(A);
if (mask == 0xFFFF) {
++i;
}
else {
if (mask == 0) {
return i * 16;
}
break;
}
}
i *= 16;
while (i < len && p[i] >= 32 && p[i] <= 127) {
i++;
}
return i;
}

由于我在这台电脑上没有64操作系统,我无法进行适当的性能测试。然而,分析运行给出:

  • 天真循环:30.44
  • 64位整数:15.22(在32位操作系统上)
  • SSE impl:5.21

因此SSE版本比naive循环版本快得多。我预计64位版本在64位系统上的性能会更好——SSE和64位版本之间可能没有什么区别。

我尝试了几种方法来解决这个问题:基于SSE2和SSE4.2。SSE4.2中的字符串操作相当慢,SSE2版本很容易胜过它们。请注意,一般来说,最佳解决方案在很大程度上取决于预期答案的平均大小。

以下是answer <= 400性能最好的解决方案之一:

//SSE2 vectorization by stgatilov: no unrolling, fast BSF tail
int CommonAsciiLength_sse2_end(int len, unsigned char *p) {
const __m128i *ptr = (const __m128i *)p;
int blocks = len >> 4;
int cnt;
for (cnt = 0; cnt < blocks; cnt++) {
__m128i mask = _mm_cmplt_epi8(ptr[cnt], _mm_set1_epi8(32));
int val = _mm_movemask_epi8(mask);
if (val)
return 16 * cnt + __builtin_ctz(val);
}
__m128i mask = _mm_cmplt_epi8(ptr[cnt], _mm_set1_epi8(32));
int val = _mm_movemask_epi8(mask);
val |= -(1 << (len - 16 * cnt));
return 16 * cnt + __builtin_ctz(val);
}

请注意,对于较大的答案,此解决方案将从展开中进一步受益。

以下是不同解决方案和不同答案长度的一些时间安排。在常春藤大桥上测量。请注意,只有在一次跑步中比较计时才有意义,在不同平均值的跑步之间进行比较。答案是不正确的。

All checked.
Average answer = 7.0
Time = 4.879   (1884680192) original
Time = 6.021   (1884680192) bitmask
Time = 5.205   (1884680192) Pete
Time = 5.094   (1884680192) sse2
Time = 5.301   (1884680192) sse2_x4
Time = 1.603   (1884680192) sse42
Time = 1.235   (1884680192) sse2_end
Time = 2.319   (1884680192) sse2_x4_end
=========================================
All checked.
Average answer = 47.0
Time = 5.825   (-1867343006) original
Time = 4.792   (-1867343006) bitmask
Time = 4.490   (-1867343006) Pete
Time = 4.327   (-1867343006) sse2
Time = 5.260   (-1867343006) sse2_x4
Time = 3.347   (-1867343006) sse42
Time = 2.505   (-1867343006) sse2_end
Time = 3.008   (-1867343006) sse2_x4_end
=========================================
All checked.
Average answer = 151.4
Time = 4.372   (-2086294174) original
Time = 2.150   (-2086294174) bitmask
Time = 1.662   (-2086294174) Pete
Time = 1.492   (-2086294174) sse2
Time = 2.249   (-2086294174) sse2_x4
Time = 1.649   (-2086294174) sse42
Time = 0.986   (-2086294174) sse2_end
Time = 1.398   (-2086294174) sse2_x4_end
=========================================
All checked.
Average answer = 426.8
Time = 3.772   (1814680269) original
Time = 1.320   (1814680269) bitmask
Time = 0.830   (1814680269) Pete
Time = 0.692   (1814680269) sse2
Time = 0.870   (1814680269) sse2_x4
Time = 1.186   (1814680269) sse42
Time = 0.531   (1814680269) sse2_end
Time = 0.573   (1814680269) sse2_x4_end
=========================================
All checked.
Average answer = 1083.4
Time = 2.788   (358018991) original
Time = 0.819   (358018991) bitmask
Time = 0.443   (358018991) Pete
Time = 0.344   (358018991) sse2
Time = 0.347   (358018991) sse2_x4
Time = 0.813   (358018991) sse42
Time = 0.297   (358018991) sse2_end
Time = 0.256   (358018991) sse2_x4_end

这里提供了所有解决方案的完整代码以及测试。

相关文章: