Меняется ли порядок байтов, является ли объединение более эффективным, чем битовые сдвиги?

Меня попросили принять вызов, чтобы изменить порядковый номер типа int. У меня была идея использовать битовые сдвиги

int    swap_endianess(int color)
{
    int a;
    int r;
    int g;
    int b;

    a = (color & (255 << 24)) >> 24;
    r = (color & (255 << 16)) >> 16;
    g = (color & (255 << 8)) >> 8;
    b = (color & 255)
    return (b << 24 | g << 16 | r << 8 | a);
}

Но кто-то сказал мне, что было проще использовать объединение, содержащее int и массив из четырех символов (если int хранится в 4-х символах), заполнить int и затем перевернуть массив.

union   u_color
{
  int   color;
  char  c[4];
};

int             swap_endianess(int color)
{
  union u_color ucol;
  char          tmp;

  ucol.color = color;
  tmp = ucol.c[0];
  ucol.c[0] = ucol.c[3];
  ucol.c[3] = tmp;
  tmp = ucol.c[1];
  ucol.c[1] = ucol.c[2];
  ucol.c[2] = tmp;
  return (ucol.color);
}

Каков более эффективный способ обмена байтами между этими двумя? Есть ли более эффективные способы сделать это?

РЕДАКТИРОВАТЬ

После тестирования на I7, способ объединения занимает около 24 секунд (измеряется с time команда), в то время как способ сдвига битов занимает около 15 секунд на 2 000 000 000 итераций. Дело в том, что если я скомпилирую с -O1, оба метода займут всего 1 секунду, а 0,001 секунды с -O2 или -O3.

Методы битового сдвига компилируются в bswap в ASM с -02 и -03, но не в способе объединения, gcc, похоже, распознает наивный шаблон, но не сложный способ объединения, чтобы это сделать. В заключение прочитайте нижнюю строку @user3386109.

2 ответа

Решение

Вот правильный код для функции замены байтов

uint32_t changeEndianess( uint32_t value )
{
    uint32_t r, g, b, a;

    r = (value >> 24) & 0xff;
    g = (value >> 16) & 0xff;
    b = (value >>  8) & 0xff;
    a =  value        & 0xff;

    return (a << 24) | (b << 16) | (g << 8) | r;
}

Вот функция, которая проверяет функцию замены байтов

void testEndianess( void )
{
    uint32_t value = arc4random();
    uint32_t result = changeEndianess( value );
    printf( "%08x %08x\n", value, result );
}

Используя компилятор LLVM с полной оптимизацией, полученный код сборки для testEndianess функция

0x93d0:  calll  0xc82e                    ; call `arc4random`
0x93d5:  movl   %eax, %ecx                ; copy `value` into register CX
0x93d7:  bswapl %ecx                 ; <--- this is the `changeEndianess` function
0x93d9:  movl   %ecx, 0x8(%esp)           ; put 'result' on the stack
0x93dd:  movl   %eax, 0x4(%esp)           ; put 'value' on the stack
0x93e1:  leal   0x6536(%esi), %eax        ; compute address of the format string
0x93e7:  movl   %eax, (%esp)              ; put the format string on the stack
0x93ea:  calll  0xc864                    ; call 'printf'

Другими словами, компилятор LLVM распознает все changeEndianess функция и реализует его как единый bswapl инструкция.


Примечание для тех, кто интересуется, почему звонок arc4random является необходимым. Учитывая этот код

void testEndianess( void )
{
    uint32_t value = 0x11223344;
    uint32_t result = changeEndianess( value );
    printf( "%08x %08x\n", value, result );
}

компилятор генерирует эту сборку

0x93dc:  leal   0x6524(%eax), %eax        ; compute address of format string 
0x93e2:  movl   %eax, (%esp)              ; put the format string on the stack
0x93e5:  movl   $0x44332211, 0x8(%esp)    ; put 'result' on the stack
0x93ed:  movl   $0x11223344, 0x4(%esp)    ; put 'value' on the stack
0x93f5:  calll  0xc868                    ; call 'printf'

Другими словами, учитывая жестко value в качестве входных данных компилятор предварительно вычисляет result из changeEndianess функция, и помещает это непосредственно в код сборки, полностью обходя функцию.


Суть. Напишите свой код так, как это имеет смысл, и позвольте компилятору выполнить оптимизацию. Компиляторы в наши дни потрясающие. Использование хитрых оптимизаций в исходном коде (например, в союзах) может победить оптимизации, встроенные в компилятор, что на самом деле приведет к более медленному коду.

Вы также можете использовать этот код, который может быть немного более эффективным:

#include <stdint.h>

extern uint32_t
change_endianness(uint32_t x)
{
    x = (x & 0x0000FFFFLU) << 16 | (x & 0xFFFF0000LU) >> 16;
    x = (x & 0x00FF00FFLU) <<  8 | (x & 0xFF00FF00LU) >>  8;
    return (x);
}

Это скомпилировано gcc на amd64 в следующую сборку:

change_endianness:
    roll $16, %edi
    movl %edi, %eax
    andl $16711935, %edi
    andl $-16711936, %eax
    salq $8, %rdi
    sarq $8, %rax
    orl  %edi, %eax
    ret

Чтобы получить еще лучший результат, вы можете использовать встроенную сборку. Архитектуры i386 и amd64 обеспечивают bswap инструкция делать то, что вы хотите. Как объяснил user3386109, компиляторы могут распознавать "наивный" подход и генерировать bswap инструкции, то, что не происходит с подходом сверху. Однако лучше, если компилятор недостаточно умен, чтобы обнаружить, что он может использовать bswap,

Другие вопросы по тегам