在Clang中将uint64_t最佳且方便地转换为字节数组

时间:2019-05-07 12:35:08

标签: c++ clang endianness

如果要将uint64_t转换为uint8_t[8](小端)。在小端架构上,您只能执行丑陋的reinterpret_cast<>memcpy(),例如:

void from_memcpy(const std::uint64_t &x, uint8_t* bytes) {
    std::memcpy(bytes, &x, sizeof(x));
}

这将产生高效的组装:

mov     rax, qword ptr [rdi]
mov     qword ptr [rsi], rax
ret

但是它不是便携式的。在小端机器上,它将具有不同的行为。

要将uint8_t[8]转换为uint64_t,有一个很好的解决方案-只需这样做:

void to(const std::uint8_t* bytes, std::uint64_t &x) {
    x = (std::uint64_t(bytes[0]) << 8*0) |
        (std::uint64_t(bytes[1]) << 8*1) |
        (std::uint64_t(bytes[2]) << 8*2) |
        (std::uint64_t(bytes[3]) << 8*3) |
        (std::uint64_t(bytes[4]) << 8*4) |
        (std::uint64_t(bytes[5]) << 8*5) |
        (std::uint64_t(bytes[6]) << 8*6) |
        (std::uint64_t(bytes[7]) << 8*7);
}

这看起来效率低下,但实际上使用Clang -O2会生成与以前完全相同的程序集,如果在大型字节序计算机上进行编译,它将足够聪明以使用本机字节交换指令。例如。此代码:

void to(const std::uint8_t* bytes, std::uint64_t &x) {
    x = (std::uint64_t(bytes[7]) << 8*0) |
        (std::uint64_t(bytes[6]) << 8*1) |
        (std::uint64_t(bytes[5]) << 8*2) |
        (std::uint64_t(bytes[4]) << 8*3) |
        (std::uint64_t(bytes[3]) << 8*4) |
        (std::uint64_t(bytes[2]) << 8*5) |
        (std::uint64_t(bytes[1]) << 8*6) |
        (std::uint64_t(bytes[0]) << 8*7);
}

编译为:

mov     rax, qword ptr [rdi]
bswap   rax
mov     qword ptr [rsi], rax
ret

我的问题是:是否存在用于反向转换的等效可靠优化的构造?我已经尝试过了,但是天真地编译了它:

void from(const std::uint64_t &x, uint8_t* bytes) {
    bytes[0] = x >> 8*0;
    bytes[1] = x >> 8*1;
    bytes[2] = x >> 8*2;
    bytes[3] = x >> 8*3;
    bytes[4] = x >> 8*4;
    bytes[5] = x >> 8*5;
    bytes[6] = x >> 8*6;
    bytes[7] = x >> 8*7;
}

编辑:经过一些试验,只要您使用uint8_t* __restrict__ bytes,此代码就可以在GCC 8.1和更高版本中得到最佳编译。但是我仍然没有找到Clang会优化的形式。

4 个答案:

答案 0 :(得分:3)

这是我根据OP评论中的讨论可以测试的内容:

void from_optimized(const std::uint64_t &x, std::uint8_t* bytes) {
    std::uint64_t big;
    std::uint8_t* temp = (std::uint8_t*)&big;
    temp[0] = x >> 8*0;
    temp[1] = x >> 8*1;
    temp[2] = x >> 8*2;
    temp[3] = x >> 8*3;
    temp[4] = x >> 8*4;
    temp[5] = x >> 8*5;
    temp[6] = x >> 8*6;
    temp[7] = x >> 8*7;
    std::uint64_t* dest = (std::uint64_t*)bytes;
    *dest = big;
}

这样看起来会使编译器更清楚,并使它假定必要的参数来对其进行优化(在GCC和带有-O2的Clang上)。

在Clang 8.0.0(test on Godbolt)上编译为x86-64(小端):

mov     rax, qword ptr [rdi]
mov     qword ptr [rsi], rax
ret

在Clang 8.0.0(test on Godbolt)上编译为aarch64_be(大端):

ldr     x8, [x0]
rev     x8, x8
str     x8, [x1]
ret

答案 1 :(得分:2)

首先,无法优化原始from实现的原因是因为您要通过引用和指针传递参数。因此,编译器必须考虑它们两者都指向同一地址(或至少它们重叠)的可能性。由于您对(可能)相同的地址进行了8次连续的读取和写入操作,因此as-if rule无法在此处应用。

请注意,仅通过从函数签名中删除&,显然GCC already considers this as proof bytes并不指向x,因此可以安全地对其进行优化。但是,for Clang this is not good enough。 从技术上讲,bytes当然可以指向from的堆栈内存(也称为x),但是我认为这是未定义的行为,因此Clang只是错过了这种优化。

您对to的实现不会遇到此问题,因为您以 first 的方式读取了bytes然后,您对x进行了一项大任务。因此,即使xbytes指向同一个地址,因为您先进行所有阅读,然后进行所有书写(而不是像在from中那样进行读写操作),可以优化。

Flávio Toribio's answer之所以起作用,是因为它正是这样做的:它首先读取所有值,然后才写入目标。

但是,实现这一目标的方法比较简单:

void from(uint64_t x, uint8_t* dest) {
    uint8_t bytes[8];
    bytes[7] = uint8_t(x >> 8*7);
    bytes[6] = uint8_t(x >> 8*6);
    bytes[5] = uint8_t(x >> 8*5);
    bytes[4] = uint8_t(x >> 8*4);
    bytes[3] = uint8_t(x >> 8*3);
    bytes[2] = uint8_t(x >> 8*2);
    bytes[1] = uint8_t(x >> 8*1);
    bytes[0] = uint8_t(x >> 8*0);

    *(uint64_t*)dest = *(uint64_t*)bytes;
}

被编译为

mov     qword ptr [rsi], rdi
ret

在小端上并

rev     x8, x0
str     x8, [x1]
ret

在大端上。

请注意,即使您通过引用传递了x,Clang也可以对其进行优化。但是,这将导致每条指令再增加一条指令:

mov     rax, qword ptr [rdi]
mov     qword ptr [rsi], rax
ret

ldr     x8, [x0]
rev     x8, x8
str     x8, [x1]
ret

分别。

还请注意,您可以使用类似的技巧来改进to的实现:与其通过非const引用传递结果,还可以采用“更自然”的方法,并从函数中将其返回: / p>

uint64_t to(const uint8_t* bytes) {
    return
        (uint64_t(bytes[7]) << 8*7) |
        (uint64_t(bytes[6]) << 8*6) |
        (uint64_t(bytes[5]) << 8*5) |
        (uint64_t(bytes[4]) << 8*4) |
        (uint64_t(bytes[3]) << 8*3) |
        (uint64_t(bytes[2]) << 8*2) |
        (uint64_t(bytes[1]) << 8*1) |
        (uint64_t(bytes[0]) << 8*0);
}

摘要:

  1. 请勿通过引用传递参数。
  2. 首先阅读所有内容,然后再撰写所有内容。

这是little endianbig endian的最佳解决方案。请注意,tofrom是真正的逆运算,如果一个接一个地执行,则可以优化为无操作。

答案 2 :(得分:2)

关于返回值呢? 易于推理和小型组装:

#include <cstdint>
#include <array>

auto to_bytes(std::uint64_t x)
{
    std::array<std::uint8_t, 8> b;
    b[0] = x >> 8*0;
    b[1] = x >> 8*1;
    b[2] = x >> 8*2;
    b[3] = x >> 8*3;
    b[4] = x >> 8*4;
    b[5] = x >> 8*5;
    b[6] = x >> 8*6;
    b[7] = x >> 8*7;
    return b;
}

https://godbolt.org/z/FCroX5

和大字节序:

#include <stdint.h>

struct mybytearray
{
    uint8_t bytes[8];
};

auto to_bytes(uint64_t x)
{
    mybytearray b;
    b.bytes[0] = x >> 8*0;
    b.bytes[1] = x >> 8*1;
    b.bytes[2] = x >> 8*2;
    b.bytes[3] = x >> 8*3;
    b.bytes[4] = x >> 8*4;
    b.bytes[5] = x >> 8*5;
    b.bytes[6] = x >> 8*6;
    b.bytes[7] = x >> 8*7;
    return b;
}

https://godbolt.org/z/WARCqN

(std :: array无法用于-target aarch64_be?)

答案 3 :(得分:0)

您提供的代码过于复杂。您可以将其替换为:

void from(uint64_t x, uint8_t* dest) {
    x = htole64(x);
    std::memcpy(dest, &x, sizeof(x));
}

是的,它使用Linux-ism htole64(),但是如果您在另一个平台上,则可以轻松地重新实现它。

在小端和大端平台上,Clang和GCC可以完美地优化这一点。