Союзы, псевдонимы и тип-наказания на практике: что работает, а что нет? - PullRequest
0 голосов
/ 19 февраля 2019

У меня проблемы с пониманием того, что можно и нельзя делать с помощью союзов с GCC.Я читаю вопросы (в частности здесь и здесь ) об этом, но они фокусируются на стандарте C ++, я чувствую, что есть несоответствие между стандартом C ++ и практикой (обычно используемые компиляторы).

В частности, недавно я нашел запутанную информацию в онлайн-документе GCC , когда читал о флаге компиляции -fstrict-aliasing .В нем говорится:

-fstrict-aliasing

Разрешить компилятору принимать самые строгие правила псевдонимов, применимые к компилируемому языку.Для C (и C ++) это активирует оптимизации на основе типа выражений.В частности, предполагается, что объект одного типа никогда не будет находиться по тому же адресу, что и объект другого типа, если типы почти не совпадают.Например, unsigned int может иметь псевдоним int, но не void* или double.Тип символа может быть псевдонимом любого другого типа.Обратите особое внимание на код, подобный следующему:

union a_union {
  int i;
  double d;
};

int f() {
  union a_union t;
  t.d = 3.0;
  return t.i;
}

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

Это то, что я понял из этого примера, и мои сомнения:

1) алиасинг работает только междуаналогичные типы, или char

Следствие 1): псевдоним - как следует из слова - это когда у вас есть одно значение и два члена для доступа к нему (т.е. одни и те же байты);

Сомнение: похожи ли два типа, когда они имеют одинаковый размер в байтах?Если нет, то что такое похожие типы?

Следствие 1) для не похожих типов (что бы это ни значило), алиасинг не работает;

2) type punning - это когда мы читаем другой член, чем тот, которому мы написали;это обычное явление, и оно работает, как и ожидалось, до тех пор, пока доступ к памяти осуществляется через тип объединения;

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

Я запутался, потому что он говорит, что unsigned int и double не похожи, поэтому псевдонимы не работают;затем в примере это псевдоним между int и double, и он ясно говорит, что работает как положено, но называет это типизацией: не потому, что типы являются или не похожи, а потому, что он читает из члена, который не записал.Но чтение от члена, которого он не написал, - то, для чего я понял псевдонимы (как предполагает слово).Я потерялся.

Вопросы: Может ли кто-нибудь прояснить разницу между псевдонимами и тип-наказанием, и какие виды использования этих двух методов работают так, как ожидается в GCC?А что делает флаг компилятора?

Ответы [ 5 ]

0 голосов
/ 28 февраля 2019

Я думаю, что было бы хорошо добавить дополнительный ответ, просто потому, что когда я задавал вопрос, я не знал, как удовлетворить свои потребности без использования UNION: я стал упрямым использовать его, потому что, казалось, он точно отвечал моим потребностям.

Хороший способ сделать типизацию и избежать возможных последствий неопределенного поведения (в зависимости от компилятора и других настроек env.) - это использовать std :: memcpy и копировать байты памяти из одного типа в другой.Это объясняется - например - здесь и здесь .

Я также часто читал, что когда компилятор создает допустимый код для наложения типов с использованием объединений, он производиттот же двоичный код, что и при использовании std :: memcpy.

Наконец, даже если эта информация не отвечает непосредственно на мой исходный вопрос, она настолько жестко связана, что я счел полезным добавить ее сюда.

0 голосов
/ 19 февраля 2019

Согласно сноске 88 в проекте C11 N1570, «правило строгого наложения имен» (6.5p7) предназначено для указания обстоятельств, при которых компиляторы должны учитывать возможность того, что вещи могут иметь псевдонимы, но не пытаются определить, чтопсевдоним равен .Где-то вдоль линии появилось распространенное мнение, что доступы, отличные от определенных правилом, представляют собой «псевдонимы», а разрешенные - нет, но на самом деле верно обратное.

Учитывая такую ​​функцию, как:

int foo(int *p, int *q)
{ *p = 1; *q = 2; return *p; }

В разделе 6.5p7 не говорится, что p и q не будут псевдонимами, если они идентифицируют одно и то же хранилище.Скорее, он указывает, что им разрешено для псевдонима.

Обратите внимание, что не все операции, связанные с доступом к хранилищу одного типа в качестве другого, представляют псевдонимы.Операция над lvalue, которая только что визуально получена из другого объекта, не "псевдоним" этого другого объекта.Вместо этого он является операцией над этим объектом.Псевдоним возникает, если между временем создания ссылки на какое-либо хранилище и временем его использования на то же хранилище ссылаются каким-либо образом , не полученным из первого , или код входит в контекст, в котором это происходит.

Хотя способность распознавать, когда lvalue является производной от другого, является проблемой качества реализации, авторы Стандарта должны были ожидать, что реализации распознают некоторые конструкции, выходящие за рамки мандата.Нет общего разрешения на доступ к любому хранилищу, связанному со структурой или объединением, с использованием lvalue типа member, и ничего в Стандарте прямо не указано, говорит , что операция, включающая someStruct.member, должна быть распознана какоперация на someStruct.Вместо этого, авторы Стандарта ожидали, что авторы компиляторов, которые предпринимают разумные усилия для поддержки конструкций своих клиентов, должны располагать лучшими возможностями, чем Комитет, чтобы оценивать потребности этих клиентов и удовлетворять их.Поскольку любой компилятор, который предпринимает даже отдаленно разумные усилия для распознавания производных ссылок, заметил бы, что someStruct.member является производным от someStruct, авторы Стандарта не видели необходимости явно указывать это.

К сожалению,обработка таких конструкций, как:

actOnStruct(&someUnion.someStruct);
int q=*(someUnion.intArray+i)

эволюционировала из "Достаточно очевидно, что actOnStruct и разыменование указателя должны действовать на someUnion (и, следовательно, на все его элементы), что нетнужно предписывать такое поведение "to" Поскольку стандарт не требует, чтобы реализации признавали, что вышеуказанные действия могут повлиять на someUnion, любой код, основанный на таком поведении, нарушен и не нуждается в поддержке ".Ни одна из вышеперечисленных конструкций не поддерживается надежно gcc или clang, за исключением режима -fno-strict-aliasing, хотя большинство «оптимизаций», которые будут заблокированы их поддержкой, будут генерировать код, который «эффективен», но бесполезен.

Если вы используете -fno-strict-aliasing на любом компиляторе, имеющем такую ​​опцию, почти все будет работать.Если вы используете -fstrict-aliasing в icc, он будет пытаться поддерживать конструкции, использующие типизацию без псевдонимов, хотя я не знаю, есть ли какая-либо документация о том, какие конструкции он выполняет или не обрабатывает.Если вы используете -fstrict-aliasing на gcc или clang, все, что работает, является чисто случайным.

0 голосов
/ 19 февраля 2019

В ANSI C (AKA C89) у вас есть (раздел 3.3.2.3 Структура и члены объединения):

, если доступ к элементу объекта объединения осуществляется после сохранения значения в другомчлен объекта, поведение определяется реализацией

В C99 у вас есть (раздел 6.5.2.3 Структура и члены объединения):

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

IOW, в C допускается перетаскивание типов на основе объединения, хотя фактическая семантика может отличаться в зависимости от поддерживаемого стандарта языка (обратите внимание, что семантика C99 более узкая)чем определяемая реализацией C89 ).

В C99 у вас также есть (раздел 6.5 Выражения):

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

- тип, совместимый с действительным типом объекта,

- квалифицированная версия типа, совместимого с эффективным типом объекта,

- тип, который является типом со знаком или без знака, соответствующим действующему типу объекта,

- тип, который является типом со знаком или без знака, соответствующим квалифицированной версии действующеготип объекта,

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

- тип символа.

И есть раздел (6.2.7 Совместимый тип и составной тип)в C99, который описывает совместимые типы:

Два типа имеют совместимый тип, если их типы совпадают.Дополнительные правила определения совместимости двух типов описаны в 6.7.2 для спецификаторов типов, в 6.7.3 для классификаторов типов и в 6.7.5 для деклараторов....

И затем (6.7.5.1 объявления указателей):

Для совместимости двух типов указателей оба должны быть одинаково квалифицированы, и оба должны быть указателями насовместимые типы.

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

Вы можете найти похожий язык в различных версиях стандарта C ++.Однако, насколько я могу судить, в C ++ 03 и C ++ 11 объединение типов на основе объединения явно не разрешено (в отличие от C).

0 голосов
/ 19 февраля 2019

Терминология - отличная вещь, я могу использовать ее так, как хочу, и так могут все остальные!

два типа похожи, когда они имеют одинаковый размер в байтах?Если нет, то что такое похожие типы?

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

алиасирует конкретный случай типового наказания, когда типы похожи?

Типовое наказание - это любой метод, который обходит типсистема.

Псевдоним - это особый случай, который включает размещение объектов разных типов по одному и тому же адресу.Псевдонимы обычно разрешены, когда типы похожи, и запрещены в противном случае.Кроме того, можно получить доступ к объекту любого типа через lvalue char (или аналогично char), но делать обратное (т. Е. Доступ к объекту типа char через lvalue разнородного типа) не допускается.Это гарантируется как стандартами C, так и C ++, GCC просто реализует то, что предписано стандартами.

Документация GCC, похоже, использует "наказание типов" в узком смысле чтения члена объединения, отличного от того, к которому последний был записан.Этот тип наказания типов допускается стандартом C, даже если типы не похожи.OTOH стандарт C ++ не позволяет этого.GCC может или не может распространять разрешение на C ++, документация по этому вопросу не ясна.

Без -fstrict-aliasing GCC, очевидно, ослабляет эти требования, но неясно, до какой степени.Обратите внимание, что -fstrict-aliasing является значением по умолчанию при выполнении оптимизированной сборки.

Итог, просто запрограммируйте на стандарт.Если GCC ослабляет требования стандарта, это не имеет значения и не стоит хлопот.

0 голосов
/ 19 февраля 2019

Псевдоним можно понимать буквально за то, что он означает: это когда два разных выражения ссылаются на один и тот же объект.Наказание типа - это «каламбур» типа, т. Е. Использование объекта некоторого типа в качестве другого типа.

Формально наложение типа является неопределенным поведением с несколькими исключениями.Обычно это происходит, когда вы небрежно возитесь с битами

int mantissa(float f)
{
    return (int&)f & 0x7FFFFF;    // Accessing a float as if it's an int
}

Исключения составляют (упрощенно)

  • Доступ к целым числам как к их беззнаковым / подписанным аналогам
  • Доступ к чему-либо какa char, unsigned char или std::byte

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

void transform(float* dst, const int* src, int n)
{
    for(int i = 0; i < n; i++)
        dst[i] = src[i];    // Can be unrolled and use vector instructions
                            // If dst and src alias the results would be wrong
}

. Что говорит GCC, так это то, что он немного ослабляет правила и позволяет вводить типы через объединения, даже если стандарт не требует его

union {
    int64_t num;
    struct {
        int32_t hi, lo;
    } parts;
} u = {42};
u.parts.hi = 420;

Это гарантия типа gcc, gcc сработает.Другие дела могут показаться работающими, но однажды они могут молча сломаться.

...