Правильно ли указано правило строгой алиасии?

Как ранее установленный, объединение формы

union some_union {
    type_a member_a;
    type_b member_b;
    ...
};

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

Это в дальнейшем поддерживается в сноске 95, в которой объясняется, как использование типа punning является предполагаемым использованием союзов.

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

int strict_aliasing_example(int *i, float *f)
{
    *i = 1;
    *f = 1.0;
    return (*i);
}

который компилятор может оптимизировать для чего-то вроде

int strict_aliasing_example(int *i, float *f)
{
    *i = 1;
    *f = 1.0;
    return (1);
}

поскольку он может с уверенностью предположить, что запись в *f не влияет на значение *i.

Однако, что происходит, когда мы передаем два указателя членам одного и того же союза? Рассмотрим этот пример, предполагая типичную платформу, где float - это число с плавающей запятой с одиночной точностью IEEE 754, а int - 32-битное целое число из двух дополнений:

int breaking_example(void)
{
    union {
        int i;
        float f;
    } fi;

    return (strict_aliasing_example(&fi.i, &fi.f));
}

Как было установлено ранее, fi.i и fi.f относятся к области перекрывающейся области памяти. Чтение и запись их безусловно легально (запись разрешена только после того, как союз был инициализирован) в любом порядке. На мой взгляд, ранее обсуждавшаяся оптимизация, выполняемая всеми основными компиляторами, дает неправильный код, поскольку два указателя разного типа юридически указывают на одно и то же местоположение.

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

Скажите, пожалуйста, почему я ошибаюсь.

A связанный вопрос появился во время исследования.

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

Ответ 1

Начиная с вашего примера:

int strict_aliasing_example(int *i, float *f)
{
    *i = 1;
    *f = 1.0;
    return (*i);
}

Позвольте сначала признать, что в отсутствие каких-либо союзов это нарушит правило строгого сглаживания, если i и f оба указывают на один и тот же объект; предполагая, что объект не имеет эффективного типа, тогда *i = 1 устанавливает эффективный тип int и *f = 1.0, а затем устанавливает его в float, а заключительный return (*i) затем обращается к объекту с эффективным типом float через lvalue типа int, что явно не допускается.

Вопрос в том, будет ли это все равно равным строгому сглаживанию, если оба i и f указывают на членов одного и того же объединения. Доступ к члену профсоюза через "." оператор доступа к члену, спецификация говорит (6.5.2.3):

Постфиксное выражение, за которым следует. оператор и идентификатор обозначает элемент структуры или объект объединения. Значение в том, что (95) и является значением l, если первое выражение равно lvalue.

В сноске 95, упомянутой выше, говорится:

Если элемент, используемый для чтения содержимого объекта объединения, не является тот же самый, что и последний элемент, используемый для хранения значения в объекте, Соответствующая часть объектного представления значения равна реинтерпретируется как представление объекта в новом типе, как описано в 6.2.6 (процесс, иногда называемый "пингом типа" ). Это может быть представление ловушки.

Это явно предназначено для того, чтобы разрешить использование типа punning через объединение, но следует отметить, что (1) сноски являются ненормативными, то есть они не должны запрещать поведение, а скорее должны разъяснять намерение некоторых часть текста в соответствии с остальной частью спецификации, и (2) это пособие для типа punning через объединение производится только для доступа через оператор доступа члена профсоюза, что означает, что ваш пример хранится с помощью указателя на не- существующий член профсоюза, и тем самым совершает строгое нарушение псевдонимов, так как он обращается к члену, который активен, используя lvalue неподходящего типа. (Я мог бы добавить, что я не вижу, как в сноске описывается поведение, которое иначе присуще спецификации, то есть оно, по-видимому, нарушает правило ISO, не запрещающее поведение, ничто иное в спецификации, по-видимому, не допускает применения типа punning через объединение).

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

Часто возникает путаница, вызванная другой частью спецификации, однако также в 6.5.2.3:

Для упрощения использования профсоюзов предусмотрена одна специальная гарантия: если объединение содержит несколько структур, которые имеют общую начальную (см. ниже), и если объект объединения в настоящее время содержит один этих структур, разрешено проверять общие начальные часть любого из них в любом месте, где декларация завершенного типа союза видно.

Хотя это не относится к вашему примеру, поскольку нет обычной начальной последовательности, я видел, как люди читали это как общее правило для управления типом пиннинга (по крайней мере, когда задействована общая начальная последовательность); они полагают, что это означает, что при использовании полного объявления профсоюза должно быть возможно использовать такой тип punning, используя два указателя для разных членов профсоюза (так как слова в этом случае появляются в приведенном выше параграфе). Однако я хотел бы указать, что вышеприведенный параграф применяется только к доступу членов профсоюза через ".". оператор. Проблема с согласованием этого понимания заключается в том, что в этом случае полная декларация объединения должна быть видимой, поскольку в противном случае вы не сможете ссылаться на членов профсоюза. Я думаю, что это сбой в формулировке, в сочетании с аналогичной плохими формулировками в примере 3 (Следующий пример не является допустимым фрагментом (поскольку тип объединения не виден...), когда видимость соединения не является решающим фактором), что заставляет некоторых людей толковать, что исключение общей исходной последовательности предназначено для применения во всем мире, а не только для доступа членов через ".". оператор, как исключение из правила строгого сглаживания; и, придя к такому выводу, читатель может тогда (неправильно) интерпретировать сноску, касающуюся типа punning, применимого и в глобальном масштабе.

(Кстати, мне известно о нескольких компиляторах, которые не реализуют правило "глобальной общей исходной последовательности". Я предполагаю, что их авторы не интерпретируют спецификацию, чтобы подразумевать это правило. Я не знаю каких-либо компиляторов, которые реализуют правило глобальной общей исходной последовательности, а также не допускает произвольного произвольного каста, т.е. я не знаю компиляторов, которые идут на конкретные усилия по реализации правила. С другой стороны, мне не ясно, что такое правило будет на балансе плохое, это просто не то, что спецификация на самом деле требует, и это не то, что компиляторы реализуют).

На этом этапе вы вполне можете задаться вопросом, как чтение неактивного члена профсоюза с помощью оператора доступа участника не нарушает строгий псевдоним, если делать то же самое с помощью указателя. Это снова область, где спецификация несколько туманна; ключ заключается в том, чтобы решить, какой lvalue отвечает за доступ. Например, если объект union u имеет член a, и я прочитал его через выражение u.a, тогда мы могли бы интерпретировать это как либо доступ к объекту-члену (a), либо просто к доступу объекта объединения (u), значение которого затем извлекается из. В последнем случае нарушение псевдонимов отсутствует, поскольку ему специально разрешен доступ к объекту (то есть к активному объекту-члену) через lvalue совокупного типа, содержащего подходящий элемент (6.5¶7). В самом деле, определение оператора доступа к элементам в 6.5.2.3 поддерживает эту интерпретацию, если она несколько слабее: это значение именованного элемента - хотя это потенциально значение lvalue, нет необходимости обращаться к объекту, на который ссылается тот lvalue, чтобы получить значение члена, и поэтому исключается строгое нарушение псевдонимов.

(Мне кажется, что это не определено, как правило, только когда у объекта есть "его хранимое значение, доступное... выражением lvalue" согласно 6.5¶7, мы можем, конечно, сделать разумную решимость для себя, но то мы должны быть осторожны, чтобы допускать произвольное использование пулов через союзы в соответствии с вышеизложенным или иначе быть готовым игнорировать сноску 95. Несмотря на часто ненужные формулировки, спецификация иногда не хватает в деталях).

Аргументы о семантике союза неизменно ссылаются на DR 236 в какой-то момент. Действительно, ваш примерный код внешне очень похож на код в этом отчете о дефектах. Я хотел бы отметить, что:

  • "Комитет полагает, что пример 2 нарушает правила псевдонимов в пункте 6.5 пункта 7" - это не противоречит моим рассуждениям выше;
  • "Чтобы не нарушать правила, функция f в примере должна быть записана как" - это поддерживает мои рассуждения выше; вы должны использовать объект union (и оператор "." ) для изменения активного типа элемента, в противном случае вы обращаетесь к несуществующему члену (поскольку объединение может содержать только один элемент за раз);
  • Пример в DR 236 не относится к типу. Речь идет о том, нормально ли назначать неактивный член профсоюза с помощью указателя на этот член. Этот код немного отличается от этого в вопросе здесь, так как он не пытается снова получить доступ к "оригинальному" члену профсоюза после записи во второй член. Таким образом, несмотря на структурное сходство в примере кода, отчет о дефектах в значительной степени не связан с вашим вопросом.
  • Ответ Комитета в DR 236 утверждает, что "обе программы ссылаются на поведение undefined". Однако это обсуждение не подтверждается обсуждением, которое показывает только, что пример 2 вызывает поведение undefined. Я считаю, что ответ ошибочен.

Ответ 2

В соответствии с определением членов профсоюза в п. 6.5.2.3:

3 Постфиксное выражение, за которым следует оператор ., и идентификатор обозначает член структуры или объекта объединения....

4 Постфиксное выражение, за которым следует оператор ->, и идентификатор обозначает член структуры или объект объединения....

См. также §6.2.3 ¶1:

  • члены структур или союзов; каждая структура или объединение имеет отдельное пространство имен для своих членов (неоднозначно по типу выражения, используемого для доступа к элементу через оператор . или ->);

Понятно, что сноска 95 относится к доступу члена объединения с объединением в область видимости и с использованием оператора . или ->.

Поскольку назначения и обращения к байтам, содержащим объединение, не производятся через члены союза, а через указатели, ваша программа не вызывает правила псевдонимов членов профсоюза (включая те, которые разъяснены в сноске 95).

Кроме того, правила нормального сглаживания нарушены, так как эффективный тип объекта после *f = 1.0 равен float, но к его сохраненному значению обращается значение l типа типа int (см. §6.5 ¶7).

Примечание: Все ссылки ссылаются на this Стандартная черновик C11.

Ответ 3

Стандарт C11 (§6.5.2.3.9 ПРИМЕР 3) имеет следующий пример:

Ниже приведен неверный фрагмент (поскольку тип объединения не является видимый внутри функции f):

 struct t1 { int m; };
 struct t2 { int m; };
 int f(struct t1 *p1, struct t2 *p2)
 {
       if (p1->m < 0)
               p2->m = -p2->m;
       return p1->m;
 }
 int g()
 {
       union {
               struct t1 s1;
               struct t2 s2;
       } u;
       /* ... */
       return f(&u.s1, &u.s2);
 }

Но я не могу найти больше разъяснений по этому поводу.

Ответ 4

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

В этом случае оптимизация, описанная в strict_aliasing_example(), разрешена, поскольку компилятору разрешено считать f и i указывать на разные адреса.

breaking_example() приводит к тому, что два указателя, переданные в strict_aliasing_example(), указывают на один и тот же адрес. Это нарушает предположение, что strict_aliasing_example() разрешено делать, поэтому приводит к тому, что функция демонстрирует поведение undefined.

Итак, поведение компилятора, которое вы описываете, действительно. Дело в том, что breaking_example() заставляет указатели, переданные strict_aliasing_example() указывать на тот же адрес, который вызывает поведение undefined - другими словами, breaking_example() нарушает предположение, что компилятору разрешено делать в strict_aliasing_example().

Ответ 5

Строгое правило сглаживания запрещает доступ к одному и тому же объекту двумя указателями, которые не имеют совместимых типов, если только не указатель на тип символа:

7 Объект должен иметь сохраненное значение, доступ к которому может получить только выражение lvalue, которое имеет один из следующих типов: 88)

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

В вашем примере *f = 1.0; изменяет fi.i, но типы несовместимы.

Я думаю, что ошибка состоит в том, что объединение содержит n объектов, где n - число членов. Объединение содержит только один активный объект в любой момент во время выполнения программы в соответствии с §6.7.2.1 ¶16

Значение не более одного из членов может быть сохранено в объединенном объекте в любое время.

Поддержка этой интерпретации о том, что объединение не содержит одновременно все его объекты-члены, можно найти в п. 6.5.2.3:

и если объект объединения в настоящее время содержит одну из этих структур

Наконец, почти идентичная проблема была поднята в отчете о дефектах 236 в 2006 году.

Пример 2

// optimization opportunities if "qi" does not alias "qd"
void f(int *qi, double *qd) {
    int i = *qi + 2;
    *qd = 3.1;       // hoist this assignment to top of function???
    *qd *= i;
    return;
}  

main() {
    union tag {
        int mi;
        double md;
    } u;
    u.mi = 7;
    f(&u.mi, &u.md);
}

Комитет считает, что пример 2 нарушает правила псевдонимов в 6.5 пункт 7:

"совокупный или объединенный тип, который включает в себя один из вышеупомянутых типы среди своих членов (в том числе, рекурсивно, член subaggregate или contains union).

Чтобы не нарушать правила, функция f в примере должна быть написано как:

union tag {
    int mi;
    double md;
} u;

void f(int *qi, double *qd) {
    int i = *qi + 2;
    u.md = 3.1;   // union type must be used when changing effective type
    *qd *= i;
    return;
}

Ответ 6

Вот примечание 95 и его контекст:

Постфиксное выражение, за которым следует. оператор и идентификатор обозначает член структуры или объект объединения. Значение имеет значение именованного члена (95) и является значением lvalue, если первое выражение является lvalue. Если первое выражение имеет квалифицированный тип, результат имеет соответствующую квалификацию типа назначенного элемента.

(95) Если элемент, используемый для чтения содержимого объекта объединения, не совпадает с элементом, используемым последним для хранения значения в объекте, соответствующая часть представления объекта значение переинтерпретируется как представление объекта в новом типе, как описано в 6.2.6 (процесс, иногда называемый "пингом типа" ). Это может быть ловушечное представление.

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

Это не окончательный ответ...

Ответ 7

Откажитесь от стандарта на секунду и подумайте о том, что действительно возможно для компилятора.

Предположим, что strict_aliasing_example() определено в strict_aliasing_example.c, а breaking_example() определено в breaking_example.c. Предположим, что оба этих файла скомпилированы отдельно и затем связаны вместе, например:

gcc -c -o strict_aliasing_example.o strict_aliasing_example.c
gcc -c -o breaking_example.o breaking_example.c
gcc -o breaking_example strict_aliasing_example.o breaking_example.o

Конечно, нам нужно добавить прототип функции в breaking_example.c, который выглядит следующим образом:

int strict_aliasing_example(int *i, float *f);

Теперь рассмотрим, что первые два вызова gcc полностью независимы и не могут обмениваться информацией, кроме прототипа функции. Компилятор не знает, что i и j будут указывать на члены одного и того же объединения, когда он генерирует код для strict_aliasing_example(). В системе связей или типа ничего нет, чтобы указать, что эти указатели как-то особенны, потому что они пришли из союза.

Это подтверждает вывод о том, что в других ответах упоминалось: со стандартной точки зрения доступ к соединению через . или -> подчиняется различным правилам псевдонимов по сравнению с разыменованием произвольного указателя.

Ответ 8

До стандарта C89 подавляющее большинство реализаций определяло поведение разыменования записи на указатель определенного типа, устанавливая биты базового хранилища в моде, определенном для этого типа, и определяло поведение read- разыменовывая указатель определенного типа, считывая биты базового хранилища способом, определенным для этого типа. Хотя такие возможности не были бы полезны для всех реализаций, было множество реализаций, в которых производительность горячих контуров могла бы быть значительно улучшена, например, используя 32-разрядные нагрузки и хранилища для работы с группами по четыре байта одновременно. Кроме того, во многих таких реализациях поддержка такого поведения не стоила ничего.

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

  • Правила C89 могли быть предназначены для применения только в случаях, аналогичных тем, которые указаны в обосновании (доступ к объекту с объявленным типом как непосредственно через этот тип, так и косвенно с помощью указателя), и где компиляторы не имеют оснований ожидать наложения псевдонимов. Отслеживание каждой переменной, будь она в настоящее время кэшировано в регистре, довольно просто и возможность сохранять такие переменные в регистрах при доступе к указателям других типов - это простая и полезная оптимизация и не будет препятствовать поддержке кода, который использует более распространенные шаблоны сглаживания (имеющие компилятор, интерпретирующий приведение float* to int* как необходимость сглаживания любых значений кэшированных регистров float, просты и понятны, такие отбрасывания достаточно редки, что такой подход вряд ли может отрицательно сказаться на производительности).

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

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

К сожалению, отсутствие ясности в отношении того, что требует стандарт, привело к ситуации, когда некоторые люди считают устаревшими конструкциями, для которых не существует никаких замен. Наличие полного определения типа объединения, включающего два примитивных типа, должно интерпретироваться как указание на то, что любой доступ с помощью указателя одного типа следует рассматривать как вероятный доступ к другому, позволяющий настраивать программы, которые полагаются на наложение псевдонимов сделать это без Undefined Поведение - то, чего не достижимо ни один другой практический способ, учитывая настоящий Стандарт. К сожалению, такая интерпретация также ограничит многие оптимизации в 99% случаев, когда они будут безвредными, что делает невозможным компиляторы, которые интерпретируют Стандарт таким образом, чтобы использовать существующий код так же эффективно, как это было бы возможно.

Относительно того, правильно ли указано правило, это будет зависеть от того, что оно должно означать. Возможны множественные разумные интерпретации, но объединение их дает некоторые довольно необоснованные результаты.

PS - единственная интерпретация правил относительно сравнения указателей и memcpy, которая имела бы смысл, не придавая термину "объект" значение, отличное от его значения в правилах псевдонимов, предполагало бы, что не может быть выделена выделенная область для хранения более одного объекта. В то время как некоторые виды кода могут быть в состоянии соблюдать такое ограничение, это сделало бы невозможным использование программами собственной логики управления памятью для утилизации хранилища без чрезмерного количества вызовов malloc/free. Авторы Стандарта, возможно, намеревались сказать, что реализациям не требуется, чтобы программисты создали большой регион и разбивали его на более мелкие куски смешанного типа, но это не означает, что они предполагали реализацию общего назначения, поэтому.