Как заставить GCC генерировать команду bswap для магазина с большим эндиантом без встроенных функций?

Я работаю над функцией, которая хранит 64-битное значение в памяти в формате большого конца. Я надеялся, что могу написать переносимый код C99, который работает как на небольших, так и на больших endian-платформах, а современные компиляторы x86 генерируют инструкцию bswap автоматически без встроенных функций или встроенных функций, Поэтому я начал со следующей функции:

#include <stdint.h>

void
encode_bigend_u64(uint64_t value, void *vdest) {
    uint64_t bigend;
    uint8_t *bytes = (uint8_t*)&bigend;
    bytes[0] = value >> 56;
    bytes[1] = value >> 48;
    bytes[2] = value >> 40;
    bytes[3] = value >> 32;
    bytes[4] = value >> 24;
    bytes[5] = value >> 16;
    bytes[6] = value >> 8;
    bytes[7] = value;
    uint64_t *dest = (uint64_t*)vdest;
    *dest = bigend;
}

Это отлично работает для clang, который компилирует эту функцию:

bswapq  %rdi
movq    %rdi, (%rsi)
retq

Но GCC не может обнаружить байтовый обмен. Я попробовал несколько разных подходов, но они только ухудшили ситуацию. Я знаю, что GCC может обнаруживать байтовые свопы, используя побитовое - и, shift, и побитовое - или, но почему это не работает при написании байтов?

Изменить: Я нашел соответствующую ошибку GCC.

Ответ 1

Это похоже на трюк:

void encode_bigend_u64(uint64_t value, void* dest)
{
  value =
      ((value & 0xFF00000000000000u) >> 56u) |
      ((value & 0x00FF000000000000u) >> 40u) |
      ((value & 0x0000FF0000000000u) >> 24u) |
      ((value & 0x000000FF00000000u) >>  8u) |
      ((value & 0x00000000FF000000u) <<  8u) |      
      ((value & 0x0000000000FF0000u) << 24u) |
      ((value & 0x000000000000FF00u) << 40u) |
      ((value & 0x00000000000000FFu) << 56u);
  memcpy(dest, &value, sizeof(uint64_t));
}

clang с -O3

encode_bigend_u64(unsigned long, void*):
        bswapq  %rdi
        movq    %rdi, (%rsi)
        retq

clang с -O3 -march=native

encode_bigend_u64(unsigned long, void*):
        movbeq  %rdi, (%rsi)
        retq

gcc с -O3

encode_bigend_u64(unsigned long, void*):
        bswap   %rdi
        movq    %rdi, (%rsi)
        ret

gcc с -O3 -march=native

encode_bigend_u64(unsigned long, void*):
        movbe   %rdi, (%rsi)
        ret

Протестировано с clang 3.8.0 и gcc 5.3.0 на http://gcc.godbolt.org/ (так что я не знаю точно, какой процессор находится под ним (для -march=native), но я сильно подозреваю недавний процессор x86_64)


Если вы хотите использовать функцию, которая работает и для больших эндианских архитектур, вы можете использовать ответы здесь, чтобы определить контенту системы и добавить if, Как объединение, так и указатель запускают версии и оптимизируются как с помощью gcc, так и clang, что приводит к точной сборке (без ветвей). Полный код в godebolt:

int is_big_endian(void)
{
    union {
        uint32_t i;
        char c[4];
    } bint = {0x01020304};

    return bint.c[0] == 1;
}

void encode_bigend_u64_union(uint64_t value, void* dest)
{
  if (!is_big_endian())
    //...
  memcpy(dest, &value, sizeof(uint64_t));
}

Справочник по настройкам архитектуры Intel® 64 и IA-32 (3-542, том 2A):

MOVBE-Move Data после замены байтов

Выполняет операцию байтового обмена для данных, скопированных со второго операнд (исходный операнд) и сохранить результат в первом операнде (операнд назначения). [...]

Инструкция MOVBE предоставляется для замены байтов на чтение из памяти или записи в память; обеспечивая тем самым поддержку для преобразование малознакомых значений в формат большой буквы и наоборот.

Ответ 2

Все функции в этом ответе с выходом asm на Обозреватель компиляторов Godbolt


GNU C имеет uint64_t __builtin_bswap64 (uint64_t x), так как GNU C 4.3. Это, по-видимому, самый надежный способ заставить gcc/clang генерировать код, который не сосать для этого.

glibc предоставляет htobe64, htole64 и аналогичные хосты для/из функций BE и LE, которые меняются или нет, в зависимости от консистенции машины. См. Документы для <endian.h>. На странице руководства говорится, что они были добавлены в glibc в версии 2.9 (выпущен в 2008-11).

#define _BSD_SOURCE             /* See feature_test_macros(7) */

#include <stdint.h>

#include <endian.h>
// ideal code with clang from 3.0 onwards, probably earlier
// ideal code with gcc from 4.4.7 onwards, probably earlier
uint64_t load_be64_endian_h(const uint64_t *be_src) { return be64toh(*be_src); }
    movq    (%rdi), %rax
    bswap   %rax

void store_be64_endian_h(uint64_t *be_dst, uint64_t data) { *be_dst = htobe64(data); }
    bswap   %rsi
    movq    %rsi, (%rdi)

// check that the compiler understands the data movement and optimizes away a double-conversion (which inline-asm `bswap` wouldn't)
// it does optimize away with gcc 4.9.3 and later, but not with gcc 4.9.0 (2x bswap)
// optimizes away with clang 3.7.0 and later, but not clang 3.6 or earlier (2x bswap)
uint64_t double_convert(uint64_t data) {
  uint64_t tmp;
  store_be64_endian_h(&tmp, data);
  return load_be64_endian_h(&tmp);
}
    movq    %rdi, %rax

Вы безопасно получаете хороший код даже в -O1 из этих функций, и они используют movbe, когда -march установлен в ЦП, поддерживающий этот insn.


Если вы нацеливаете GNU C, но не glibc, вы можете взять определение из glibc (помните, что это код LGPLed):

#ifdef __GNUC__
# if __GNUC_PREREQ (4, 3)

static __inline unsigned int
__bswap_32 (unsigned int __bsx) { return __builtin_bswap32 (__bsx);  }

# elif __GNUC__ >= 2
    // ... some fallback stuff you only need if you're using an ancient gcc version, using inline asm for non-compile-time-constant args
# endif  // gcc version
#endif // __GNUC__

Если вам действительно нужен резерв, который может хорошо компилироваться в компиляторах, которые не поддерживают встроенные GNU C, код из ответа @bolov может быть использован для реализации bswap, который компилируется красиво. Макросы предварительного процессора могут использоваться для выбора, следует ли обменивать или нет (как glibc делает), чтобы реализовать хост-серверы и хост-серверы, to-LE. bswap, используемый glibc, когда __builtin_bswap или x86 asm недоступен, использует идиому с маской и сменой, найденную Боловым, была хорошей. gcc признает это лучше, чем просто переключение.


Код из этот пост-анонимный заголовок кодирования Endian-компиляции компилируется в bswap с помощью gcc, но не с clang, IDK, если есть что-то, что распознают оба распознавателя.

// Note that this is a load, not a store like the code in the question.
uint64_t be64_to_host(unsigned char* data) {
    return
      ((uint64_t)data[7]<<0)  | ((uint64_t)data[6]<<8 ) |
      ((uint64_t)data[5]<<16) | ((uint64_t)data[4]<<24) |
      ((uint64_t)data[3]<<32) | ((uint64_t)data[2]<<40) |
      ((uint64_t)data[1]<<48) | ((uint64_t)data[0]<<56);
}

    ## gcc 5.3 -O3 -march=haswell
    movbe   (%rdi), %rax
    ret

    ## clang 3.8 -O3 -march=haswell
    movzbl  7(%rdi), %eax
    movzbl  6(%rdi), %ecx
    shlq    $8, %rcx
    orq     %rax, %rcx
    ... completely naive implementation

htonll from этот ответ компилируется в два 32bit bswap в сочетании со сдвигом/или. Это отстой, но не страшно ни с gcc, ни с clang.


Мне не повезло с версией кода union { uint64_t a; uint8_t b[8]; } кода OP. clang все еще компилирует его на 64-битный bswap, но я думаю, что компилируется еще хуже код с gcc. (См. Ссылку godbolt).

Ответ 3

Мне нравится решение Питера, но здесь можно использовать еще кое-что на Haswell. У Haswell есть инструкция movbe, которая составляет 3 мопа (не дешевле, чем bswap r64 + обычная загрузка или сохранение), но быстрее на Atom/Silvermont (https://agner.org/optimize/):

// AT&T syntax, compile without -masm=intel
inline
uint64_t load_bigend_u64(uint64_t value)
{
    __asm__ ("movbe %[src], %[dst]"   // x86-64 only
             :  [dst] "=r" (value)
             :  [src] "m" (value)
            );
    return value;
}

Используйте его с чем-то вроде uint64_t tmp = load_bigend_u64(array[i]);

Вы можете изменить это, чтобы сделать функцию store_bigend, или использовать bswap чтобы изменить значение в регистре и позволить компилятору загрузить/сохранить его.


Я изменяю функцию на возвращаемое value потому что выравнивание vdest мне не было ясно.

Обычно функция защищена макросом препроцессора. Я ожидаю, что __MOVBE__ будет использоваться для movbe функции movbe, но его нет (эта машина имеет функцию):

$ gcc -march=native -dM -E - < /dev/null | sort
...
#define __LWP__ 1
#define __LZCNT__ 1
#define __MMX__ 1
#define __MWAITX__ 1
#define __NO_INLINE__ 1
#define __ORDER_BIG_ENDIAN__ 4321
...