Конвертируйте uint64_t в байтовый массив в Clang - PullRequest
12 голосов
/ 07 мая 2019

Если вы хотите преобразовать uint64_t в uint8_t[8] (little-endian). На архитектуре с прямым порядком байтов вы можете просто сделать некрасивую 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;
}

Редактировать: После некоторых экспериментов этот код компилируется оптимально с GCC 8.1 и более поздними, если вы используете uint8_t* __restrict__ bytes. Однако мне все еще не удалось найти форму, которую Clang будет оптимизировать.

Ответы [ 4 ]

3 голосов
/ 07 мая 2019

Вот что я мог протестировать, основываясь на обсуждении в комментариях ОП:

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, так и в Clang с -O2).

Компиляция в x86-64 (little-endian) на Clang 8.0.0 ( тест на Godbolt ):

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

Компиляция в aarch64_be (big endian) на Clang 8.0.0 ( тест на Godbolt ):

ldr     x8, [x0]
rev     x8, x8
str     x8, [x1]
ret
2 голосов
/ 13 мая 2019

Как насчет возврата значения?Легко рассуждать о малой сборке:

#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

(стандартный:: массив недоступен для -target aarch64_be?)

2 голосов
/ 13 мая 2019

Прежде всего, причина, по которой ваша оригинальная реализация from не может быть оптимизирована, заключается в том, что вы передаете аргументы по ссылке и по указателю. Таким образом, компилятор должен учитывать возможность того, что оба они указывают на один и тот же адрес (или, по крайней мере, на то, что они перекрываются). Поскольку у вас есть 8 последовательных операций чтения и записи по (потенциально) одному и тому же адресу, правило «как будто» *1003* здесь не может быть применено.

Обратите внимание, что просто удалив & из сигнатуры функции, очевидно, GCC уже рассматривает это как доказательство , что bytes не указывает на x, и, таким образом, это можно безопасно оптимизировать. , Однако для Clang этого недостаточно . Технически, конечно, bytes может указывать на стековую память from (или x), но я думаю, что это будет неопределенное поведение, и поэтому Clang просто пропускает эту оптимизацию.

Ваша реализация to не страдает от этой проблемы, потому что вы реализовали ее таким образом, что сначала вы читаете все значения bytes и , а затем Вы делаете одно большое задание на x. Таким образом, даже если x и bytes указывают на один и тот же адрес, так как вы сначала выполняете все чтение, а затем всю запись (вместо того, чтобы смешивать чтение и запись, как вы делаете в from), это можно оптимизировать.

Ответ Флавио Торибио работает, потому что он делает именно это: сначала читает все значения, а затем записывает их в место назначения.

Однако, есть менее сложные способы достичь этого:

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 с помощью подобного трюка: вместо передачи результата по неконстантной ссылке, используйте более естественный подход и просто верните его из функции:

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 endian и big endian . Обратите внимание, что to и from являются действительно обратными операциями, которые могут быть оптимизированы для запрета операций, если они выполняются одна за другой.

0 голосов
/ 13 мая 2019

Код, который вы дали, слишком сложен.Вы можете заменить его на:

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

Да, это использует Linux-ism htole64(), но если вы находитесь на другой платформе, вы можете легко переопределить это.

Clang и GCCоптимизируйте это идеально, как для платформ с прямым и младшим порядком байтов.

...