Страницы

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

понедельник, 25 ноября 2019 г.

Что такое strict aliasing?


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


Ответы

Ответ 1



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, которая отключает оптимизации связанные с алиасингом.

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

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