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

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

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

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 будет оптимизирован компилятором. Но почему это случилось?

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

49

Решение

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

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

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, но это ничего не значит, так как это неопределенное поведение и поэтому не предсказуемо.

В статье рассказывается о многих различных случаях и представлены несколько рабочих решений, включая типа каламбурная через союз, который четко определен в C991 и может быть неопределенным в C ++ но на практике поддерживается большинством основных компиляторов, например здесь Ссылка GCC на тип-наказание. Предыдущая тема Назначение Союзов в C и 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 говорит:

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

и соответствующий раздел из Проект стандарта C ++ является 3.10 Lvalues ​​и rvalues параграф 10

Статья Тип-штамповка и строгое наложение дает более мягкое, но менее полное введение в тему и С99 вновь дает глубокий анализ C99 и псевдонимы и не легкое чтение. Этот ответ на Доступ к неактивному члену объединения — не определено? идет по грязным деталям типа наказания через союз в C ++ и не легкое чтение тоже.


Примечания:

  1. квотирование комментарий Паскаль Куок: […] C99, который был первоначально неуклюже сформулирован, по-видимому, сделал наказание за тип через профсоюзы неопределенными. На самом деле, наказание за профсоюзы является законным в C89, законным в C11, и это было законным в C99 все время, хотя комитету потребовалось до 2004 года, чтобы исправить неправильную формулировку, и последующий выпуск TC3. open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm
41

Другие решения

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

Вот, u64 tmp никогда не изменяется как u64,
Содержание u32* изменен, но может не иметь отношения кu64 tmpтак можно рассматривать как nop за u64 tmp,

47

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 не предупреждает ни на одном уровне оптимизации, что любопытно

10
По вопросам рекламы [email protected]