Об особенностях оптимизации кода в GCC

1 сентября 2011

Сегодня товарищ redp озадачил меня интересным вопросом. Дескать, если современные компиляторы такие умные, то почему GCC не в состоянии преобразовать даже элементарный макрос инверсии байт двойного слова в ассемблерную инструкцию bswap?

Речь идет о коде вроде этого:

#include <stdio.h>
#include <time.h>

typedef unsigned int u32;

#define U8TO32_BE(p) \
  (((u32)((p)[0]) << 24) | \
   ((u32)((p)[1]) << 16) | \
   ((u32)((p)[2]) <<  8) | \
   ((u32)((p)[3])      ))


int main() {
  u32 x = (u32)time(0);
  printf("U8TO32_BE(%08x) = %08x\n", x,
    U8TO32_BE((unsigned char*)&x));
  return 0;
}

Действительно, как Visual Studio 2008, так и GCC 4.6 не в состоянии распознать в макросе U8TO32_BE простую команду bswap. Конечно, можно воспользоваться ассемблерными вставками или нестандартными расширениями языка типа _byteswap_ulong (не знаю, так ли оно называется в GCC), но эти методы плохи тем, что делают код зависимым от конкретного компилятора или архитектуры процессора.

Я переписал программу следующим образом:

#include <stdio.h>
#include <time.h>

typedef unsigned int u32;

#define BSWAP32(x) ( \
  (((x) & 0xFF) << 24) | \
  (((x) & 0xFF00) << 8) | \
  (((x) & 0xFF0000) >> 8) | \
  (((x) & 0xFF000000) >> 24))


int main() {
  u32 x = (u32)time(0);
  printf("BSWAP32(%08x) = %08x\n", x, BSWAP32(x));
  return 0;
}

И посмотрел ассемблерный код, генерируемый GCC:

/usr/local/bin/gcc46 -O2 -march=i686 -S -c bswap.c

Необходимо указать тип процессора, потому что в i386 команды bswap не было. По умолчанию GCC ничего и никак не оптимизирует, потому флаг оптимизации также необходим. В результате получаем файл bswap.s следующего содержания:

  .file "bswap.c"
  .section  .rodata.str1.1,"aMS",@progbits,1
.LC0:
  .string "BSWAP32(%08x) = %08x\n"
  .section  .text.startup,"ax",@progbits
  .p2align 4,,15
  .globl  main
  .type main, @function
main:
.LFB1:
  .cfi_startproc
  pushl %ebp
  .cfi_def_cfa_offset 8
  .cfi_offset 5, -8
  movl  %esp, %ebp
  .cfi_def_cfa_register 5
  andl  $-16, %esp
  subl  $16, %esp
  movl  $0, (%esp)
  call  time
  movl  $.LC0, (%esp)
  movl  %eax, %edx
  bswap %edx
  movl  %eax, 4(%esp)
  movl  %edx, 8(%esp)
  call  printf
  xorl  %eax, %eax
  leave
  .cfi_restore 5
  .cfi_def_cfa 4, 4
  ret
  .cfi_endproc
.LFE1:
  .size main, .-main
  .ident  "GCC: 4.6.2 20110729 (prerelease)"

Как видите, bswap появился. Что интересно, GCC 4.2 (который вышел в 2008-м году) так не умеет.

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

PS. А еще, разбираясь с GCC и тем, как он оптимизирует код, я открыл для себя отладчик kgdb (оболочка для gdb). Вполне годная штука, как оказалась. Это к вопросу о недостатке отладочных средств под UNIX.

Отладчик kgdb

Дополнение: Вспомнилось мудрая фраза, что преждевременная оптимизация — корень всех зол. Утверждается, что оптимизация с bswap может ускорить алгоритм BLAKE аж на целых 5%. Но простите, а вы уверены, что операция чтения с диска не сводит на нет эту оптимизацию при хэшировании файлов? Не лучше ли сначала получить (1) рабочее, (2) легкое в сопровождении и (3) переносимое приложение, а уже потом заниматься оптимизацией настоящих ее узких мест, если это требуется (вспоминаем правило 80/20)?

Метки: , , .

Подпишись через RSS, E-Mail, Google+, Facebook, Vk или Twitter!

Понравился пост? Поделись с другими: