Страницы

Поиск по вопросам

вторник, 2 октября 2018 г.

Что такое strict aliasing?

Часто говорят, что тот или иной код невалиден, так как он нарушает «strict aliasing». А что это такое?


Ответ

Aliasing (псевдонимы/наложение/алиасинг) - это ситуация, когда два разных имени (например указателя) обозначают один и тот же объект.
int x; int* p = &x; int& r = x; // алиасинг: x, r и *p обозначают один и тот же объект.
Это важно для оптимизатора: если есть два однотипных указателя, то после записи в один указатель, значение по другому указателю может измениться:
int f(int* a, int* b) { *a = 0; *b = 1; return *a; // в *a может быть как 0 так и 1, // оптимизатор не может использовать return 0 }
Strict aliasing (строгий алиасинг) - это неофициальное название правила, согласно которому алиасинг запрещен для разнотипных объектов. В стандарте С++ это правило звучит следующим образом:
3.10 Lvalues and rvalues [basic.lval] параграф 10: Если программа пытается получить доступ ко значению объекта через glvalue типа, который не перечислен в списке ниже, то поведение не определено: динамический тип этого объекта, в т.ч. с добавлением const/volatile или signed/unsigned тип, похожий на динамический тип объекта (например const int* похож на int*, см. 4.4. [conv.qual]); агрегатный тип (массив или класс или union), который включает в себя член данных с одним из типов, указанных выше; базовый тип динамического типа объекта (в т.ч. с добавлением const/volatile); char или unsigned char (Сноска: цель этого списка - указать случаи, когда алиасинг разрешен) Объект - это область памяти. У него есть время жизни, тип и может быть имя. Динамический тип - это тип наиболее унаследованого объекта, на который указывает выражение. Например если D наследуется от B, и есть переменная B* b = new D;, то динамический тип *b - это D
Из этого следует, что хотя указатель на один тип можно преобразовать в указатель на другой тип, из получившегося указателя нельзя ничего читать:
char* pc = new char[100]; int* pi = reinterpret_cast(pc); // OK, просто каст int i = *pi; // ЗАПРЕЩЕНО: динамический тип это char, а читается int
Оптимизатор может использовать это следующим образом:
int f(int* a, short* b) { *a = 0; *b = 1; return *a; // в *a может быть только 0, // у *b другой тип, по этому запись в *b не может менять *a // оптимизатор может изменить код на return 0 }
Пункт про массивы и классы означает, что к объекту можно обратиться через объект в котором он находится, например:
struct S { int a; };
S s; s.a = 1; S s_ = s; // Доступ к S::a через весь объект с типом S (довольно очевидно)
При этом, код с использованием другого типа не валиден не из-за strict aliasing, а из-за попытки разыменования указателя, полученного в результате reinterpret_cast. Стандарт разрешает только обратное преобразование (впрочем, тут используется термин unspecified, так что компилятор может использовать свои правила).
struct S2 { int a; }; S2* s2 = reinterpret_cast(s); int a = s2->a; // разыменование результата reinterpret_cast // при этом тип s2->a это int, так что strict aliasing не нарушен
Для union определено понятие активного члена данных, поэтому чтение другого члена нарушает strict aliasing:
union U { int i; short s; char c; }; U u; u.i = 0; // активный член short s = u.s; // ЗАПРЕЩЕНО, обращение к объекту с типом int через тип short char c = u.c; // ОК, char - это особый случай
Последний пункт списка про char или unsigned char - это лазейка для функций вида memcpy/memset/etc:
void my_zero_memory(void* p, size_t n) { char* bytes = static_cast(p); for (; n != 0; --n, ++bytes) *bytes = 0; // OK, к любому типу можно обращаться через char }
int x[100]; my_zero_memory(x, sizeof(x));
Однако любые попытки использовать другие типы приводят к неопределенному поведению, например:
// НЕПРАВИЛЬНО void my_fast_zero_memory(void* p, size_t n) { uint64_t* quads = static_cast(p); for (; n > 7; n -= 8, ++quads) *quads = 0; // НЕПРАВИЛЬНО, работает только для массивов (u)int64_t my_zero_memory(quads, n); }
К сожалению, в интернете полно такого "быстрого" кода, который в любой момент может сломаться если компилятор использует какую-нибудь оптимизацию после встраивания такой функции. (Правильный memset - это стандартный memset, или его надо писать например на ассемблере, где нет правил strict aliasing).
Поскольку такого неправильно написанного кода очень много, то в компиляторе GCC есть опция -fno-strict-aliasing, которая отключает оптимизации связанные с алиасингом.

Комментариев нет:

Отправить комментарий