在x86-64架构的处理器上高效实现128位比特串的互相关

编程语言 2026-07-12

本问题是关于将一个128位的值(Haystack)与另一个1–128位的值(Needle)进行二进制互相关,目的是在x64位模式下的英特尔/AMD x86 CPU上最大化该操作的吞吐量,重点关注它们的SIMD指令。

由于显示宽度限制,以下示例已被简化为32位:

下面是一个Needle包含在Haystack中的示例:Needle contained in the Haystack

下面是一个Needle部分重叠Haystack的示例:
eedle partially overlapping the Haystack

下面是一个与Haystack无相关性的Needle的示例:
Needle that is uncorrelated with the Haystack

以下的函数 CrossCorrelateFwd(),用普通C 语言编写,实现了上面所示的功能。为了与动画匹配并保持简洁且可与所有C 编译器兼容(无论厂商的128位扩展如何),它接收32位参数而不是128位。

它将Needle向 Haystack右滑,找到所有重叠位都匹配的第一次位移。

如果每一对重叠位都匹配,函数返回 k —— Haystack中匹配后缀开始的位置的索引。
如果没有某个位移产生完整的重叠匹配,函数返回 -1

位编号:位0 = MSB(动画中的最左边的位)。
overlapLen = min(NeedleLen, 32 - k)

#include <stdio.h>
#include <stdint.h>

int8_t CrossCorrelateFwd(uint32_t Haystack, uint32_t Needle, uint8_t NeedleLen)
{
    int shift;

    for (shift = 0; shift < 32; shift++)
    {
        int overlapLen = NeedleLen < (32 - shift) ? NeedleLen : (32 - shift);
        int allMatch = 1;
        int j;

        for (j = 0; j < overlapLen; j++)
        {
            int hBit = (int)((Haystack >> (31 - shift - j)) & 1);  // Extract bit at position 'shift + j' (0 = MSB) from a 32-bit value.
            int nBit = (int)((Needle >> (NeedleLen - 1 - j)) & 1); // Extract bit at position 'j' (0 = MSB) from an N-bit value.

            if (hBit != nBit)
            {
                allMatch = 0;
                break;
            }
        }

        if (allMatch)
            return (int8_t)shift;
    }

    return -1;
}

// Boilerplate function to convert binary string into 32 bit binary 
// Only used for this example, not useful to optimize with pmovmskb etc.
value (uint32_t)
static inline uint32_t S2B(const char* s, uint8_t* slen)
{
    uint32_t i = 0;
    const char* p = s;
    while (*p) {
        i <<= 1;
        i += (*p++ - '0');
    }

    if (slen)
        *slen = (uint8_t)(p - s);

    return i;
}


int main()
{
#define Haystack "11001010010010111010111001101011"  //Always 32 bits long.
#define Needle   "0001101"

    uint8_t NeedleLen = 0;
    int8_t idxHay = CrossCorrelateFwd(S2B(Haystack, NULL), S2B(Needle, &NeedleLen), NeedleLen);

    if (idxHay<0)
        printf("\t%s\n\t% *s\n", Haystack, 33 + NeedleLen, Needle);
    else
        printf("\t%s\n%u:\t% *s\n",Haystack, idxHay, idxHay + NeedleLen, Needle );

    return 0;
}

上面的函数可以工作,但速度非常慢。它的参数最多只有32位长,但目标是处理高达128位长的参数(一个XMM寄存器的全部容量)。

我在寻求关于如何使用x86-64 SIMD指令、MSVC intrinsics或 x86-64汇编来优化吞吐量的建议。

也许可以使用SSE4.2的字符串指令,因为这些指令已经在很大程度上实现了 CrossCorrelateFwd() 的功能,但按字节进行(参见 this question),而这里需要的是按位的互相关/搜索。也许可以将这些SSE4.2字符串指令扩展,通过在循环中对位进行移位/轮换8 次来实现按位搜索。

例如,PCMPESTRI指令由于隐式终止处理,展示了一个非常有用的按字节的末端部分匹配行为怪癖。当haystack恰好为128位,并在needle完成之前结束(由于XMM寄存器边界的部分重叠),如果haystack的后缀等于needle的前缀,则指令报告匹配。无论needle的总长度或重叠的程度如何,只要所有数据仍然在1–16字节的XMM寄存器约束内,这一规律就成立。

我也在考虑shift + PXOR + POPCOUNT或 BitScanForward (BSF) 等组合...


我在IvyBridge Xeon上运行(AVX1 + SSE4.2,且没有BMI仅有 popcnt)因此需要一个在这里也快的版本。同样,任何在后续CPU上也快的版本也会有趣。最好能够用MSVC编译。

我的Needle长度真正是任意的——从2 到128位。基准测试显示非匹配率为66%。Haystack的大小始终恰好是128位。

解决方案

你所做的本质上是一个按位子串搜索(互相关),你的代码之所以慢,是因为逐位比较并且使用嵌套循环(在32位示例中最坏情况约32×32次运算,在真实问题中约128×128次)。

在x86-64上加速的关键是以整寄存器(64/128位)而非逐位来工作,使用XOR来检测不匹配,用位掩码仅测试重叠区域。

这单独已经比你的代码快约20–50倍,

#include <stdint.h>
#include <immintrin.h>

int CrossCorrelate128(uint64_t hi, uint64_t lo,
                      uint64_t nhi, uint64_t nlo,
                      int NeedleLen)
{
    for (int shift = 0; shift < 128; shift++)
    {
        int overlap = NeedleLen < (128 - shift) ? NeedleLen : (128 - shift);

        __uint128_t H = ((__uint128_t)hi << 64) | lo;
        __uint128_t N = ((__uint128_t)nhi << 64) | nlo;

        __uint128_t aligned = N << (128 - NeedleLen - shift);

        __uint128_t mask = (((__uint128_t)1 << overlap) - 1)
                         << (128 - shift - overlap);

        if (((H ^ aligned) & mask) == 0)
            return shift;
    }

    return -1;
}

并且这也完全消除了按位提取的需要。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章