【问题标题】:Is this function a good candidate for SIMD on Intel?这个函数是英特尔 SIMD 的一个很好的候选者吗?
【发布时间】:2014-04-08 17:48:13
【问题描述】:

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

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;
        }

【问题讨论】:

  • 64 位 x86 意味着 SSE2,因此它应该使用 16 个 8 位字节。 SSE 没有整数指令。此外,如果它足够新,那么您可以使用 AVX2
  • 如果您的数据集足够大,您可以从在循环中抛出 OpenMP pragma 开始。如果您不介意我的询问,您正在处理的数据的具体性质是什么?
  • 对任何人的小提示:如果您假装p[i] 是一个签名字符(即使它不是),那么p[i] &gt;= 32 &amp;&amp; p[i] &lt;= 127 可以简化为p[i] &gt;= 32
  • 对于特定机器的更多优化使用-march=native
  • 您的性能问题可能是由多种原因造成的。您可能需要调整 SSE 例程以确保使用对齐的负载。查看生成的汇编程序,以确保有适当的寄存器使用。您可能还会发现您的性能是可变的,具体取决于您为数据分配的内存如何对齐以及如何将其提取到缓存行中。

标签: c++ c optimization simd


【解决方案1】:

我不确定这是否是您问题的答案,也不确定这是否会大大加快您的代码速度,但这是我想到的一个想法。由于 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;
}

对于需要对齐的架构,需要在第一个循环之前进行检查。

【讨论】:

  • +1 好主意 - 避免需要检查 sse 和未对齐的负载等
  • 不幸的是,现在我看到测试 (q[i] & 0x6060606060606060) != 0) 不起作用。它不保证每个字节不为零。
  • 您可以右移 1 位,或者将原始掩码与 0x20202020...
  • @Pete 你是对的。谢谢。我以这种方式编辑了帖子。
  • 您也可以通过以下方式删除 0x808080 测试:(((~x) >> 2) & ((x >> 1) | x)) == 0x20202..
【解决方案2】:

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

然后,您可以对比较结果进行“与”运算的结果使用移动掩码。如果移动掩码的结果是 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 位操作系统,我无法进行适当的性能测试。 但是,分析运行给出了:

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

所以 SSE 版本比 naive loop 版本快很多。我希望 64 位版本在 64 位系统上的性能要好得多 - SSE 和 64 位版本之间可能差别不大。

【讨论】:

  • 我建议_mm_movemask_epi8 而不是屏蔽和添加(当然会改变退出条件)
【解决方案3】:

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

这是answer &lt;= 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

所有解决方案的完整代码以及测试均可在here 获得。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-02
    • 2016-05-03
    • 1970-01-01
    • 2015-01-04
    • 2015-05-09
    相关资源
    最近更新 更多