popcnt使用硬件指令和查表法

popcnt是“population count”的缩写,该操作一般翻译为“位1计数”,即统计有多少个“为1的位”。例如,十六进制数“FF”,它有8个为1的位,即“popcnt(0xFF) = 8”。popcnt主要应用在密码学与通信安全,例如计算汉明重量(Hamming weight)。

x86体系最初是没有硬件popcnt指令的,只能靠软件计算。2008年底,Intel发布了Nehalem架构的处理器,增加了SSE4.2指令集,其中就有硬件popcnt指令。虽然它名义上是属于SSE4.2指令集,但它并不使用XMM寄存器(SSE的128位寄存器),而是使用GPR寄存器(General-Purpose Registers,通用寄存器)。甚至它的CPUID标志位也不是SSE4.2(CPUID.01H:ECX.SSE4_2[bit 20]),而是专门的POPCNT标志位(CPUID.01H:ECX.POPCNT[bit 23])。

参考[C++] 测试硬件popcnt(位1计数)指令与各种软件算法,利用模板实现静态多态优化性能popcount 算法分析这两篇文章

对比 popcount 的各种算法,高效在于能利用并行计算,去掉循环,使用减法和模运算。
通过减1的循环算法(parse/dense)在知道数只有三五位为1(或0)的话,其实效率也不赖。
查表法的效率挺不错的,如果没有硬件指令的支持,选用这个是可以的。
Hacker's Delight 中的算法,在开源项目中广为引用。

总的来说,硬件指令最快,查表其次,然后是Hacker's Delight里的hacker_popcnt实现

gcc的5.50.6 X86 Built-in Functions中提到

The following built-in functions are changed to generate new SSE4.2 instructions when -msse4.2 is used.

int __builtin_popcount (unsigned int)
Generates the popcntl machine instruction.

int __builtin_popcountl (unsigned long)
Generates the popcntl or popcntq machine instruction, depending on the size of unsigned long.

int __builtin_popcountll (unsigned long long)
Generates the popcntq machine instruction.

但事实上在编译时加入-mpopcnt(同时会定义 __POPCNT__宏)即可让这些函数生成对应的硬件指令。另外也可以在c文件中添加一行#pragma GCC target ("popcnt")来使其生成硬件指令。当没有生成硬件指令的时候,其会调用gcc用软件实现的popcnt函数(该函数是采用了Hacker's Delight这本书里的hacker_popcnt算法)

也就是说gcc的内置popcnt函数对应着硬件指令实现和hacker_popcnt实现。而我想优先使用硬件指令实现,其次是查表法实现。所以就有了如下代码

static inline uint32_t popcnt32(uint32_t v)
{
#ifdef __POPCNT__
    return __builtin_popcount(v);
#else
    static const uint32_t countTable[256] = {
        0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4,
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
        4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8};
    return (countTable[(uint8_t)v] + countTable[(uint8_t)(v >> 8)]) + (countTable[(uint8_t)(v >> 16)] + countTable[(uint8_t)(v >> 24)]);
#endif
}

static inline uint32_t popcnt64(uint64_t v)
{
#ifdef __POPCNT__
    return __builtin_popcountll(v);
#else
    return popcnt32((uint32_t)v) + popcnt32((uint32_t)(v >> 32));
#endif
}

static inline uint32_t popcnt_array(uint32_t *x, uint32_t len)
{
    uint32_t cnt0 = 0, cnt1 = 0, seg = len >> 2, res = len & 0x3;
    uint64_t *X = (uint64_t *)x;

    seg <<= 1;
    for (uint32_t i = 0; i < seg; i += 2)
    {
        cnt0 += popcnt64(X[i]);
        cnt1 += popcnt64(X[i + 1]);
    }

    for (uint32_t i = 1; i <= res; i++)
    {
        cnt0 += popcnt32(x[len - i]);
    }

    return cnt0 + cnt1;
}
posted @ 2020-10-15 10:22  Tifa_Best  阅读(2487)  评论(0编辑  收藏  举报