【问题标题】:Efficiently bitshifting bytes in 32/64 bit quantities?以 32/64 位有效地移位字节?
【发布时间】:2016-05-07 02:29:51
【问题描述】:

为简单起见,假设我使用的是 32 位 little-endian 处理器并声明了以下 4 字节缓冲区:

unsigned char buffer[] = { 0xab, 0xcd, 0xef, 0x46 };

假设我的目标是将缓冲区中的每个字节按位左移 4 位。也就是说,我想将缓冲区值转换为: { 0xbc, 0xde, 0xf4, 0x60 }。要执行这样的转换,可以编写如下代码:

for (int i = 0; i < 3; ++i)
{
  buffer[i] <<= 4; 
  buffer[i] |= (buffer[i + 1] >> 4);
}
buffer[3] <<= 4;

虽然这可行,但我更愿意使用处理器的原生 32 位寄存器同时移动所有 4 个字节:

unsigned char buffer[] = { 0xab, 0xcd, 0xef, 0x46 };
unsigned int *p = (unsigned int*)buffer; // unsigned int is 32 bit on my platform
*p <<= 4;

上面的 sn-p 成功地执行了一个转变,但不是我正在寻找的方式。看来,由于我将缓冲区转换为无符号整数,因此使用值 0x46efcdab(而不是 0xabcdef46)加载(小端序)寄存器。因此,执行 4 位左移会导致 0xb0dafc6e 而不是 0xbcdef460

除了在移位之前交换字节(例如htonl 等),还有什么技巧可以按照我正在寻求的方式有效地移位字节吗?

提前感谢您的见解。

【问题讨论】:

  • @user3386109,是的,它是 x86
  • 那我同意 nneonneo 的回答。编译器会将htonl 实现为一条指令(在适当的优化级别)。不知道最低限度是多少,但-O3 绝对有效。
  • 如果你使用了 C 提供的固定类型,你可以避免一大堆假设 unsigned 在你的平台上有多宽,等等。参见 stdint.h,它们就是这样为了。如果你想序列化数据,使用适当的位移/屏蔽,而不是强制转换,你的代码会调用未定义的行为..
  • @Olaf,您能否详细说明我的代码如何调用未定义的行为?
  • 对于初学者来说,它可能会出现对齐问题。而且你违反了有效类型规则(又名严格别名)。

标签: c bit-manipulation bit-shift


【解决方案1】:

使用htonl/ntohlnetwork(big-endian)字节顺序和native字节顺序之间翻转:

uint32_t *p = (uint32_t*)buffer;
*p = htonl(ntohl(*p) << 4);

实际上,这会将缓冲区内容以大端顺序加载为整数,执行移位,然后以大端顺序将其写回。

这会在 x86 上编译成几个 bswap 指令,因此它应该相当高效 (gcc -O3)。


这里有一些测试代码(buffer 是全局的以避免常量折叠,return 可以防止死代码消除):

#include <stdint.h>    // uint32_t
#include <arpa/inet.h> // ntohl, htonl

unsigned char buffer[] = { 0xab, 0xcd, 0xef, 0x46 };

int main() {
    uint32_t *p = (uint32_t*)buffer; // unsigned int is 32 bit on my platform
    *p = htonl(ntohl(*p) << 4);
    return *p;
}

这会编译成以下相当简单的机器代码(x86-64;LLVM 7.0.2;cc -O2):

0000000000000000    pushq   %rbp           ; frame setup
0000000000000001    movq    %rsp, %rbp     ; frame setup
0000000000000004    movl    (%rip), %eax   ; load buffer
000000000000000a    bswapl  %eax           ; endian flip
000000000000000c    shll    $0x4, %eax     ; shift
000000000000000f    bswapl  %eax           ; endian flip
0000000000000011    movl    %eax, (%rip)   ; save buffer
0000000000000017    popq    %rbp           ; finish
0000000000000018    retq

【讨论】:

  • 如果有足够高的march 选项(如atom 或haswell),它将发出movbe 而不是mov+bswap
  • 感谢您指出 htonl/ntohl 实际上并不调用调用指令
【解决方案2】:

只是为了比较,你可以不使用htonl/ntohl来做到这一点。这假设一个 little-endian CPU:

#include <stdint.h>

void lshift(unsigned char* buf) {
  uint32_t* p = (uint32_t*)buf;
  uint32_t lo = *p & 0x0F0F0F0F;
  uint32_t hi = *p & 0xF0F0F000;
  *p = (lo << 4) | (hi >> 12);
}

以及生成的带有gcc -O3的程序集:

pushq   %rbp
movq    %rsp, %rbp
movl    (%rdi), %eax
movl    %eax, %ecx
shll    $4, %ecx
andl    $-252645136, %ecx       ## imm = 0xFFFFFFFFF0F0F0F0
shrl    $12, %eax
andl    $986895, %eax           ## imm = 0xF0F0F
orl     %ecx, %eax
movl    %eax, (%rdi)
popq    %rbp
retq

根据bswapl 的周期数,它可能是更快的替代方案。

【讨论】:

  • 感谢发布仅使用位运算符的解决方案
猜你喜欢
  • 1970-01-01
  • 2014-08-13
  • 1970-01-01
  • 2012-10-26
  • 1970-01-01
  • 2011-03-29
  • 2011-04-23
  • 2016-09-14
  • 2014-09-22
相关资源
最近更新 更多