Strict aliasing C/C++

kataich

Пытаюсь разобраться со strict aliasing в C/С++. Никак не могу понять чёткого определения, какие указатели действительно нарушают это правило. Ниже я приведу 4 примера. Скажите, пожалуйста, во всех ли случаях strict aliasing нарушается, и в каком месте конкретно.

#include <stdlib.h>
#include <stdio.h>

int main
{
void *buf;

buf = malloc(8);
if (buf == NULL)
return -1;

{
int *int_ptr = (int *)buf;

*int_ptr = 0;
}

printf("int = %d\n", *(int *)buf);

{
long *long_ptr = (long *)buf;

*long_ptr = 1;
}

printf("long = %ld\n", *(long *)buf);

free(buf);

return 0;
}


#include <stdlib.h>
#include <stdio.h>

inline void int_buf(int *ptr)
{
*ptr = 0;

return;
}

inline void long_buf(long *ptr)
{
*ptr = 1;

return;
}

int main
{
void *buf;

buf = malloc(8);
if (buf == NULL)
return -1;

int_bufint *)buf);

printf("int = %d\n", *(int *)buf);

long_buflong *)buf);

printf("long = %ld\n", *(long *)buf);

free(buf);

return 0;
}


#include <stdlib.h>
#include <stdio.h>

inline void int_buf(int *ptr)
{
*ptr = 0;

return;
}

inline void long_buf(long *ptr)
{
*ptr = 1;

return;
}

int main
{
void *buf;
int *int_ptr;
long *long_ptr;

buf = malloc(8);
if (buf == NULL)
return -1;

int_ptr = (int *)buf;
long_ptr = (long *)buf;

int_buf(int_ptr);

printf("int = %d\n", *int_ptr);

long_buf(long_ptr);

printf("long = %ld\n", *long_ptr);

free(buf);

return 0;
}


#include <stdlib.h>
#include <stdio.h>

inline void int_buf(int *ptr)
{
*ptr = 0;

return;
}

inline void long_buf(long *ptr)
{
*ptr = 1;

return;
}

int main
{
void *buf;
int *int_ptr;
long *long_ptr;

buf = malloc(8);
if (buf == NULL)
return -1;

int_ptr = (int *)buf;

int_buf(int_ptr);

printf("int = %d\n", *int_ptr);

free(buf);

/* Accidentally we got the very
* same pointer as before so
* pointing to the same memory
* region
*/
buf = malloc(8);
if (buf == NULL)
return -1;

long_ptr = (long *)buf;

long_buf(long_ptr);

printf("long = %ld\n", *long_ptr);

free(buf);

return 0;
}

4 пример, кажется, совершенно верным, но что его отличает?
Update: 5 пример. Есть ли тут проблема?


#include <stdlib.h>
#include <stdio.h>

inline void int_buf(void *ptr)
{
int *int_ptr = (int *)ptr;
*int_ptr = 0;

return;
}

inline void long_buf(void *ptr)
{
long *long_tr = (long *)ptr;
*long_ptr = 1;

return;
}

int main
{
void *buf;

buf = malloc(8);
if (buf == NULL)
return -1;

int_buf(buf);

printf("int = %d\n", *(int *)buf);

long_buf(buf);

printf("long = %ld\n", *(long *)buf);

free(buf);

return 0;
}

evolet

самое главное, что тебе надо знать про strict aliasing - это опция -fno-strict-aliasing

kataich

самое главное, что тебе надо знать про strict aliasing - это опция -fno-strict-aliasing
Не всё так просто же? Это не панацея, и компилятор не такой умный, чтобы указать на все такие места.
Все примеры, которые я привёл, прекрасно компилируются в этой опцией. Значит всё ОК, по-твоему?

serega1604

Это не панацея, и компилятор не такой умный, чтобы указать на все такие места.
Он же с этим ключом просто не будет полагаться на то что какие-то указатели ссылаются на различное место в памяти, т.е. не будет делать оптимизаций, связанных с strict aliasing.
а ты наверно подумал про -Wstrict-aliasing

kataich

Ага, перепутал с -Wstrict-aliasing, прошу прощения. Но отключать оптимизацию нельзя. Хочется писать правильный код, а для этого надо понять эту тему.

evolet

Но отключать оптимизацию нельзя
что действительно нельзя делать, так это выключать вибратор, оказавшийся в анусе, ну а если ты подумаешь его не дай боже вынуть, знай - в мире мигом умрет не менее 100 котиков

kataich

Но отключать оптимизацию нельзя
что действительно нельзя делать, так это выключать вибратор, оказавшийся в анусе, ну а если ты подумаешь его не дай боже вынуть, знай - в мире мигом умрет не менее 100 котиков
Спасибо, всё сразу же стало на свои места.
Ну а если по делу, то зачем выключать оптимизацию, если ты понимаешь, что не нарушаешь никаких правил.

Maurog

Скажите, пожалуйста, во всех ли случаях strict aliasing нарушается, и в каком месте конкретно.
мне кажется, что проблема с алиасингом только в примере 3
как я представляю себе проблему: если мы имеем пару (для простоты) переменных несвязаннного типа (long, int в данных примерах) и идет запись в одну переменную (или обе) и чтение из другой переменной, то поведение будет неопределено из-за нарушения алиасинга (когда по факту обе переменные пересекаются в памяти)
еще надо учесть, что char* может алиаситься с любым типом
вот тут алиасинг нарушен:

void* buf = malloc(8);
int* a = (int*)buf;
long* b = (long*)buf;
//main part:
*a = 8;
*b = 9;
printf("a=%d, b= %d", *a, *b);

на корректность не претендую, могу дать первую ссылку из гугла: http://dbp-consulting.com/tutorials/StrictAliasing.html и ссылка оттуда: http://www.cs.technion.ac.il/users/yechiel/CS/C++draft/ratio...

Dasar

если мы имеем пару (для простоты) переменных несвязаннного типа (long, int в данных примерах)
чем пара несвязанных переменных (int*, long*) отличается от пары таких же несвязанных переменных, но с типами (void*, int*)?

Maurog

чем пара несвязанных переменных (int*, long*) отличается от пары таких же несвязанных переменных, но с типами (void*, int*)?
не понял твой вопрос, уточни плиз
void* - это указатель на данные, но данные изменить нет возможности (надо перейти к другому типу)
вся тема об изменении данных

Dasar

void* - это указатель на данные, но данные изменить нет возможности (надо перейти к другому типу)
Что будет если добавить передачу void* переменной в другую функцию, где она уже и преобразуется к другому типу?

kataich

Общая логика такова, что нет никакой проблемы, если два указателя будут указывать на одну и ту же область памяти. Не важно, каков тип этих указателей. Проблемы начнутся тогда, когда оба из них будут разыменованы. В этом случает компилятор рассматривает их, как указывающих на разные области памяти и порядок разыменования может меняться в зависимости от оптимизации, что может привести к проблеме.
Вопрос, на который я не могу ответить - это каковы эти указатели: в одной функции, области видимости или ещё что-то. Ведь иначе malloc/new не работали бы, так как, очевидно, разные типы данных могут указывать на одну и ту же область памяти. Поэтому мне и хотелось бы отличить пример 4 от предыдущих трёх.
Да, замечание по поводу char абсолютно верно. Этот тип может алиасится с любым других. Поэтому, к примеру, memcpy не имеет проблемы с алиасингом, насколько я понимаю.

kataich

Да, по сути второй пример это иллюстрирует. Можно ввести множество модификаций. И я не понимаю, валиден ли такой подход или нет. Опять же, алиасинг - это когда два указателя указывают на одну и ту же область памяти. И мне не понятно, есть ли какое ограничение, когда эти указатели становятся 'далёкими друг от друга', и алиасинга нет. Или это распространяется на контекст всей программы.

kataich

Насколько я понял, имеет в виду следующий код. Есть ли тут проблема?

#include <stdlib.h>
#include <stdio.h>

inline void int_buf(void *ptr)
{
int *int_ptr = (int *)ptr;
*int_ptr = 0;

return;
}

inline void long_buf(void *ptr)
{
long *long_tr = (long *)ptr;
*long_ptr = 1;

return;
}

int main
{
void *buf;

buf = malloc(8);
if (buf == NULL)
return -1;

int_buf(buf);

printf("int = %d\n", *(int *)buf);

long_buf(buf);

printf("long = %ld\n", *(long *)buf);

free(buf);

return 0;
}

Maurog

Насколько я понял, имеет в виду следующий код. Есть ли тут проблема?
я не вижу здесь проблем (возможные проблемы с выравниванием оставляем за скобками)

kataich

я не вижу здесь проблем (возможные проблемы с выравниванием оставляем за скобками)
Я тоже не вижу, а она есть (с) :)
В действительности, я специально сделал функции inline, поскольку, тогда это не отличается от случая на мой взгляд.
Строго говоря, алиасинг присутствует int_ptr и long_ptr указывают на одну и ту же область памяти, но имеют разный тип.

Maurog

Строго говоря, алиасинг присутствует int_ptr и long_ptr указывают на одну и ту же область памяти, но имеют разный тип.
алиасинга здесь не вижу, потому что переменные имеют непересекающиеся времена жизни. они указывают на одну и ту же память в разные моменты времени

kataich

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

ppplva

В четвертом примере нет алиасинга так это разные locations.
If a program attempts to access the stored value of an object through an lvalue of other than one of the following types the behavior is undefined
То есть в каждый момент времени каждый адрес памяти имеет какой-то один конкретный тип (с оговорками) и через указатель другого типа туда лезть нельзя.
Это разрешает кучу разных оптимизаций.

Maurog

Мой контраргумент: компилятор видит, что чтения из int_ptr и long_ptr нет, а только присваивание, и просто выбросит эти блоки при оптимизации.
какие именно блоки выбросит? там, где присваивание? их выбросить нельзя, т.к. туда смотрит переданный ptr и программа напечатает мусор при такой "оптимизации".

kataich

Не мог бы ты пояснить значения слова "location"?
P.S. То есть во всех других случаях aliasing есть?

kataich

какие именно блоки выбросит? там, где присваивание? их выбросить нельзя, т.к. туда смотрит переданный ptr и программа напечатает мусор при такой "оптимизации".
Да, блоки, где присваивание.
Вот здесь разбирается точно такой же пример, по-моему мнению.
P.S. Правда у меня (g++ 4.6.3) не воспроизводит результат.

ppplva

Присваивание можно выбросить только если доказано что его результат не используется. В примере по ссылке это так, потому что int и float не могут занимать одну и ту же память (иначе UB). В твоем примере (1-3 и 5) присваивается long и печатается тоже long. Теоретически, компилятор мог бы переставить присваивание long и печать int из тех же соображений, но это не дает никакого выигрыша, так что вряд ли.
У слова location здесь нет никакого специального смысла, просто область памяти.

kataich

переставить
Если бы имели что-то типа вот этого

{
int *int_ptr = (int *)buf;

*int_ptr = 0;

printf("int = %d\n", *int_ptr);
}

{
long *long_ptr = (long *)buf;

*long_ptr = 1;

printf("long = %ld\n", *long_ptr);
}

То компилятор мог бы переставить блоки местами.
Значит, примеры 1-3,5 содержат проблему!
У слова location здесь нет никакого специального смысла, просто область памяти
А что, если malloc возратит ту же саму область памяти во второй раз, что и в первый.
Получается, два указателя разных типов ссылаются на одну и ту же область.

ppplva

Я слегка нагнал, весь код в этом треде корректный.
Проблема случается когда в память пишется один тип, а читается другой. В примерах память переиспользуется для хранения объектов разного типа, это нормально.
 {
int *int_ptr = (int *)buf;

*int_ptr = 0;

printf("int = %d\n", *int_ptr);
}

Перенос printf внутрь блока ничего не меняет, int_ptr и (int *)buf очевидно совпадают. И вообще все изменения кода между 1-3,5 чисто косметические, это на самом деле один и тот же пример.
А что, если malloc возратит ту же саму область памяти во второй раз, что и в первый.
Получается, два указателя разных типов ссылаются на одну и ту же область.
Так не одновременно же. Первый указатель нельзя использовать после free - даже если ты убедился что он равен второму.

Maurog

весь код в этом треде корректный
я с тобой не согласен. я по-прежнему считаю, что в примере 3 от ТС проблема с алиасингом. в этом примере компилятор не переставит присваивания в int_ptr и long_ptr только если будет знать, что они алиасятся.
вдобавок, я приводил некорректный код
в обоих примерах компилятору не составит труда выявить алиасинг, но с точки зрения стандарта есть нарушение, ведущее к UB

Maurog

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

ppplva

Да, твой пример конечно некорректный.
Про 3 - не согласен.
If a program attempts to access the stored value of an object through a glvalue of other than one of the
following types the behavior is undefined:
— the dynamic type of the object,
...

Покажи плз в какой строке программы нарушается это правило (или найди другое правило).

Maurog

Покажи плз в какой строке программы нарушается это правило
в этой строке идет доступ к записанному int через указатель на long:
*ptr = 1;
повторю ссылку http://dbp-consulting.com/tutorials/StrictAliasing.html , где в параграфе So what can alias поясняют, что несвязанные типы не алиасятся, что может привести к UB в случае реального пересечения по памяти
чуть выше такое:
Anything not on the list can be assumed to not alias, and compiler writers are free to do optimizations that make that assumption.
в целом, описание в стандарте проблемы алиасинга мне не кажется кристально ясным :(

bleyman

Во-первых, все пять примеров не ОК потому что ты должен использовать "char *" вместо "void *" в таких ситуациях. Смотри C99 standard, section 6.5, paragraph 7.
Далее, если ты будешь использовать char*, то все пять примеров будут ок по-моему, что наводит тебя на мысль об обратном?
Как бы strict aliasing означает что компайлер имеет право кэшировать значения твоих переменных в регистрах и не инвалидировать (статически) закэшированные значения после доступа via incompatible pointer type. То есть записать что-то куда-то через int* и достать через long* — не ОК, но в этом случае ты пишешь и достаёшь значения с одним и тем же типом, причём компилятор должен понимать что объект в обоих случаях алиазится с char* переменной и поэтому оба доступа транзитивно алиасятся. Мне так кажется. Но вообще вопрос интересный, что тебя на него натолкнуло?

ppplva

Неее, запись не является "access the stored value". Иначе реюз памяти без деаллокации будет невозможен, как и всякие pool allocators, и union.

bleyman

Неее, запись не является "access the stored value". Иначе реюз памяти без деаллокации будет невозможен, как и всякие pool allocators, и union.
Что, почему? Для unions специально есть исключения в стандарте (зачем бы они были нужны, если бы они были не нужны? pool allocators use char*, что ты имеешь в виду под "реюз памяти без деаллокации"?

kataich

Но вообще вопрос интересный, что тебя на него натолкнуло?
Да всё очень прозаично - просто реальный случай в проекте.
В моей программе есть компоненты, которые общаются "сообщениями". Сообщение представляет собой заголовок и данные. Существуют несколько типов сообщений. Все они имеют один и тот же заголовок, но данные разнятся. Я выделил единый кусок памяти, чтобы в него вмещались все типы сообщений, а также завёл несколько указателей. Указатель на заголовок (указывает на начало выделенного куска памяти указатели на данные (тип данных разный для разных типов сообщений которые все по сути указывают на одну и ту же область памяти (начало + смещение на величину заголовка). Весь процесс примерно выглядит следующих образом: некоторые функции заполняют данные заголовка (получая на вход указатель на заголовок) и данные (получая на вход указатель на данные а потом вызвают функцию 'отправки сообщений' (эта функция 'безопасна' с точки зрения алиасинга в том смысле, что рассматривате всё как массив char). Что-то типа этого:

char *buf = (char *)malloc(BIG_ENOUGH_SIZE);

header *header = (header *)buf;
msg_type1 *msg_type1_ptr = (msg_type1 *buf + sizeof(*header;
msg_type2 *msg_type2_ptr = (msg_type2 *buf + sizeof(*header;

fill_header(header);
fill_msg_type1(msg_type1);
send(buf);

fill_header(header);
fill_msg_type2(msg_type2)
send(buf);

etc

Всё работает (пока?) верно. Потом коллега обратил внимание на то, что указатели алиасятся. Мы начали думать - и понеслось...
Про "access the stored value" я тоже не совсем понял. Что означает "acess"? Чтение или запись?
Тогда в примере 3, как сказал , действительно, есть проблема, поскольку идёт запись по указателю long на то место, в которое до этого шла запись по указателю int. Но она, вроде как, есть и в предыдущих примерах 1-2.
Единственное, что может их отличать, это то, что в тот момент, когда идёт запись по указателю long, указатель int становится каким-то образом неважным, но я не понимаю это, честно говоря.

ppplva

Для union нет исключений в стандарте. Он в каждый момент времени содержит _одно_ значение из набора, и читать можно только то, что было записано последним. Union - это не способ обойти strict aliasing.
Говоря pool allocator я имел в виду любую бодягу которая реализует malloc/free не вызывая malloc/free, кешируя аллокации. Неважно как оно у себе внутри объявляет указатель на блок памяти. С точки зрения компилятора внутренности этого блока сначала используются для хранения одних данных, потом других. store A => load A => store B => load B. Точно как в примерах в первом сообщении.

kataich

Правильно ли я понимаю твою мысль? Если каждое обращение к данной ячейке памяти мыслить как пару (тип операции: read|write; тип, с помощью которого идёт обращение) то в последовательности всех обращений к данной ячейке не может быть соседних пар вида (write; type1 (read; type2). Все остальные последовательности валидны.
P.S. При этом (write, type1 (read, char) валидны так как char алиасится со всем

ppplva

Да, я это именно так понимаю.
Вот здесь чувак правильно объясняет со ссылками на Стандарт:
http://stackoverflow.com/questions/18624449/shared-memory-bu...
Оставить комментарий
Имя или ник:
Комментарий: