我正在尝试优化以下函数(简化了一下,但它是我的程序花了很多时间的循环):
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位? llvm与-O3没有做任何聪明的优化.
更新:
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; }
解决方法
您可以使用_mm_cmplt_epi8和_mm_cmpgt_epi8(msvc intrinsics)对比较进行矢量化.
然后,您可以对ANDing比较结果的结果使用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; }
由于我在这台PC上没有64 OS,我不能进行适当的性能测试.
但是,分析运行给出了:
>天真循环:30.44
> 64位整数:15.22(在32位操作系统上)
> SSE impl:5.21
所以SSE版本比朴素循环版本快得多.我希望64位版本在64位系统上表现更好 – SSE和64位版本之间可能差别不大.