【问题标题】:Testing equality between two __m128i variables测试两个 __m128i 变量之间的相等性
【发布时间】:2015-01-08 22:40:09
【问题描述】:

如果我想在两个 __m128i 变量之间进行按位相等测试,我需要使用 SSE 指令还是可以使用 ==?如果不是,我应该使用哪个 SSE 指令?

【问题讨论】:

标签: c x86 sse simd


【解决方案1】:

虽然使用_mm_movemask_epi8 是一种解决方案,但如果您的处理器采用SSE4.1,我认为更好的解决方案是使用在FLAGS 寄存器中设置零或进位标志的指令。 This saves a test or cmp instruction

要做到这一点,你可以这样做:

if(_mm_test_all_ones(_mm_cmpeq_epi8(v1,v2))) {
    //v0 == v1
}

编辑:正如 Paul R 指出的 _mm_test_all_ones 生成两条指令:pcmpeqdptest_mm_cmpeq_epi8 总共是三个指令。这是一个更好的解决方案,总共只使用两条指令:

__m128i neq = _mm_xor_si128(v1,v2);
if(_mm_test_all_zeros(neq,neq)) {
    //v0 == v1
}

这会生成

pxor    %xmm1, %xmm0
ptest   %xmm0, %xmm0

【讨论】:

  • 请注意,_mm_test_all_ones 是一个生成两条指令的宏:_mm_cmpeq_epi32_mm_testc_si128,因此您的解决方案中总共有三个 SSE 指令。不过,将其与 _mm_movemask_epi8 的“old skool”实现进行基准测试会很有趣。
  • @PaulR,好点子,我还没有找到将其归结为两条指令的方法。我觉得现在应该可以了。您的解决方案基本上是 pcmpeqb、pmovmsk、测试。我认为应该可以到 pcmpxxx、ptest。
  • 是的,这应该是可能的——不过,_mm_testX_si128 (PTEST) 的设计似乎存在一个根本缺陷,因为你不能轻易地用它来测试全为 1,因此您总是需要一条额外的指令来在某个点反转所有位。
  • 一句警告:该技巧不适用于浮点向量,因为可能出现 +/-0。
  • ptest 如果您正在分支,则比较结果实际上并没有更快:它是 2 uops 加上 jcc 的 1。 pmovmskb 为 1,cmp/jcc 宏熔断为 1。但在 CPU 上,pxor 可以在比pcmpeqb/w/d/q 更多的端口上运行,这很有趣。
【解决方案2】:

您可以使用比较,然后从比较结果中提取掩码:

__m128i vcmp = _mm_cmpeq_epi8(v0, v1);       // PCMPEQB
uint16_t vmask = _mm_movemask_epi8(vcmp);    // PMOVMSKB
if (vmask == 0xffff)
{
    // v0 == v1
}

这适用于 SSE2 及更高版本。

正如@Zboson 所指出的,如果您有 SSE 4.1,那么您可以这样做,可能会稍微高效一些,因为它是两个 SSE 指令,然后是一个标志测试(ZF ):

__m128i vcmp = _mm_xor_si128(v0, v1);        // PXOR
if (_mm_testz_si128(vcmp, vcmp))             // PTEST (requires SSE 4.1)
{
    // v0 == v1
}

FWIW 我刚刚在 Haswell Core i7 上对这两种实现进行了基准测试,使用 clang 编译测试工具,时序结果非常相似 - SSE4 实现似乎稍微快一些,但很难衡量差异。

【讨论】:

  • 时间可能很棘手。您可能需要展开。 xor 的延迟是 1,ptest 是 2。而 cmpeq 是 1,movemask 3,test,1。所以从延迟的角度来看,SSE4.1 方法大约是延迟的一半。 xor 0.33,ptest 1 的倒数吞吐量,而 cmpeq 是 0.5,movemask 1,test 0.25。对于互惠的吞吐量,它更接近。
  • 我做了一些展开和其他一些事情,看看我是否可以使差异更大,但它仍然相对较小(几个百分比)。编译器(clang)在每种情况下都在测试后生成相同的三条指令,因此除了 3 条 SSE2 指令(每次迭代总共 6 条指令)与 2 条 SSE4 指令(5 条指令)之外,两种情况下展开的指令序列都是相同的每次迭代的总数)。它在两种情况下都在测试后使用 SETE,因此没有分支。我将数据集很好地保存在 L2 缓存中。
  • 感谢您的检查。我想我只是对我的聪明解决方案并没有那么好(还)感到失望。可能您必须找到这样一种情况,即 SSE4 版本的总融合微操作数为 4,但 SSE2 版本超过了 4,或者 SSE2 版本需要两次相同的端口。您可以使用 IACA 进行检查。可能它表明在您当前的两个测试中都没有区别。我的意思是块吞吐量是一样的。因此,您必须找到一个可以发挥作用的测试。这是我唯一能想到的。
  • 好吧,无论如何,pxor 和 ptest 之间存在串行依赖关系,所以我不确定在这种情况下它们是否在同一个端口上是否重要,但无论如何这是一个有用的练习,当然,谁知道未来的架构会用这个做什么。
  • 不客气,如果我知道 IACA,我可能不会在 *.com/questions/25899395/… 上花费 500 代表赏金,​​但我从中学到了很多,所以我很高兴我做到了。
【解决方案3】:

考虑使用 SSE4.1 指令ptest

if(_mm_testc_si128(v0, v1)) {if equal}

else {if not} 

ptest 计算 a 和掩码中 128 位(表示整数数据)的按位与,如果结果为零则返回 1,否则返回 0。

【讨论】:

  • 按位 AND 不测试相等性本身 - 您需要先进行比较,然后测试比较结果。
  • 这肯定是错误的。我今天犯了这个错误。您不使用“testc”测试等效性。 “TestC”首先生成“and-not”,然后检查所有位是否为零。因此,_mm_testc_si128(anything, zero) 将始终返回 true。特别是, testc(anything, zero) 实际上是“零 & (~anything) == 0”,这始终是正确的。 TestC 当然是一个有用的函数,但它不能回答这个特定的问题。如果你想“只”做一个和操作,你应该使用“testz”来代替。