【问题标题】:Find index of maximum element in x86 SIMD vector在 x86 SIMD 向量中查找最大元素的索引
【发布时间】:2014-06-28 17:28:39
【问题描述】:

我正在考虑为 uint32_t 实现 8 元堆排序。为此,我需要一个函数来选择 8 元素向量中最大元素的索引,以便我可以将其与父元素进行比较,并有条件地执行交换和进一步的 siftDown 步骤。

(8 uint32_ts 可以更改为例如 16 uint32_ts 或 8 uint64_t 或 x86 SIMD 可以有效支持的任何内容)。

我对如何做到这一点有一些想法,但我正在寻找比非矢量化代码更快的东西,特别是我正在寻找能够让我进行快速堆排序的东西。

我有 clang++ 3.3 和 Core i7-4670,所以我应该能够使用最新的 x86 SIMD 东西。

(顺便说一句:这是一个更大项目的一部分:https://github.com/tarsa/SortingAlgorithmsBenchmark,例如四元堆排序,所以在实施 SIMD 堆排序后,我可以立即比较它们)

重复一遍 - 问题是:在 x86 SIMD 向量中计算最大元素索引的最有效方法是什么?

PS: 这不是链接问题的重复 - 请注意,我要求的是最大元素的 index,而不仅仅是元素值。

【问题讨论】:

  • 不,它不是重复的。请注意,我正在寻找最大元素的索引,而不仅仅是它的值。
  • 我猜你可以使用 __m128 或 __m256。
  • 我不认为你想要索引的事实对答案有很大的影响 - SSE 指令集是围绕垂直而不是水平计算构建的。
  • 我刚刚在 SSE4.1 中发现了一个水平搜索指令:PHMINPOSUW。可能这可以解决问题,但现在的问题是例如使用两个 16 位最大选择 + 一些掩码进行 32 位最大选择。

标签: c++ x86 sse simd avx


【解决方案1】:

水平操作对于 SIMD 来说是个坏消息,尤其是对于 AVX,其中大多数 256 位指令实际上被分解为两个单独的 128 位操作。话虽如此,如果你真的必须在 8 个元素上进行水平 32 位最大值,那么我认为一般方法必须是:

  • 找到最大值(通常是几个移位/置换和最大值操作)
  • 在第二个向量的所有 8 个元素上求最大值(可以与之前的操作结合使用)
  • 比较原始向量和最大向量 (_mm256_cmpeq_epi32)
  • 提取标量掩码 (_mm256_movemask_epi8)
  • 将标量掩码转换为索引

这是我刚刚汇总的 AVX2 实现的第一次通过 - 我在 2.6 GHz Haswell 上对其进行了测试和基准测试,它以大约 1.7 ns/向量的速度运行(包括加载向量和存储结果索引):

uint8_t _mm256_hmax_index(const __m256i v)
{
    __m256i vmax = v;

    vmax = _mm256_max_epu32(vmax, _mm256_alignr_epi8(vmax, vmax, 4));
    vmax = _mm256_max_epu32(vmax, _mm256_alignr_epi8(vmax, vmax, 8));
    vmax = _mm256_max_epu32(vmax, _mm256_permute2x128_si256(vmax, vmax, 0x01));

    __m256i vcmp = _mm256_cmpeq_epi32(v, vmax);

    uint32_t mask = _mm256_movemask_epi8(vcmp);

    return __builtin_ctz(mask) >> 2;
}

【讨论】:

【解决方案2】:

在 n 路 SIMD 向量上执行水平操作(点积、求和、最大索引等)的最有效方法是一次执行 n 个操作,方法是转置它们并改用垂直操作。某些 SIMD 架构对水平操作有更好的支持,但总的来说,分块转置方法会更加灵活和高效。

【讨论】:

    【解决方案3】:

    使用Vc 库我会写:

    size_t maximumIndex(Vc::uint_v vec) {
      const unsigned int max = vec.max();
      return (max == vec).firstOne();
    }
    

    使用内在函数应该是这样的(这是没有 AVX2 的 AVX - 使用 AVX2 会变得稍微容易一些):

    size_t maximumIndex(_mm256i vec) {
      __m128i lo = _mm256_castsi256_si128(vec);
      __m128i hi = _mm256_extractf128_si256(vec, 1);
      __m128i tmp = _mm_max_epu32(lo, hi);
      tmp = _mm_max_epu32(tmp, _mm_shuffle_epi32(tmp, _MM_SHUFFLE(1, 0, 3, 2)));
      tmp = _mm_max_epu32(tmp, _mm_shufflelo_epi16(tmp, _MM_SHUFFLE(1, 0, 3, 2))); // using lo_epi16 for speed here
      const int max = _mm_cvtsi128_si32(tmp);
      tmp = _mm_packs_epi16(_mm_packs_epi32(_mm_cmpeq_epi32(_mm_set1_epi32(max), lo),
                                            _mm_cmpeq_epi32(_mm_set1_epi32(max), hi)),
                            _mm_setzero_si128());
      return _bit_scan_forward(_mm_movemask_epi8(tmp));
    }
    

    顺便说一句,如果您想从 SIMDized 合并排序中获得一些灵感,请看这里:http://code.compeng.uni-frankfurt.de/projects/vc/repository/revisions/master/entry/src/avx_sorthelper.cpp

    【讨论】:

    • 那个 sorthelper 应该只对单个向量进行排序?它似乎不是任意大小数组的通用排序算法。
    • 另外,我可以窃取您在此处发布的内容吗? :)
    • 合并排序只是第一步。您可以从那里继续对更大的数据集实施合并排序。
    • 是的,请随意使用/使用我的代码。 Vc代码,顺便说一句,是LGPL3(将来会是BSD)。
    • 感谢您的修复。我忽略了“未签名”的细节...... :-)
    猜你喜欢
    • 2021-07-02
    • 1970-01-01
    • 2021-11-07
    • 2021-06-20
    • 1970-01-01
    • 2014-09-03
    • 1970-01-01
    • 2020-12-05
    相关资源
    最近更新 更多