使用AVX-512指令来实现一个比特位反转

项目里面遇到需要把一个45位的bitstring反转一下,就是把第45位和第1位互换,第44位和第2位互换,依次类推。数据是用一个u64的整数来存储的,也就是需要把一个u64整数的低45位做一个比特位反转。

首先想到的就是去Intel的网站上看看有没有什么指令能做这件事,在https://software.intel.com/sites/landingpage/IntrinsicsGuide/上面只找到了_bswap和_bswap64这两个和需求看着有点沾边的指令,其中_bswap是针对32位整数的,_bswap64是针对64位整数的。其中_bswap64的解释如下:

__int64 _bswap64 (__int64 a)
Synopsis
__int64 _bswap64 (__int64 a)
#include <immintrin.h>
Instruction: bswap r64
Description
Reverse the byte order of 64-bit integer a, and store the result in dst. This intrinsic is provided for conversion between little and big endian values.
Operation

dst[7:0] := a[63:56]
dst[15:8] := a[55:48]
dst[23:16] := a[47:40]
dst[31:24] := a[39:32]
dst[39:32] := a[31:24]
dst[47:40] := a[23:16]
dst[55:48] := a[15:8]
dst[63:56] := a[7:0]

一看这个按照字节来反转的,不符合要求,只能继续在网上找。首先就找到了stackoverflow上的一个question https://stackoverflow.com/questions/746171/efficient-algorithm-for-bit-reversal-from-msb-lsb-to-lsb-msb-in-c, 从2009年提出问题,陆陆续续到2020年还有人在回答,看来这个按比特位反转还是一个很common的需求。这question的回答里面有不少的方案,其中有两个比较简单好理解:

首先是一个最简单粗暴的:

uint64_t reverse(const uint64_t n,
                 const uint64_t k)
{
        uint64_t r, i;
        for (r = 0, i = 0; i < k; ++i)
                r |= ((n >> i) & 1) << (k - i - 1);
        return r;
}

这个很简单,也很好理解,就是从最低位开始,把每一位的值放到结果的对应位置上去,code简洁明了,我很喜欢,就是效率不太高,需要做k(45)次循环,而且每次循环需要至少5次运算。

第二种是一个更快一些的方法, 先对每个字节里面的8比特进行反转,然后再把所有的字节一起反转,这里面对每个字节里面的8比特反转的方法比较巧妙:

b = ((b * 0x0802LU & 0x22110LU) | (b * 0x8020LU & 0x88440LU)) * 0x10101LU >> 16;

但是对字节进行反转,原作者用的是移位操作,比较繁琐。结合前面我们发现的_bswap64,我自己写出了下面的第二个处理函数:

uint64_t reverse(const uint64_t n,
                 const uint64_t k)
{
    uint64_t in = n;
    uint8_t *bytes = (uint8_t *)&in;
    for (uint32_t i = 0; i < 8; i++)
    {
        bytes[i] = (bytes[i] * 0x0202020202ULL & 0x010884422010ULL) % 1023;
    }
    return (uint64_t)_bswap64(in)>>(64 - k);
}

这个方法只需要循环8次,每个循环里面也只有3次运算,而且也很好理解。不过在这里我们需要证明一下先对每个字节里面的8比特进行反转没然后再把所有字节一起反转得到的就是全部比特的反转,证明如下:

设比特位r = 8*i + j,其中i是该比特位所在的字节,j是该比特位在字节i里面的偏移,, i, j 的范围都是从0到7。我们先对字节i里面的8比特做反转(j -->7 - j),得到8*i + 7 - j,然后我们对所有的字节做个反转(i -->7 - i),得到8*(7 - i) + 7 - j = 63 - (8*i + j) = 63 - r,的确就是我们想要的全部比特的反转。

这个方法快了很多,但还是需要做8次循环,有没有更快的办法呢?这里想到了前面学习的_mm512_shuffle_epi8指令,一次能对64个数的顺序调整,参看https://blog.csdn.net/zzokest/article/details/117846549

而我们这里刚好是64个比特位,但是我们也看到_mm512_shuffle_epi8指令是把数据分成4组来做的,顺序调整只能在每组之内,所以没法办法做到我们的64个比特位反转,但是可以一次做8个字节的内部比特位反转,也就是方法二里面的8次循环,这样就得到了我们的第三种方法:

uint64_t reverse(const uint64_t n,
                 const uint64_t k)
{
    const __m512i bytes   = _mm512_movm_epi8(in);
    const __m512i mask    = _mm512_set_epi8(8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,
                                       8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,
                                       8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,
                                       8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7);
    const uint64_t rBytes = _mm512_movepi8_mask(_mm512_shuffle_epi8(bytes, mask));
    return _bswap64(in )>>(64 - k);
}

其中指令_mm512_movm_epi8和_mm512_movepi8_mask是相对应的操作,前面的指令是把每一个比特位扩展成一个字节,后面的指令是把一个字节缩小为一个比特位。需要这么两个步骤是因为我们要用到的_mm512_shuffle_epi8指令是针对字节来做顺序调整的。

综上,这第三种使用AVX-512指令的方法应该是比特位反转最快的方法了,只需要4个指令的时间就够了。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值