Недавно в университете мы читали лекцию о специальностях программирования на нескольких языках.
Лектор записал следующую функцию:
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
будет оптимизирован компилятором. Но почему это случилось?
Я понимаю, что существуют обстоятельства, когда переменные должны быть объявлены летучий так что компилятор не трогает их, даже если он думает, что они никогда не читаются и не пишутся, но я не знаю, почему это произошло бы здесь.
Этот код нарушает строгие правила алиасинга что делает незаконным доступ к объект через указатель другого типа, хотя доступ через * 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 ++ и не легкое чтение тоже.
Примечания:
В C ++ аргументы указателя предполагаются не псевдонимами (кроме char*
) если они указывают на принципиально разные типы (правила «строгого алиасинга»). Это позволяет некоторые оптимизации.
Вот, u64 tmp
никогда не изменяется как u64
,
Содержание u32*
изменен, но может не иметь отношения кu64 tmp
так можно рассматривать как nop
за u64 tmp
,
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 не предупреждает ни на одном уровне оптимизации, что любопытно…