Почему оптимизация убивает эту функцию?

Недавно у нас была лекция в университете о специальных программах на нескольких языках.

Лектор записал следующую функцию:

inline u64 Swap_64(u64 x)
{
    u64 tmp;
    (*(u32*)&tmp)       = Swap_32(*(((u32*)&x)+1));
    (*(((u32*)&tmp)+1)) = Swap_32(*(u32*) &x);

    return tmp;
}

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

Он сказал, что все назначения переменной tmp будут оптимизированы компилятором. Но почему это произойдет?

Я понимаю, что существуют ситуации, когда переменные должны быть объявлены волатильными, чтобы компилятор их не трогал, даже если он думает, что они никогда не читаются и не записываются, но я не знаю, почему это произойдет здесь.

Ответ 1

Этот код нарушает строгие правила псевдонимов, что делает его незаконным для доступа к объекту с помощью указателя другого типа, хотя доступ через * char ** разрешен. Компилятору разрешено предположить, что указатели разных типов не указывают на одну и ту же память и соответственно оптимизируют. Это также означает, что код вызывает undefined поведение и может действительно что-то сделать.

Одна из лучших ссылок для этой темы - Понимание строгого сглаживания, и мы можем видеть, что первый пример аналогичен в OP код:

uint32_t swap_words( uint32_t arg )
{
  uint16_t* const sp = (uint16_t*)&arg;
  uint16_t        hi = sp[0];
  uint16_t        lo = sp[1];

  sp[1] = hi;
  sp[0] = lo;

 return (arg);
} 

В статье объясняется, что этот код нарушает правила строгого сглаживания, поскольку sp является псевдонимом arg, но они имеют разные типы и говорят, что, хотя он будет скомпилирован, скорее всего, arg не изменится после того, как swap_words вернется, Хотя с помощью простых тестов я не могу воспроизвести этот результат либо с кодом выше, либо с кодом OP, но это ничего не значит, поскольку это поведение undefined и, следовательно, не предсказуемо.

В статье рассказывается о многих разных случаях и представлено несколько рабочих решений, в том числе тип-punning через объединение, которое хорошо определено в C99 1 и может быть undefined в С++, но на практике поддерживается большинством основных компиляторов, например, здесь ссылка gcc на запись типа. Предыдущая тема Цель союзов на C и С++ входит в подробности. Хотя в этой теме много потоков, это, кажется, делает лучшую работу.

Код для этого решения выглядит следующим образом:

typedef union
{
  uint32_t u32;
  uint16_t u16[2];
} U32;

uint32_t swap_words( uint32_t arg )
{
  U32      in;
  uint16_t lo;
  uint16_t hi;

  in.u32    = arg;
  hi        = in.u16[0];
  lo        = in.u16[1];
  in.u16[0] = lo;
  in.u16[1] = hi;

  return (in.u32);
}

Для справки соответствующий раздел из черновик проекта C99 в строгом псевдониме 6.5 выражает пункт 7, в котором говорится:

Объект должен иметь сохраненное значение, доступ к которому может получить только выражение lvalue, которое имеет один из следующих типов: 76)

- тип, совместимый с эффективным типом объекта,

     

- квалифицированная версия типа, совместимая с эффективным типом объекта,

     

- тип, который является подписанным или неподписанным типом, соответствующим эффективному типу   Объект,

     

- тип, который является подписанным или неподписанным типом, соответствующим квалифицированной версии   эффективный тип объекта,

     

- совокупность или тип объединения, который включает один из вышеупомянутых типов среди его   члены (в том числе, рекурсивно, член субагрегата или объединенного союза) или

     

- тип символа.

и в сноске 76 говорится:

Цель этого списка - указать те обстоятельства, при которых объект может или не может быть сглажен.

а соответствующий раздел из код проекта С++ - 3.10 Lvalues ​​и rvalues ​​para 10

Статья Type-punning and strict-aliasing дает более мягкое, но менее полное введение в тему и C99 revisited дает глубокий анализ C99 и сглаживания и не является легким чтением. Этот ответ Доступ к неактивному члену профсоюза - undefined? перебирает грязные данные о типе-пуринге через объединение в С++ и также не является легким.


Сноска:

  • Цитата comment от Pascal Cuoq: [...] C99, который изначально был неуклюже сформулирован, появлялся, чтобы совершать пушки через союзы undefined. На самом деле, тип-караунд, хотя профсоюзы являются законными на C89, законными на C11, и он был законным на C99 все время, хотя до 2004 года потребовалось, чтобы комитет исправил неправильную формулировку и последующий выпуск TC3. open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm

Ответ 2

В С++ аргументы указателя предполагаются не псевдонимами (кроме char*), если они указывают на принципиально разные типы (правила "строгого сглаживания" ). Это позволяет некоторые оптимизации.

Здесь u64 tmp никогда не изменяется как u64.
Содержание u32* изменено, но может быть не связано с 'u64 tmp', поэтому его можно рассматривать как nop для u64 tmp.

Ответ 3

g++ (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1:

> g++ -Wall -std=c++11 -O0 -o sample sample.cpp

> g++ -Wall -std=c++11 -O3 -o sample sample.cpp
sample.cpp: In function ‘uint64_t Swap_64(uint64_t)’:
sample.cpp:10:19: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
     (*(uint32_t*)&tmp)       = Swap_32(*(((uint32_t*)&x)+1));
                   ^
sample.cpp:11:54: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
     (*(((uint32_t*)&tmp)+1)) = Swap_32(*(uint32_t*) &x);
                                                      ^

Clang 3.4 не предупреждает ни о каком уровне оптимизации, что любопытно...