如何使用 SIMD 比较两个字符向量并将结果存储为浮点数?

Posted

技术标签:

【中文标题】如何使用 SIMD 比较两个字符向量并将结果存储为浮点数?【英文标题】:How to compare two char vectors using SIMD and store the result as floats? 【发布时间】:2016-05-23 23:30:06 【问题描述】:

目标:使用最少数量的算术运算(即每个mask1 AND mask2)识别内在函数以转换 4 个布尔“uint8_t”。

更新:为了优化代码,我在 C++ 中使用 SIMD。与Loading 8 chars from memory into an __m256 variable as packed single precision floats 相比,目标是处理/支持大规模数组的掩码。后者使用“内部”掩码属性(“https://software.intel.com/sites/landingpage/IntrinsicsGuide/#expand=10,13”)进行示例:

  uint8_t mask1[4] = 0, 1, 1, 0;  uint8_t mask2[4] = 1, 1, 0, 0; float data[4] = 5, 4, 2, 1;
   //! Naive code which works:                                                                                                                                                                                 
    float sum = 0;
    for(int i = 0; i < 4; i++) 
      if(mask1[i] && mask2[i]) sum += data[i];
    
  

从上面我们观察到掩码的使用与简单的算术相结合:尽管上述一组操作由优化的算术支持,但“内部”有几个弱点:(a)限制操作的数量和(b)放置对更新编译器的要求(并非总是如此)。

上下文: 挑战涉及从“char”数据类型到“float”数据类型的转换。为了演示我的代码中的错误,这里有一个简短的摘录:

//! Setup, a setup which is wrong as mask1 and mask2 are chars and not floats.
#include <emmintrin.h>
#include <x86intrin.h>                                                               

char mask1[4] = 0, 1, 0, 1;
char mask2[4] = 1, 0, 0, 1;
const int j = 0;

//! The logics, which is expected to work correct for flroats, ie, not chars.
const __m128 vec_empty_empty = _mm_set1_ps(0);              
const __m128 vec_empty_ones = _mm_set1_ps(1);
const __m128 term1  = _mm_load_ps(&rmul1[j2]); 
const __m128 term2  = mm_load_ps(&rmul2[j2]);
__m128 vec_cmp_1 = _mm_cmplt_ps(term1, vec_empty_empty); 
__m128 vec_cmp_2 = _mm_cmplt_ps(term2, vec_empty_empty); 

//! Intersect the values: included to allow other 'empty values' than '1'.
vec_cmp_1 =  _mm_and_ps(vec_cmp_1, vec_empty_ones);
vec_cmp_2 = _mm_and_ps(vec_cmp_2, vec_empty_ones);

//! Seperately for each 'cell' find the '1's which are in both:
__m128 mask = _mm_and_ps(vec_cmp_1, vec_cmp_2); 

上面的结果将用于与浮点向量float arr[4] 相交(即相乘)。因此,如果有人对如何将 SIMD 字符向量转换为浮点 SIMD 向量有任何建议,我将不胜感激! ;)

【问题讨论】:

您能否提供一个非 simd mcve,包括您想要实现的目标和预期输出? Loading 8 chars from memory into an __m256 variable as packed single precision floats的可能重复 感谢您的回答:wrt。 @Pixelchemist 我现在已经把答案更详细了。 写。 @PeterCordes 的建议提到的建议仅描述标量操作,即不包括使用基于向量的优化:简而言之,该建议导致超过 2 倍的性能问题(与非屏蔽的后继方案相比)。 好吧,这与我的问题不同。更新了我的答案。 【参考方案1】:

使用 SSE4.1 pmovsxbdpmovzxbd 将 4 字节的块进行符号或零扩展为 32 位整数元素的 16B 向量。

请注意,using pmovzxbd (_mm_cvtepu8_epi32) as a load 似乎不可能既安全又高效地编写,因为没有具有更窄内存操作数的内在函数。 (更新:一些现代编译器能够将像_mm_loadu_si32 这样的窄负载折叠到pmovzx 的内存源操作数中,例如clang 但不是GCC:https://godbolt.org/z/KPxboPecr)

要进行比较部分,请使用pcmpeqd 生成元素中全零或全一的掩码(即-1)。用它来屏蔽 FP 数据的向量。 (全零是0.0在IEEE浮点数中的位表示,0.0是加法标识。)


如果您的元素始终只有 0 或 1,您可以使用 uint32_t 来保存所有四个字节,并使用标量 AND(C 的 &amp; 运算符)作为所有四个 mask1[i] &amp;&amp; mask2[i] 检查的 SWAR 实现。将该整数放入一个向量和pmovsxbd。如果您的元素实际上是 0 和 -1(全一),这会更好,否则您需要一个额外的步骤来获得矢量掩码。 (例如,针对全零向量的 pcmpeqb)。

如果您不能使用-1 而不是1,那么您最好的选择可能仍然是将两个掩码解压缩为32 位元素和pcmpeqd

总体思路是:

          // mask1 = _mm_loadu_si32(something)  // movd load if necessary
__m128i m1vec = _mm_cvtepi8_epi32(mask1);         // where mask1 has to be a __m128i vector already, not a 4byte memory location.
__m128i m2vec = _mm_cvtepi8_epi32(mask2);         // pmovsx

// sign-extension turns each 0 or -1 byte into a 0 or -1 dword (32bit) element

__m128i mask = _mm_and_si128(mask1, mask2);
// convert from 0/1 to 0/-1 if necessary.  I'm assuming the simple case.

__m128 masked_floats = _mm_and_ps(floats, _mm_castsi128_ps(mask));   // 0.0 or original value

sum = _mm_add_ps(sum, masked_floats);

如果掩码元素可以是 0 / -1 以外的值,您可能需要使用 _mm_cmpeq_epi32(m1vec, _mm_setzero_si128()) 或其他东西分别对它们进行布尔化。 (这会将非零变为零,反之亦然)

请参阅x86 标签 wiki 获取链接,尤其是。 https://software.intel.com/sites/landingpage/IntrinsicsGuide/

【讨论】:

以上是关于如何使用 SIMD 比较两个字符向量并将结果存储为浮点数?的主要内容,如果未能解决你的问题,请参考以下文章

连续迭代器上的 SIMD 指令

计算两个 _m128i SIMD 向量之间的匹配字节数

使用 SIMD 根据另一个向量位值计算值的乘积

带有 Altivec 的 SIMD:为啥将两个向量相乘比相加两个向量更快?

使用 GCC 向量扩展存储、修改和检索字符串?

NASM ctypes SIMD - 如何访问返回到ctypes的128位数组?