【问题标题】:Can I use SIMD to bucket sort / categorize?我可以使用 SIMD 进行桶排序/分类吗?
【发布时间】:2019-02-22 16:36:33
【问题描述】:

我对 SIMD 很好奇,想知道它是否可以处理这个用例。

假设我有一个由 2048 个整数组成的数组,例如 [0x018A, 0x004B, 0x01C0, 0x0234, 0x0098, 0x0343, 0x0222, 0x0301, 0x0398, 0x0087, 0x0167, 0x0389, 0x03F2, 0x0034, 0x0345, ...]

注意它们都是如何以 0x00、0x01、0x02 或 0x03 开头的。我想把它们分成4个数组:

  • 一个代表所有以 0x00 开头的整数
  • 一个代表所有以 0x01 开头的整数
  • 一个代表所有以 0x02 开头的整数
  • 一个代表所有以 0x03 开头的整数

我想我会有这样的代码:

int main() {
   uint16_t in[2048] = ...;

   // 4 arrays, one for each category
   uint16_t out[4][2048];

   // Pointers to the next available slot in each of the arrays
   uint16_t *nextOut[4] = { out[0], out[1], out[2], out[3] };

   for (uint16_t *nextIn = in; nextIn < 2048; nextIn += 4) {

       (*** magic simd instructions here ***)

       // Equivalent non-simd code:
       uint16_t categories[4];
       for (int i = 0; i < 4; i++) {
           categories[i] = nextIn[i] & 0xFF00;
       }
       for (int i = 0; i < 4; i++) {
           uint16_t category = categories[i];
           *nextOut[category] = nextIn[i];
           nextOut[category]++;
       }
   }
   // Now I have my categoried arrays!
}

我想我的第一个内循环不需要 SIMD,它可以只是一个(x &amp; 0xFF00FF00FF00FF00) 指令,但我想知道我们是否可以将第二个内循环变成一个 SIMD 指令。

对于我正在执行的“分类”操作,是否有任何类型的 SIMD 指令?

“插入”说明似乎有些希望,但我有点太幼稚了,无法理解 https://software.intel.com/en-us/node/695331 的描述。

如果没有,有什么可以接近的吗?

谢谢!

【问题讨论】:

  • 分散的商店 - 为此您需要 AVX-512。它可能也不会超级高效。
  • 感谢领导,这非常有趣!经过一番阅读,看起来分散的商店可以将一堆数字存储到一堆对应的指针中。我如何从这些数字(0x00-0x03)映射到指针?还有,为什么不高效?
  • 因为分散存储仍然会成为缓存未命中的瓶颈,或者如果不是像常规存储一样仍然限制为每个时钟 1 个元素。分散指令还解码为很多微指令(不仅仅是收集负载),因此它们会消耗前端吞吐量。您还必须检测冲突(当向量中的多个元素将进入同一个桶时,您需要将它们写入顺序地址,而不是相互踩踏,并且您必须将每个桶的位置计数器增加正确的数量.)
  • 因此,与一次执行 1 个元素相比,SIMD 版本需要做的额外工作很多。也许通过高效的vpconflictd(例如在 KNL 但不是 Skylake-avx512)你可以领先。这类似于直方图问题(您正在递增每个桶计数器的数组),但更难,因为您实际上仍然必须保留每个元素。
  • 感谢您提供所有这些信息,非常感谢!我也想知道这种冲突情况,听起来 vpconflictd 是我们能得到的最接近的。我会做一些关于直方图问题的阅读,谢谢你的领导!

标签: c arrays x86 simd bucket-sort


【解决方案1】:

您可以使用 SIMD 来做到这一点,但它的速度取决于您有哪些可用的指令集,以及您在实施中的聪明程度。

一种方法是获取数组并“筛选”它以分离出属于不同存储桶的元素。例如,从包含 16 个 16 位元素的数组中获取 32 个字节。使用一些cmpgt 指令来获取一个掩码,该掩码确定每个元素是属于00 + 01 存储桶还是02 + 03 存储桶。然后使用某种“压缩”或“过滤”操作将所有被屏蔽的元素连续移动到一个寄存器的一端,然后对未屏蔽的元素进行相同的移动。

然后再重复一遍,从01 中挑选出00 和从03 中挑选出02

使用 AVX2,您可以从 this question 开始,以获取有关“压缩”操作的灵感。使用 AVX512,您可以使用 vcompress 指令来提供帮助:它完全执行此操作,但仅以 32 位或 64 位粒度执行,因此您至少需要对每个向量执行几次。

您也可以尝试垂直方法,在其中加载 N 个向量,然后在它们之间交换,以便第 0 个向量具有最小的元素等。此时,您可以对压缩阶段使用更优化的算法(例如,. 如果您对足够多的向量进行垂直排序,则末尾的向量可能完全以0x00 等开头。

最后,您还可以考虑以不同方式组织数据,无论是在源中还是作为预处理步骤:将始终为 0-3 的“类别”字节与有效负载字节分开。许多处理步骤只需要在一个或另一个上发生,因此您可以通过将它们分开来潜在地提高效率。例如,您可以对所有类别的 32 个字节进行比较操作,然后对 32 个有效负载字节进行压缩操作(至少在每个类别唯一的最后一步中)。

这将导致字节元素数组,而不是 16 位元素,其中“类别”字节是隐含的。您已将数据大小减半,这可能会加快您将来对数据执行的所有其他操作。

如果您无法以这种格式生成源数据,您可以在将有效负载放入正确的存储桶时使用分桶作为删除标记字节的机会,因此输出为uint8_t out[4][2048];。如果您按照 cmets 中的讨论使用pshufb byte-shuffle 执行 SIMD 左包,则可以选择仅将有效负载字节打包到低半部分的 shuffle 控制向量。

(在 AVX512BW 之前,x86 SIMD 没有任何可变控制字混洗,只有字节或双字,因此您已经需要一个字节混洗,它可以在将有效负载字节打包到底部。)

【讨论】:

  • 是的,如果您可以有效地左包装,过滤器和左包装应该适用于 2、3 或 4 个桶。用于 4..16 个存储桶的缓存阻塞多步方法可能很好,但是在某些时候复制数据太多次将不再值得。缓存局部性(只有 2 到 4 个输出流)确实有助于多步左包与指针的单遍直方图。我在评论问题时想到了这个想法的开始,但我认为拒绝了它,因为我在想象更多的桶,并没有想到多通道的想法。
  • 因为在这种情况下我们不需要保留顺序(要求比真正的“过滤”或“压缩”操作弱),vpermd 实际上似乎适用于很多配置,因为您可以将“完整” DWORD 从一个通道交换到另一个通道以尝试填充它并使事物连续,但某些情况下不起作用(例如 5 和 3 个元素)而且无论如何它看起来很昂贵。也许缺乏订单保存也可能导致 LUT 或其他方法的简化,但我没有看到。
  • AVX512BW vpermw 是可变掩码,但在 SKX 上需要多个微指令。关于不保留顺序的有趣点,确实允许解包到 dword、过滤器/左包,然后使用通道内 vpackusdw 或其他东西打包两个结果。此外,同意 OP 应将类别字节与数据字节分开存储。除非您有 AVX512BW,否则子 dword 操作也可能是字节操作。
  • @PeterCordes - 可能存在混淆,我并没有不同意,甚至没有真正提到vpermw(是的,我的意思是在 AVX2 中)。我只是同意你的观点,你不妨使用字节操作(在 AVX2 中),只是说如果你密集地打包有效负载字节,你的 LUT 会爆炸。所以它是关于打包有效负载字节以及它如何导致不同的技术,因为权衡不同(你确实需要字节粒度的随机播放,而不仅仅是使用字节随机播放来随机播放 WORD 元素,所以......大 LUT)。
  • 您始终可以使用movq 加载/比较/pmovmskb 处理 8x1 字节的块。那么你只需要一个 256 * 8 字节的 LUT,并且无需任何额外的指令来扩展它就可以使用它。或pdep。所以更密集的包装不会伤害,它可能没有帮助。
猜你喜欢
  • 1970-01-01
  • 2022-08-16
  • 2017-12-24
  • 1970-01-01
  • 2014-02-09
  • 1970-01-01
  • 2018-07-01
  • 2013-05-31
  • 1970-01-01
相关资源
最近更新 更多