有没有办法根据编译时未知的掩码长度来掩码 __m128i 寄存器的一端?
Posted
技术标签:
【中文标题】有没有办法根据编译时未知的掩码长度来掩码 __m128i 寄存器的一端?【英文标题】:Is there a way to mask one end of a __m128i register based on mask length that is not known at compile time? 【发布时间】:2020-12-07 17:11:31 【问题描述】:我有一个看似简单的问题。将字符串加载到 __m128i 寄存器中(使用 _mm_loadu_si128),然后找到字符串的长度(使用 _mm_cmpistri)。现在,假设长度小于 16,我希望在第一个字符串结尾之后只有零,零。实现这一点的一种方法是将“len”字节复制到另一个寄存器,或者将长度为 8 * len 的 1 的掩码复制到原始寄存器。但是要找到创建这种仅依赖于计算长度的掩码的简单方法并不容易。
【问题讨论】:
pcmpeqb
/ pmovmskb
/ tzcnt
会给您位置,然后您可以使用它将滑动窗口索引到 0xff, ..., 0 ...
的缓冲区或类似的东西以获得 AND面具。例如Vectorizing with unaligned buffers: using VMASKMOVPS: generating a mask from a misalignment count? Or not using that insn at all
@PeterCordes 感谢您的回答,我仍在尝试解码。您是在说“使用其他指令而不是 cmpistri 来查找 0”吗?切题:这些 SSE4.2 内在函数在为 -m32 编译时可用吗?
是的,pcmpistri
不是一个特别快的指令,尽管它还不错。 SSE2 pcmpeqb 对零将是正常的方式。但是,是的,SSE4.2 指令/内在函数在 32 位模式下可用,您可以使用来自 pcmpistri
的整数结果,而不是对比较掩码结果进行位扫描;因为它在 Skylake 上只花费 3 微指令(但全部用于端口 0),它实际上是不错的(uops.info)。但是延迟高。与往常一样,您必须使用 -march=nehalem
或具有它们的东西进行编译,或者手动启用才能使用 C 内部函数。
我明白了,但 _mm_crc32_u64 似乎不适用于 m32。 (为 IvyBridge 编译)
@JacekAmbroziak 我认为this 是正确的(不使用pcmpistri
)但可以实现您的既定目标。如果您知道 __m128i
包含一个零,那么如果有一个技巧可以仅使用 SIMD 指令创建 __m128i not_zero
掩码,那么它也可能会得到改进。
【参考方案1】:
我会这样做。未经测试。
// Load 16 bytes and propagate the first zero towards the end of the register
inline __m128i loadNullTerminated( const char* pointer )
// Load 16 bytes
const __m128i chars = _mm_loadu_si128( ( const __m128i* )pointer );
const __m128i zero = _mm_setzero_si128();
// 0xFF for bytes that were '\0', 0 otherwise
__m128i zeroBytes = _mm_cmpeq_epi8( chars, zero );
// If you have long strings and expect most calls to not have any zeros, uncomment the line below.
// You can return a flag to the caller, to know when to stop.
// if( _mm_testz_si128( zeroBytes, zeroBytes ) ) return chars;
// Propagate the first "0xFF" byte towards the end of the register.
// Following 8 instructions are fast, 1 cycle latency/each.
// Pretty sure _mm_movemask_epi8 / _BitScanForward / _mm_loadu_si128 is slightly slower even when the mask is in L1D
zeroBytes = _mm_or_si128( zeroBytes, _mm_slli_si128( zeroBytes, 1 ) );
zeroBytes = _mm_or_si128( zeroBytes, _mm_slli_si128( zeroBytes, 2 ) );
zeroBytes = _mm_or_si128( zeroBytes, _mm_slli_si128( zeroBytes, 4 ) );
zeroBytes = _mm_or_si128( zeroBytes, _mm_slli_si128( zeroBytes, 8 ) );
// Now apply that mask
return _mm_andnot_si128( zeroBytes, chars );
更新:这是另一个版本,使用了 Noah 关于 int64 -1
指令的想法。
可能会快一些。 Disassembly.
__m128i loadNullTerminated_v2( const char* pointer )
// Load 16 bytes
const __m128i chars = _mm_loadu_si128( ( const __m128i* )pointer );
const __m128i zero = _mm_setzero_si128();
// 0xFF for bytes that were '\0', 0 otherwise
const __m128i zeroBytes = _mm_cmpeq_epi8( chars, zero );
// If you have long strings and expect most calls to not have any zeros, uncomment the line below.
// You can return a flag to the caller, to know when to stop.
// if( _mm_testz_si128( eq_zero, eq_zero ) ) return chars;
// Using the fact that v-1 == v+(-1), and -1 has all bits set
const __m128i ones = _mm_cmpeq_epi8( zero, zero );
__m128i mask = _mm_add_epi64( zeroBytes, ones );
// This instruction makes a mask filled with lowest valid bytes in each 64-bit lane
mask = _mm_andnot_si128( zeroBytes, mask );
// Now need to propagate across 64-bit lanes
// ULLONG_MAX if there were no zeros in the corresponding 8-byte long pieces of the string
__m128i crossLaneMask = _mm_cmpeq_epi64( zeroBytes, zero );
// Move the lower 64-bit lanes of noZeroes64 into higher position
crossLaneMask = _mm_unpacklo_epi64( mask, crossLaneMask );
// Update the mask.
// Lower 8 bytes will not change because _mm_unpacklo_epi64 copied that part from the mask.
// However, upper lane may become zeroed out.
// Happens when _mm_cmpeq_epi64 detected at least 1 '\0' in any of the first 8 characters.
mask = _mm_and_si128( mask, crossLaneMask );
// Apply that mask
return _mm_and_si128( mask, chars );
【讨论】:
pslldq/por
依赖链的 8 个周期可能比 pmovmskb / bsf / load 的延迟低 1 或 2 个周期。但是,这些操作需要 8 微指令,如果您没有用于非破坏性复制和转移的 AVX,则另外需要 4 movdqa 微指令。 (您可以使用_mm_shuffle_epi32
复制和移位 4 和 8 字节粒度以避免这种情况,因为可以保持低 4 或 8 字节不变,而不是移入零来提供 OR)跨度>
另外,也许我们可以在这里做一些更聪明的事情:(SRC-1) XOR (SRC)
(如 blsmsk)可以设置 qword 中最低设置位以下的所有位,如果我们使用 psubq
(或 @987654329 @ 带 -1)。如果我们可以从两个 8 字节的一半扩展为一个 16 字节的向量,我们就可以用它进行与操作。
@PeterCordes 确实,这是一个权衡。根据我的经验,不访问内存而是计算更多指令的代码总体上会稍微快一些。尤其是一旦集成到更大的系统中:微基准对 L1D 缓存没有任何其他用途。
是的,对于每个较大的操作使用一次的东西,避免内存可能是最好的。对于每个内部循环使用一次的东西(而不是在延迟关键路径上),L1d 未命中可以在多次迭代中分期偿还。权衡的明智选择取决于用例。 SIMD 代码需要 一些 个向量常量是正常的,一个 32 字节的表相当于 2 个常量(负载可能在关键路径上除外)。顺便说一句,这使得@Noah 的解决方案再好不过了。您可以通过 63 廉价生成 1,1
和 pcmpeqd
/ psrlq
和 0,1
,但编译器将 constprop&load
@Noah 您最近发布的解决方案有一个错误。它分别屏蔽 __m128i 的低位和高位。 4a,61,63,65,6b,00,00,00,63,65,6b,20,00,00,00,00
【参考方案2】:
static const __m128i ZERO =
_MM_SETR_EPI32(0u, 0u, 0u, 0u);
static const __m128i INDEXES =
_MM_SETR_EPI8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15);
static const __m128i ONES = _MM_SETR_EPI32(0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF);
_Alignas(32) static unsigned char MASK_SOURCE[32] =
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF;
static __m128i mask_string1(__m128i input, uint32_t *const plen)
const __m128i zeros = _mm_cmpeq_epi8(input, ZERO);
if (_mm_testz_si128(zeros, zeros))
*plen = 16;
return input;
else
const uint32_t length = _tzcnt_u32(_mm_movemask_epi8(zeros));
*plen = length;
return
length < 15 ?
_mm_and_si128(input, _mm_loadu_si128((__m128i_u *) (MASK_SOURCE + (16 - length)))) :
input;
static __m128i mask_string2(__m128i input, uint32_t *const plen)
__m128i zeros = _mm_cmpeq_epi8(input, ZERO);
if (_mm_testz_si128(zeros, zeros))
*plen = 16;
return input;
else
const uint32_t length = _tzcnt_u32(_mm_movemask_epi8(zeros));
*plen = length;
if (length < 15)
zeros = _mm_or_si128(zeros, _mm_slli_si128(zeros, 1));
zeros = _mm_or_si128(zeros, _mm_slli_si128(zeros, 2));
zeros = _mm_or_si128(zeros, _mm_slli_si128(zeros, 4));
zeros = _mm_or_si128(zeros, _mm_slli_si128(zeros, 8));
// Now apply that mask
return _mm_andnot_si128(zeros, input);
else
return input;
static __m128i mask_string3(__m128i input, uint32_t *const plen)
const __m128i zeros = _mm_cmpeq_epi8(input, ZERO);
if (_mm_testz_si128(zeros, zeros))
*plen = 16;
return input;
else
const uint32_t length = _tzcnt_u32(_mm_movemask_epi8(zeros));
*plen = length;
return
length < 15 ?
_mm_andnot_si128(_mm_cmpgt_epi8(INDEXES, _mm_set1_epi8(length)), input) :
input;
__m128i set_zeros_3(__m128i v, uint32_t *plen)
// cmp zeros
__m128i eq_zero = _mm_cmpeq_epi8(ZERO, v);
if (_mm_testz_si128(eq_zero, eq_zero))
*plen = 16;
return v;
else
*plen = _tzcnt_u32(_mm_movemask_epi8(eq_zero));
#ifdef COND
if (_mm_testz_si128(eq_zero, eq_zero))
return;
#endif
__m128i eq_zero64 = _mm_cmpeq_epi64(eq_zero, ZERO);
__m128i mask64_1 = _mm_unpacklo_epi64(ONES, eq_zero64);
// add(-1) / sub(1)
__m128i partial_mask = _mm_add_epi64(eq_zero, ONES);
#if defined __AVX512F__ && defined __AVX512VL__
__m128i result =
_mm_ternarylogic_epi64(partial_mask, mask64_1, v, (1 << 7));
#else
__m128i mask = _mm_and_si128(mask64_1, partial_mask);
__m128i result = _mm_and_si128(mask, v);
#endif
return result;
【讨论】:
我已经发布了 4 种不同方法的代码,用于屏蔽加载到 16 字节向量中的字符串。我们感兴趣的是 1) 字符串长度,2) 清理后的字符串(终止 0 后没有随机字节)。当输入字符串超过 15 个字符时,向量中不会有 0。在这种情况下,我们需要遍历字符串,直到遇到零。但是对于长度为 [1, 15] 的字符串,我们将学习该长度并且还将清理字符串。这些方法来自 Peter Cordes、Soonts 和 Noah。 您最好将_mm_movemask_epi8
的输出分配给一个变量并在其上测试0。 _mm_testz_si128
的延迟为 3 微秒,与 _mm_movemask_epi8
+ testl; jz
相同。 _mm_movemask_epi8
在端口 0 上,因此它采用了一个执行单元,否则该单元将朝着关键路径依赖链工作。
我这样做了,但没有观察到最终时间的任何差异。所有方法都执行“相同”,这是一个有点令人沮丧的结果。所有方法都运作良好,选择纯粹是“审美”。我觉得这很奇怪。
如果绝大多数时间都不是 0,那么您可能只是每次都在点击 if 语句。我看到的最好结果(以明显的优势)是this(基本上所有测试数据,除了没有0的情况下,分支有帮助)。
@Noah 事实上,在我的 4.82 亿个字符串文件中,大约 77% 的字符串长度小于 16 个字符。这真的很棒,因为只需第一批 16 个字符就可以处理很多。在当前的速度测试中,我什至不做任何循环,只处理第一批。我想知道我们是否可以从这场比赛没有获胜者的事实中学到一些重要的东西。如果您想自己运行速度测试,请成为我的客人(我可以在 github 上发布)。目前还不确定这是否值得任何人花时间。以上是关于有没有办法根据编译时未知的掩码长度来掩码 __m128i 寄存器的一端?的主要内容,如果未能解决你的问题,请参考以下文章