Строго соответствует ли такое использование профсоюзов?

Учитывая код:

struct s1 {unsigned short x;};
struct s2 {unsigned short x;};
union s1s2 { struct s1 v1; struct s2 v2; };

static int read_s1x(struct s1 *p) { return p->x; }
static void write_s2x(struct s2 *p, int v) { p->x=v;}

int test(union s1s2 *p1, union s1s2 *p2, union s1s2 *p3)
{
  if (read_s1x(&p1->v1))
  {
    unsigned short temp;
    temp = p3->v1.x;
    p3->v2.x = temp;
    write_s2x(&p2->v2,1234);
    temp = p3->v2.x;
    p3->v1.x = temp;
  }
  return read_s1x(&p1->v1);
}
int test2(int x)
{
  union s1s2 q[2];
  q->v1.x = 4321;
  return test(q,q+x,q+x);
}
#include <stdio.h>
int main(void)
{
  printf("%d\n",test2(0));
}

Во всей программе существует один объект объединения --_ 2_. Его активный член устанавливается на v1, затем на v2, а затем снова на v1. Код использует только оператор адресации на q.v1 или результирующий указатель, когда этот член активен, и аналогично q.v2. Поскольку p1, p2 и p3 относятся к одному типу, должно быть совершенно законным использование p3->v1 для доступа к p1->v1 и p3->v2 для доступа к p2->v2.

Я не вижу ничего, что могло бы оправдать ошибку компилятора в выводе 1234, но многие компиляторы, включая clang и gcc, генерируют код, который выводит 4321. Я думаю, что происходит то, что они решают, что операции на p3 на самом деле не изменят содержимое любых битов в памяти их можно просто игнорировать, но я не вижу в Стандарте ничего, что могло бы оправдать игнорирование того факта, что p3 используется для копирования данных из p1->v1 в p2->v2 и наоборот.

Есть ли в Стандарте что-нибудь, что могло бы оправдать такое поведение, или компиляторы просто не следуют ему?


person supercat    schedule 13.09.2017    source источник
comment
Если код был unsigned x вместо unsigned short x, видите ли вы ту же проблему?   -  person chux - Reinstate Monica    schedule 14.09.2017
comment
@chux: Да. В более ранней версии кода также тестировалось копирование байтов объекта в две переменные типа unsigned char с последующей их записью (которые компиляторы тоже не поддерживают), и было удобнее делать это с двумя байтами, чем с четырьмя. Проблема в том, что компилятор полностью оптимизирует операции над p3 и теряет предоставленную им информацию, связанную с псевдонимом.   -  person supercat    schedule 14.09.2017
comment
Я подозревал, что unsigned потерпит неудачу точно так же, как unsigned short. С unsigned мы можем отложить любую из обычных проблем с продвижением, которые не должны на это повлиять.   -  person chux - Reinstate Monica    schedule 14.09.2017
comment
@chux: Хотя unsigned short может продвигаться как int или unsigned, приведение значений 32767u и ниже к int полностью определено Стандартом для всех реализаций.   -  person supercat    schedule 14.09.2017
comment
Пример потенциальной проблемы со строгим псевдонимом без каламбура!   -  person curiousguy    schedule 20.09.2017
comment
@curiousguy: Я только что опубликовал еще одно неприятное сообщение, которое включает в себя переупорядочение памяти записи, а не упорядочение чтения и записи. Последний, который я нашел особенно любопытным, потому что он не включает компилятор, оптимизирующий чтение и запись, который должен был принудительно установить порядок некоторых других операций чтения и записи, но был скорректирован так, что то, что должно быть условной записью, превратилось в безусловную. пишите с условно выбранным значением.   -  person supercat    schedule 05.10.2017
comment
Вам также может быть интересно скомпилировать программу с -fsanitize=undefined и посмотреть, о чем предупреждает UBSan. Вы должны запустить свою программу с ее тестовыми данными, потому что UBSan - это средство проверки в реальном времени. Не дает ложных срабатываний.   -  person jww    schedule 29.10.2017


Ответы (4)


Я считаю, что ваш код соответствует требованиям, и есть недостаток в -fstrict-aliasing режиме GCC и Clang.

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

В стандарте C ++ [class.union] / 5 определяет, что происходит, когда оператор = используется в выражении доступа к объединению. Стандарт C ++ гласит, что когда объединение участвует в выражении доступа к члену встроенного оператора =, активный член объединения изменяется на член, участвующий в выражении (если тип имеет тривиальный конструктор, но поскольку это это код на C, у него есть тривиальный конструктор).

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

Даже если я использую размещение new для явного изменения того, какой член объединения активен, что должно быть подсказкой компилятору, что активный член изменился, GCC все равно генерирует код, который выводит 4321.

Это похоже на ошибку с GCC и Clang, предполагающую, что переключение активного члена объединения не может здесь произойти, потому что они не распознают возможность того, что p1, p2 и p3 все указывают на один и тот же объект.

GCC и Clang (и почти любой другой компилятор) поддерживают расширение C / C ++, где вы можете читать неактивный член объединения (получая в результате любое потенциально мусорное значение), но только если вы делаете этот доступ в доступе к члену выражение с участием союза. Если бы v1 не был активным членом, read_s1x не было бы определено поведение в соответствии с этим правилом, зависящим от реализации, потому что объединение не входит в выражение доступа к члену. Но поскольку v1 является активным участником, это не имеет значения.

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

person Myria    schedule 13.09.2017
comment
Я думаю, что фундаментальная проблема, с которой сталкиваются многие компиляторы, заключается в том, что в их промежуточном коде отсутствуют инструкции, которые заставили бы компилятор действовать так, как если бы он мог получить доступ к любому произвольному объекту типа T, без того, чтобы код фактически выполнял такой доступ. Было бы глупо иметь компилятор, генерирующий машинный код, который на самом деле читает и записывает через p3, но я не думаю, что промежуточный код компилятора имеет какой-либо другой способ выражения семантики, предписанной стандартом. - person supercat; 14.09.2017
comment
Если код делает что-то с p3, что потребует от компилятора фактического выполнения доступа, у компиляторов не будет проблем с распознаванием алиасинга. Проблема возникает, если компилятор решает оптимизировать код, который не должен генерировать никаких машинных инструкций, но повлияет на эффективные типы объектов или активные члены. Подобные проблемы возникают при использовании доступа типа char. Сторонники строгого псевдонима утверждают, что решение проблем с псевдонимом состоит в использовании указателей на символы или объединений, но это не поможет, если компиляторы оптимизируют такие вещи. - person supercat; 14.09.2017
comment
@supercat, вы можете найти множество примеров этих ловушек оптимизации в сети. - person 0___________; 14.09.2017
comment
@ PeterJ_01: Из того, что я видел, большинство таких примеров включает код, который, используя достаточно искаженную интерпретацию Стандарта, можно рассматривать как вызывающий UB. Моя цель здесь - получить пример, поведение которого четко, недвусмысленно и бесспорно определено Стандартом. - person supercat; 14.09.2017
comment
@supercat, вы решили разрешить оптимизацию, вы должны понимать эффекты или скомпилировать с отключенной оптимизацией - person 0___________; 14.09.2017
comment
@ PeterJ_01: Авторы gcc заявляют, что любой код, который дает сбой из-за их оптимизации, не работает, и ссылаются на Стандарт как на оправдание этой точки зрения. - person supercat; 14.09.2017
comment
@supercat, если вы считаете, что это ошибка оптимизатора, просто сообщите об этом. Вы также можете отказаться от использования gcc и clang - person 0___________; 14.09.2017
comment
@ PeterJ_01: Это не просто gcc и clang. Многие компиляторы на Godbolt ведут себя одинаково, что предполагает, что это поведение является намеренным, что заставляет меня задаться вопросом, интерпретируют ли авторы компилятора стандарт таким образом, чтобы оправдать свое поведение. - person supercat; 14.09.2017
comment
@supercat Вы сообщали об ошибке GCC и Clang? Если ты не хочешь, я могу это сделать. - person Myria; 15.09.2017
comment
@ Мирия: Пожалуйста. Проблема выходит далеко за рамки этого. Из всех компиляторов, которые я тестировал на Godbolt с включенным псевдонимом на основе типов (для некоторых я не мог понять, как его включить), только icc работал правильно. - person supercat; 15.09.2017
comment
@Myria Я уверился, что ваша аргументация имеет смысл, но все же у меня есть сомнения, когда я не говорю о стандарте C. См. Мой ответ на попытку интерпретации, которая согласуется с наблюдаемым поведением компилятора (для обсуждения) :) - person ; 15.09.2017
comment
@supercat Я сообщил об этом в GCC и Clang, используя ваш код. - person Myria; 16.09.2017
comment
@Myria: Между прочим, хотя конкретный пример был придуман, чтобы показать проблему как можно проще, проблема вполне могла возникнуть в реальном коде. Например, если код исследует элемент массива, использует цикл для преобразования всего в массиве в другой тип с тем же представлением, действует с некоторыми вещами как этот новый тип, а затем использует другой цикл для преобразования всего обратно, циклы, которые делают преобразование может быть оптимизировано. Я думаю, проблема в том, что Стандарт описывает эффективные типы в терминах объектов, а не lvalue, но нет никакого способа ... - person supercat; 18.09.2017
comment
... что gcc или clang могут представлять понятие этого кода, изменяющего эффективный тип X на Y, и что X может, но не обязательно, тот же X, идентифицированный каким-либо другим lvalue, за исключением физического чтения объекта как типа X и записать его как тип Y. - person supercat; 18.09.2017
comment
@Myria: Слышали ли вы что-нибудь в ответ на отчет об ошибке? - person supercat; 30.09.2017
comment
@Myria: Меня озадачивает, почему люди из этих списков считают определенные вещи несложными. Что касается правила общей начальной последовательности, например, скажите, что любой доступ к структуре, которая является частью полного объявления типа объединения , который виден в точке доступа, считается способным получить доступ к членам CIS других типов в этом союзе. Я не уверен, почему люди думают, что для этого нужно знать каждый тип, который кто-либо может объявить где угодно. - person supercat; 04.10.2017
comment
@Myria: Что касается конкретной проблемы, я думаю, что лучшим решением было бы, чтобы Стандарт распознавал различные режимы псевдонима, один из которых сделал бы эффективные типы постоянными и не позволял членам союза принимать их адреса (укажите [] в качестве операторов которые могут воздействовать на массивы без их разложения на типы указателей), один из которых потребует, чтобы все обращения обрабатывались как volatile, а некоторые из них будут находиться между этими крайностями. C используется для стольких целей, что любой единичный набор правил, который пытается удовлетворить их всем, в лучшем случае будет служить им всем плохо. - person supercat; 04.10.2017
comment
@Myria: Я бы с удовольствием поделился тем, что подозреваемый, если бы его распознать, стал бы самым популярным режимом псевдонима - тем, который было бы проще описать однозначно, чем настоящие правила, поддерживал бы большую часть кода, который в настоящее время потребовал бы -fno-strict-aliasing, и все же позволял бы большинство оптимизаций, которые теперь возможны, и многие другие, которые нет, - person supercat; 04.10.2017
comment
@Myria: я только что опубликовал еще один вопрос, который, как мне кажется, может представлять другую ошибку в gcc (ту, которую clang не разделяет). - person supercat; 05.10.2017

При строгой интерпретации стандарта этот код может не соответствовать. Сосредоточимся на тексте известного §6.5p7:

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

(курсив мой)

Ваши функции read_s1x() и write_s2x() делают противоположное тому, что я выделил жирным шрифтом выше в контексте всего вашего кода. Имея только этот абзац, вы можете сделать вывод, что это недопустимо: указателю на union s1s2 будет разрешено использовать псевдоним указателя на struct s1, но не наоборот.

Такая интерпретация, конечно, будет означать, что код должен работать так, как задумано, если вы «встроите» эти функции вручную в свой test(). Это действительно так, с gcc 6.2 для i686-w64-mingw32.


Добавляем два аргумента в пользу строгой интерпретации, представленной выше:

  • Хотя всегда разрешено использовать псевдоним любого указателя с помощью char *, массив символов не может быть псевдонимом какого-либо другого типа.

  • # P8 #
    # P9 #
    # P10 #
person Community    schedule 15.09.2017
comment
Применение оператора & к члену структуры или объединения дает указатель на этот тип элемента. Кроме того, если член структуры или объединения является массивом, с этим элементом будет невозможно сделать что-либо, кроме, используя указатель на его составляющий тип. Хотя я полагаю, что можно было бы прочитать стандарт таким образом, чтобы сказать, что применение оператора адресации к члену структуры или объединения даст указатель, который фактически не может использоваться для каких-либо целей, если он сначала не будет преобразован в символьный тип , было бы более разумно разрешить реализациям обрабатывать результат ... - person supercat; 15.09.2017
comment
... оператор & как имеющий тип, несовместимый с любым типом указателя, кроме void*, если только он не был применен к члену символьного типа (в этом случае он, естественно, приведет к char*). Однако, как это бывает, хотя я не тестировал их все на всех компиляторах, другие средства управления эффективными типами (например, чтение всех отдельных байтов объекта в отдельные объекты типа unsigned char, а затем запись всех отдельных байтов объекта) также не работают на gcc и clang. Я думаю, проблема в том, что в компиляторах отсутствует какая-либо концепция кода, которая могла бы ... - person supercat; 15.09.2017
comment
... изменить эффективный тип объекта или активного члена объединения, но это не требует создания каких-либо фактических загрузок или сохранений машинного кода. - person supercat; 15.09.2017

Я не читал стандарт, но играть с указателями в режиме строгого псевдонима (т.е. использовать -fstrict-alising) опасно. См. онлайн-документ gcc:

Обратите особое внимание на такой код:

union a_union {
  int i;
  double d;
};

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

Распространена практика чтения из другого члена союза, чем тот, которому в последний раз писали (называемый type-punning). Даже с -fstrict-aliasing допускается перенаправление типов при условии, что доступ к памяти осуществляется через тип объединения. Итак, приведенный выше код работает так, как ожидалось. См. Перечисления объединений структур и реализация битовых полей. Однако этот код не может:

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

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

int f() {
  double d = 3.0;
  return ((union a_union *) &d)->i;
}

Опция -fstrict-aliasing включается на уровнях -O2, -O3, -Os.

Нашли что-нибудь похожее во втором примере, да?

person walkerlala    schedule 27.10.2017
comment
Обратите внимание, что мой пример принимает адрес члена объединения только после того, как записал этот член, и отказывается от этого указателя перед записью другого члена. Проблема в том, что оба gcc и clang пытаются применить две конфликтующие оптимизации: исключение кода, который читал бы один член объединения, а затем записывал точно такой же битовый шаблон в другой, было бы прекрасной оптимизацией изолированно, но сбивает более поздние оптимизации. Что трагично, так это то, что авторы стандарта C не лучше сформулировали идею о том, что компиляторы могут обычно предполагать, что псевдонимов нет , когда нет доказательств этого ... - person supercat; 27.10.2017
comment
... но этим качественным компиляторам не следует упускать из виду наличие псевдонимов в полезных случаях. Хотя в Стандарте прямо не говорится, что взятие адреса члена союза (активного или неактивного) должно быть признано доказательством псевдонима, наиболее вероятная причина в том, что они думали, что это было очевидным. Если бы компилятор распознал принятие адресов членов объединения как свидетельство псевдонима, пропуск последовательности чтения-записи не имел бы значения. - person supercat; 27.10.2017
comment
@supercat вы берете адрес члена объединения (то есть адрес структуры s2 из объединения s1s2), и это незаконно в соответствии с приведенным выше примером. - person walkerlala; 28.10.2017
comment
@supercat Я прочитал почти все комментарии, которые вы предоставили в другом ответе, но все же я не понимаю, за что вы спорите. Какова ваша позиция? Вы думаете, что и gcc, и clang делают неправильные вещи? Или вы просто вините стандарт Си? - person walkerlala; 28.10.2017
comment
Поведение clang и gcc здесь однозначно несовместимо. Кроме того, в опубликованном Обосновании Стандарта прямо указано, что вместо того, чтобы пытаться обязать все необходимое для того, чтобы сделать реализацию полезной, ожидается, что если реализации будут выполнять то, что требуется Стандартом, они естественным образом сделают другие необходимые вещи. чтобы сделать их полезными. Чтобы реализация могла эффективно поддерживать стандарт, она должна иметь способ приспособления действий, которые могут изменить активный тип объединения или эффективный тип хранилища, не зная, ... - person supercat; 28.10.2017
comment
... действительно изменит это или нет. Реализация с такой способностью могла бы легче всего поддерживать стандарт, просто рассматривая член-адрес-объединение как имеющий такую ​​семантику. Я не думаю, что авторы Стандарта учли возможность того, что реализации могут стремиться избежать этого и вместо этого использовать более сложные и менее полезные способы удовлетворения требований Стандарта. - person supercat; 28.10.2017

Речь идет не о соответствии или несоответствии - это одна из «ловушек» оптимизации. Все ваши структуры данных были оптимизированы, и вы передаете один и тот же указатель на оптимизированные данные, поэтому дерево выполнения сокращается до простого printf значения.

  sub rsp, 8
  mov esi, 4321
  mov edi, OFFSET FLAT:.LC0
  xor eax, eax
  call printf
  xor eax, eax
  add rsp, 8
  ret

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

int test(union s1s2 *p1, union s1s2 *p2, volatile union s1s2 *p3)
/* ....*/

main:
  sub rsp, 8
  mov esi, 1234
  mov edi, OFFSET FLAT:.LC0
  xor eax, eax
  call printf
  xor eax, eax
  add rsp, 8
  ret

это довольно тривиальный тест, только искусственно усложненный.

person 0___________    schedule 13.09.2017
comment
Конечно, усложнение кода приведет к правильной его обработке компиляторами. Однако я не вижу в Стандарте ничего, что позволило бы соответствующим компиляторам требовать такой бесполезный код, и сомневаюсь в разумности оптимизатора, требующего от программистов добавления кода, который, по их мнению, бесполезен. - person supercat; 14.09.2017
comment
@supercat, использующий этот логин, не соответствует любой оптимизации, которая изменяет или сокращает код (например, не вызывает функцию). Это ловушка - все локальные переменные, ни одна из них не используется и т. Д. Написав код таким образом и допуская оптимизацию, программист должен учитывать эти эффекты. - person 0___________; 14.09.2017
comment
Измените свой код на: int test(union s1s2 *p1, union s1s2 *p2, union s1s2 *p3) { if (read_s1x(&p1->v1)) { unsigned short temp; temp = p3->v1.x; p3->v2.x = temp; write_s2x(&p2->v2,1234); temp = p3->v2.x; p3->v1.x = temp; printf("%d\n",p3->v2.x); } return read_s1x(&p1->v1); } - person 0___________; 14.09.2017
comment
Программа, которую я опубликовал, была намеренно придумана, чтобы заставить компиляторы делать незаконные оптимизации, но это не делает их оптимизацию согласованной. Если бы разработчики компилятора хотели указать, что определенные оптимизации делают код несоответствующим, и согласились с тем, что несовместимость с такими оптимизациями не означает, что код сломан, а просто означает, что код и оптимизатор не подходят друг для друга, это было бы хорошо. , хотя лучший компилятор должен стараться максимизировать совместимость с существующим кодом. - person supercat; 14.09.2017
comment
@supercat Я знаю, что это умышленное злоупотребление :). Никто вменяемый не стал бы писать подобное. Какова настоящая причина поста? Сообщение об ошибке? Что-то другое. Логический ответ: оптимизация кода муравьев может иметь некоторые побочные эффекты, и программист должен иметь это в виду. - person 0___________; 14.09.2017
comment
Вопрос в том, есть ли в Стандарте что-нибудь, что могло бы оправдать такое поведение, которое кажется широко распространенным, или же части Стандарта, относящиеся к алиасингу, следует рассматривать как бессмысленные, поскольку им все равно никто не следует? - person supercat; 14.09.2017