Почему GCC и Clang не делают эту оптимизацию псевдонимов?

У меня есть случай, когда друг применяет объект небазового класса типа «Базовый» в объект типа класса «Производный», где «Производный» является производным классом «Базового» и добавляет только функции, но не данные. В приведенном ниже коде я добавил элемент данных x в производный класс.

struct A {
  int a;
};

struct B : A {
  // int x;
  int x;
};

A a;

int g(B *b) {
   a.a = 10;
   b->a++;
   return a.a;
}

При включенном строгом анализе псевдонимов GCC (также Clang) всегда возвращает 10, а не 11, потому что b никогда не может указывать на a в четко определенном коде. Однако, если я удалю B::x (что на самом деле имеет место в коде моего друга), выходной ассемблерный код GCC не оптимизирует обратный доступ к a.a и перезагружает значение из памяти. Таким образом, код моего друга, который вызывает g, «работает» на GCC (как он и предполагал), хотя я думаю, что он все еще имеет неопределенное поведение.

g((B*)&a);

По сути, в тех же двух случаях GCC оптимизирует один случай и не оптимизирует другой случай. Это потому, что b может тогда юридически указывать на a? Или это потому, что GCC просто хочет не ломать реальный код?


Я проверил ответ, в котором говорится

Если вы удалите B::x, тогда B будет соответствовать требованиям 9p7 для класса со стандартной компоновкой, и доступ станет совершенно четко определенным, поскольку эти два типа совместимы с компоновкой, 9.2p17.

С двумя перечислениями, совместимыми с макетом

enum A : int { X, Y };
enum B : int { Z };

A a;

int g(B *b) {
   a = Y;
   *b = Z;
   return a;
}

Выходные данные ассемблера для g возвращают 1, а не 0, несмотря на то, что A и B совместимы с компоновкой (7.2p8).


Итак, мой дополнительный вопрос (цитирую ответ): "два класса с абсолютно одинаковым макетом могут считаться "почти одинаковыми" и не подлежат оптимизации".. Может ли кто-нибудь предоставить доказательства этого для GCC или Clang?


person Johannes Schaub - litb    schedule 19.06.2013    source источник
comment
Где g() называется? С g(&a)? Я не думаю, что использование глобальной переменной и указателя на одну и ту же глобальную переменную в одной и той же функции — это нечто иное, как поведение undefined. А, как мы знаем, неопределенное поведение может привести к самым разным вещам, включая то, что вы ожидали.   -  person Mats Petersson    schedule 19.06.2013
comment
@MatsPetersson g называется как g((B*)&a). В моем тестовом фрагменте он не вызывается (мне нужен был только вывод ассемблера для g)   -  person Johannes Schaub - litb    schedule 19.06.2013
comment
У вас есть глава и стих о неопределенном поведении?   -  person doctorlove    schedule 19.06.2013
comment
Можете ли вы уточнить отказ от обратного доступа, например, опубликовав соответствующие выходные данные сборки?   -  person slaphappy    schedule 19.06.2013
comment
@doctorlove: это доступ к объекту через выражение несовместимого типа. Доступ к a можно получить через выражение типа A, но не типа B.   -  person MSalters    schedule 19.06.2013
comment
Можете ли вы уточнить, что (ли?) Ваш вопрос конкретно спрашивает, почему g++ ведет себя так, как он ведет перед лицом этого неопределенного поведения? Другими словами, это вопрос компилятора или вопрос языка?   -  person Mark B    schedule 19.06.2013
comment
@MarkB в некотором роде и то, и другое. если вы обнаружите, что это не неопределенное поведение, мне не терпится это услышать.   -  person Johannes Schaub - litb    schedule 19.06.2013
comment
Ваш вопрос следует направить в список рассылки GCC. Почему некоторые реализации делают это в случае неопределенного поведения? Потому что детали реализации добавляются к этому через какое-то внутреннее представление. Кроме того, если это GCC-изм, он может исчезнуть при использовании -std=c++11 вместо -std=gnu++11.   -  person rubenvb    schedule 19.06.2013
comment
@doctorlove насколько я понимаю, ни одна из пуль 3.10p10 не совпадает   -  person Johannes Schaub - litb    schedule 19.06.2013
comment
Мне не нравится менталитет некоторых, когда возникают вопросы о неопределенном поведении. Неопределенное поведение не означает, что разработчики компиляторов отключают свой мозг и играют в кости. И это также не означает, что им все равно.   -  person Johannes Schaub - litb    schedule 19.06.2013
comment
@Johannes Но почему их это должно волновать?   -  person Ivan Aksamentov - Drop    schedule 19.06.2013
comment
Я согласен, 3.10p10 было тем, что я нашел - к сожалению, мой графический драйвер решил перестать работать. И я согласен, undefined просто означает, что производитель компилятора должен сделать что-то максимально значимое, но в некоторых ситуациях ужасно сложно что-то сделать, и компилятору нужно понять, что aa и b-›a здесь одно и то же. [или не понимать, но перестраховаться], им пришлось бы хранить и загружать данные без необходимости, на тот случай, если ваш объект b, который не должен быть указателем на объект a, на самом деле тот же самый a.a. Люди предпочитают быстрый код, когда он написан правильно.   -  person Mats Petersson    schedule 19.06.2013
comment
@MatsPetersson, извините, я думаю, что у меня была запутанная формулировка в моем вопросе. Я перефразировал, надеюсь, станет понятнее. Конечно, сама функция не имеет неопределенного поведения.   -  person Johannes Schaub - litb    schedule 19.06.2013
comment
Возможно, из-за того, что эти два случая заканчиваются двумя разными путями при анализе возможного алиасинга, и компилятор считает, что эти объекты имеют одинаковую структуру, поэтому может случиться так, что они на самом деле используются взаимозаменяемо, тогда как в этом случае ясно, что B не может он использовал для объекта A, поэтому мне не нужно это «исправлять». Я не написал ни строчки в gcc/g++ (я отправил небольшой патч кому-то, работающему над вариантом x86, но я думаю, что он был переработан), так что я просто предполагаю задействованную логику.   -  person Mats Petersson    schedule 19.06.2013
comment
@JohannesSchaub-litb Неопределенное поведение делает означает, что некоторые авторы компиляторов (по крайней мере, с g++) попытаются сделать так, чтобы код не работал во время выполнения. (Я не думаю, что это проблема здесь, но я видел это в других случаях.)   -  person James Kanze    schedule 19.06.2013
comment
@JohannesSchaub-litb Что касается того, почему компилятор делает две разные вещи: Бен Фойгт указал в своем ответе на одну возможную причину. Я не уверен, что согласен с логикой, но разработчики g++ могут. В этом случае, если макет B совместим с A, они считают, что b в g может быть глобальным псевдонимом a, а если нет, то нет. (И, конечно же, поскольку их поведение явно не определено в C++03, им не нужно делать это условно.)   -  person James Kanze    schedule 19.06.2013
comment
@Mats: В последнем случае, когда B имеет стандартный макет, ему разрешено создавать псевдонимы, и поэтому компилятор должен фактически генерировать доступы.   -  person Ben Voigt    schedule 19.06.2013
comment
@BenVoigt: Итак, действительно, анализатор псевдонимов использует два разных пути в зависимости от того, один и тот же макет, а не один и тот же макет. Хорошо знать.   -  person Mats Petersson    schedule 19.06.2013
comment
Спросить, почему оптимизация не была применена, — сложный вопрос. Основной ответ заключается в том, что реализация этого конкретного компилятора не выполняет эту оптимизацию в этом конкретном случае, возможно, эвристика не обнаружила этого, или что это никогда не рассматривалось, или... Более интересный вопрос как раз наоборот : гарантировано ли, что он всегда будет перезагружаться из памяти? И я не верю, что это так, но я думаю, что вы согласны со мной здесь, верно?   -  person David Rodríguez - dribeas    schedule 01.07.2013
comment
@JohannesSchaub-litb: Вы воспроизвели это, предотвратив встраивание g? Если нет, попробуйте сделать это. (Один из способов сделать это — сохранить адрес g в изменчивой переменной-указателе функции, а затем вызвать ее оттуда.)   -  person user541686    schedule 01.08.2013
comment
@Mehrdad в моем тестовом примере я не звонил g. Я только что скомпилировал его отдельно без функции main.   -  person Johannes Schaub - litb    schedule 04.02.2017
comment
@JohannesSchaub-litb С какими версиями компилятора возникает эта проблема? В конце концов, 13-й год был четыре года назад.   -  person Iwillnotexist Idonotexist    schedule 06.02.2017
comment
Я тестировал на GCC, clang, ICC, все выдает одну и ту же сборку, как описывает JohannesSchaub-litb. Я также пробую разные версии этих компиляторов. Все трое согласны. Если B не объявляет какой-либо нестатический член данных или какую-либо виртуальную функцию, то компилятор предполагает, что *b может быть псевдонимом для a. Может ранняя оптимизация заменяет производный класс, не добавляющий никаких данных (в т.ч. vptr) базовым?   -  person Oliv    schedule 07.02.2017
comment
@Oliv, это может быть случай, когда две структуры \ классы отличаются последними членами только при обнаружении компилятором. Например. законно использовать такую ​​пару в объединении и получать доступ к общим членам каждого из них, независимо от того, какая структура была написана последней. Мы можем увидеть сбой строгого сглаживания, потому что компилятор столкнулся с пограничным случаем.   -  person Swift - Friday Pie    schedule 07.02.2017
comment
@Swift, я сделал новые тесты: если A полиморфен, а B происходит от A, что бы вы ни объявляли внутри B, компилятор никогда не выполняет строгую оптимизацию псевдонимов, каким бы ни был компилятор и какая бы его версия. Так что это определенно не связано с глубиной совместимости макетов. Я прочитал ваш ответ. Я все еще не удовлетворен на 100%, я также ожидал ответа на вопрос: почему все компиляторы кажутся такими пессимистичными при выполнении строгой оптимизации сглаживания?   -  person Oliv    schedule 07.02.2017
comment
Отличный вопрос, потому что он уникален и интересен. Чтобы получить хороший ответ, я думаю, вы должны опубликовать его в списке рассылки gcc, отправив электронное письмо по адресу [email protected]. Затем, если вы получите удовлетворительный ответ от них, вы можете опубликовать его здесь.   -  person Hadi Brais    schedule 09.02.2017
comment
@Oliv, было ли рассматриваемое поле введено в A или в его базовом классе?   -  person Swift - Friday Pie    schedule 10.02.2017
comment
@Swift, вы найдете код, перейдя по ссылке ниже (надеюсь, эта ссылка будет работать, когда вы нажмете на нее!). Есть функции g и gr. аргумент указателя gr ограничен, что требует строгой оптимизации псевдонимов, поэтому вы можете сравнить сборку, сгенерированную со строгим псевдонимом или без него: [godbolt.org/g/JQvuOP]   -  person Oliv    schedule 10.02.2017
comment
@Oliv У меня сейчас мало времени, но я только что проверил ваш код с помощью clang++ 4. Разница в переводе возникает в InstCombine-Transformation (вы можете сравнить ./opt test.bc -S с ./opt -instcombine тест.bc-S). Этот шаг удаляет битовую передачу и загрузку и сразу начинает возвращать 10. Вы найдете этот файл в исходной библиотеке clang/Transforms/InstCombine/InstructionCombining.cpp. За несколько секунд, которые я просмотрел в файле, я не смог найти место, где имеет значение алиасинг.   -  person overseas    schedule 10.02.2017


Ответы (4)


Если вы удалите B::x, то B будет соответствовать требованиям 9p7 для класса стандартного макета, и доступ станет совершенно четко определенным, поскольку эти два типа совместимы по макету, 9.2 p17, и оба члена имеют один и тот же тип.


Класс стандартной компоновки — это класс, который:

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

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

person Ben Voigt    schedule 19.06.2013
comment
Ой, подождите, пункт 9.2p19 не тот пункт, потому что он говорит о союзах. Но правило ЕСТЬ. Все еще ищу. - person Ben Voigt; 19.06.2013
comment
Ок, думаю 9.2p17 и 5.2.10p7 тут уместны. - person Ben Voigt; 19.06.2013
comment
Хорошо, они совместимы с макетом, но это все еще не позволяет получить доступ к одному с использованием lvalue другого. 3.10p10 не упоминает совместимость макета. Есть ли что-то еще, связанное с совместимостью макета, что делает его определенным? - person Johannes Schaub - litb; 19.06.2013
comment
Это С++11. В C++03 поведение не определено. (Но, конечно, одна из причин, по которой авторы стандартов приняли его определение, заключается в том, что он действительно работал во всех реализациях.) - person James Kanze; 19.06.2013
comment
@James: Но я считаю, что этот вопрос касается стандарта C++, то есть C++11. Это предположение, если специально не упомянута другая версия. - person Ben Voigt; 19.06.2013
comment
@JohannesSchaub-litb: вы обращаетесь к объекту динамического типа int через lvalue типа int& через тип структуры, содержащий int. 3.10p10 говорит, что все в порядке. - person Ben Voigt; 19.06.2013
comment
@BenVoigt Я не знаю. Он специально спросил, почему g++ может делать разные вещи в двух случаях. И g++ еще не реализует C++11 (хотя этот эффект может быть связан с экспериментальной предварительной реализацией). - person James Kanze; 19.06.2013
comment
@James: я ответил, почему поведение отличается. Когда B имеет нестатические данные-члены, он перестает быть классом стандартного макета и, следовательно, больше не совместим по макету с A или чем-либо еще. Что позволяет большую оптимизацию. - person Ben Voigt; 19.06.2013
comment
@BenVoigt Я провел тест с перечислениями, и GCC оптимизирует его с помощью псевдонимов. Вопрос отредактирован. - person Johannes Schaub - litb; 19.06.2013
comment
@JohannesSchaub-litb: отредактировал мой ответ, чтобы явно указать требование, чтобы оба члена имели один и тот же тип (что требуется строгим правилом псевдонимов). Я думал, что это очевидно, и я упомянул об этом в своем предыдущем комментарии. - person Ben Voigt; 19.06.2013
comment
@BenVoigt, тогда какая разница остается по сравнению с перечислением? В обоих случаях у нас есть два разных типа (B и A в одном случае — это разные классы, а в другом — разные перечисления). Разница не имеет значения, потому что они совместимы по макету (по-вашему,?). Затем в случае структуры мы должны дополнительно проверить, что член имеет тот же тип. В случае enum у нас нет другого типа для проверки. Поэтому, если один из этих методов работает, я ожидаю, что случай перечисления будет работать. Почему все наоборот? - person Johannes Schaub - litb; 19.06.2013
comment
@BenVoigt, эта пуля не предназначена для таких случаев. В противном случае вы могли бы использовать произвольную структуру для доступа к int, если бы только структура также содержала этот int. Этот маркер предназначен для предотвращения кэширования всей структуры, если вы измените один из ее элементов, используя указатель на этот элемент (и наоборот). См. обоснование C. - person Johannes Schaub - litb; 19.06.2013
comment
Тем не менее, давайте предположим, что пуля применима, но тогда совместимость компоновки будет совершенно неуместной — ведь структура содержит член типа int! - person Johannes Schaub - litb; 19.06.2013
comment
@JohannesSchaub-litb: для строгого псевдонима требуется, чтобы вы использовали тип (здесь int&), который соответствует объекту в местоположении. Совместимость макетов гарантирует, что вы смотрите в нужное место, т. е. p->*&A::x и p->*&B::x разрешаются в один и тот же объект, с одинаковым выравниванием. В случае enum совместимость макета снова гарантирует правильное расположение и правильное выравнивание, но у вас неправильный тип. - person Ben Voigt; 20.06.2013
comment
@BenVoigt, пожалуйста, потерпите меня, но я до сих пор не понимаю, почему в случае класса B и A нет конфликта псевдонимов. Очевидно, вы говорите, что анализ псевдонимов не проверяет типы классов на соответствие, а только те члены, к которым осуществляется доступ. Но если это так, то зачем делать список классов? В нем говорится, что тип является (возможно, cv-квалифицированным) типом базового класса динамического типа объекта - это явно относится к типам классов в доступе. Или вы говорите, что a.b не представляет собой доступ к значению объекта с помощью lvalue типа decltype(a)? - person Johannes Schaub - litb; 20.06.2013
comment
Другими словами, вы говорите, что это будет нарушением псевдонимов: *b = B() (при условии, что тело вопроса моей функции и что b фактически указывает на A). Тогда b->x++ допустимо (поскольку окончательное lvalue имеет тип int, а выражение объекта не имеет отношения к анализу алиасинга), но *b = B() нет (поскольку окончательное lvalue имеет тип B)? Извините, но я пока совсем не понимаю, что вы говорите. - person Johannes Schaub - litb; 20.06.2013
comment
@JohannesSchaub-litb маркированный список - одна из величайших загадок C++. - person curiousguy; 06.07.2013
comment
В строгом правиле псевдонимов нет исключений для классов, совместимых с макетом, поэтому мне не ясно, как, по вашему мнению, это отвечает на вопрос. Предложил бы отредактировать еще несколько объяснений в ответе. - person M.M; 11.02.2017
comment
@MM, кроме того, ответ Swifts показывает, что даже структуры, совместимые с макетом, оптимизированы для псевдонимов, если они не находятся в отношениях наследования. Так что этот ответ кажется неверным (?) - person Johannes Schaub - litb; 11.02.2017

Неопределенное поведение включает случаи, когда оно работает, даже если не должно.

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

union Packet {
   struct Header {
   short type;
   short size;  
   } header;
   struct Data {
   short type;
   short size;  
   unsigned char data[MAX_DATA_SIZE];
   } data;
}

Это строго ограничено объединениями, но многие компиляторы поддерживают это как своего рода расширение, при условии, что «неполный» тип будет заканчиваться массивом неопределенного размера. Если вы удалите дополнительный статический не-член из дочернего класса, он действительно станет тривиальным и совместимым с макетом, что позволяет использовать псевдонимы?

struct A {
  int a;
};

struct B  {
  int a;
  //int x;
};

A a;

int g(B *b) {
   a.a = 10;
   b->a++;
   return a.a;
}

Тем не менее, по-прежнему выполняет оптимизацию псевдонимов. В вашем случае с таким же количеством нестатических членов предполагается, что наиболее производный класс будет таким же, как базовый класс. Давайте инвертируем порядок:

#include <vector>
#include <iostream>

struct A {
  int a;
};

struct B : A  {
  int x;
};

B a;

int g(A *b) {
   a.a = 10;
   b->a++;
   return a.a;
}

int main()
{
    std::cout << g((A*)&a);
}

Это возвращает 11, как и ожидалось, поскольку B явно также является A, в отличие от исходной попытки. Давай играть дальше

struct A {
  int a;
};

struct B : A {
    int  foo() { return a;}
};

Не вызовет оптимизацию алиасинга, если только foo() не является виртуальным. Добавление нестатического или константного члена в B приведет к ответу «10», добавление нетривиального конструктора или статического — нет.

PS. Во втором примере

enum A : int { X, Y };
enum B : int { Z };

Совместимость макетов между этими двумя здесь определяется C++ 14, и они не совместимы с базовым типом (но конвертируемы). хотя что-то вроде

 enum A a = Y;
 enum B b = (B*)a;

может привести к неопределенному поведению, как если бы вы попытались отобразить float с произвольным 32-битным значением.

person Swift - Friday Pie    schedule 07.02.2017
comment
Это меня смущает: тем не менее, по-прежнему выполняется оптимизация псевдонимов. . Поэтому, когда A и B являются несвязанными классами, которые совместимы по макету (фактически, структурно эквивалентны!), тогда он выполняет оптимизацию. Но если все равно, а есть просто производная от базы связь, как в моем тестовом примере, то оптимизация не выполняется. Это указывает на то, что дело не только в совместимости компоновки или структурной эквивалентности, но и в наследовании, верно? Или я неправильно понимаю ответ? - person Johannes Schaub - litb; 11.02.2017
comment
Награждение за это, так как это был самый полезный ответ до сих пор. - person Johannes Schaub - litb; 11.02.2017
comment
@Johannes Schaub - litb По иронии судьбы, мои студенты, когда им бросали вызов с этим кодом, указывали на (B *) & A и говорили, что это неправильно, основывая свое понимание точно на отношении базового и производного классов (и они знают о базовом механизме адресации) Если стандарт не регулирует это (или мы этого не обнаружили), то все равно имеет смысл с точки зрения строгого правила алиасинга интерпретировать разные типы в манере полиморфизма? A не является B, если они не связаны, B также является A, но не равен, и указатель на A может быть указателем на B, когда A является базовым классом B, - person Swift - Friday Pie; 12.02.2017

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

Теперь, если вы активируете флаг строгого алиасинга, вы позволяете компилятору оптимизировать код для UB. Как использовать этот UB, зависит от компилятора. Вы можете увидеть ответы на этот вопрос.

Что касается gcc, документация для -fstrict-aliasing показывает, что он может оптимизировать на основе:

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

Мне не удалось найти определение для «почти одинакового», но два класса с абсолютно одинаковым макетом могут считаться «почти одинаковыми», и они не учитываются при оптимизации.

person J. Calleja    schedule 11.02.2017
comment
Функция g сама по себе не имеет неопределенного поведения, поэтому это не может объяснить разницу в выводах оптимизатора двух компиляторов. - person Johannes Schaub - litb; 11.02.2017
comment
Когда вы активируете флаг -fstrict-alisiang, вы сообщаете компилятору, что он может оптимизировать, предполагая, что вы не делаете ничего, что приводит к UB из-за алиасинга. Вы можете добраться до b == &a только через UB. Таким образом, оптимизатор замечает, что b != &a и g() всегда будут возвращать 10. Поэтому я подумал, что настоящий вопрос заключается в том, почему оптимизатор не всегда использует эту оптимизацию, в зависимости от объявления B. По стандарту ничего не нашел, но выяснил, что в документации говорится о нестандартной концепции почти тех же типов. - person J. Calleja; 12.02.2017

Я считаю, что следующее является допустимым С++ (без вызова UB):

#include <new>

struct A {
  int a;
};

struct B : A {
  // int x;
};

static A a;

int g(B *b);
int g(B *b) {
   a.a = 10;
   b->a++;
   return a.a;
}

int f();
int f() {
  auto p = new (&a) B{};
  return g(p);
}

потому что (глобальный) a всегда ссылается на объект типа A (даже если он является подобъектом B-объекта после вызова f()), а p указывает на объект типа B.

Если вы отметите a как длительность хранения static (как я сделал выше), все протестированные мной компиляторы с радостью применят строгое сглаживание и оптимизируют для возврата 10.

С другой стороны, если вы пометите g() __attribute__((noinline)) или добавите функцию h(), которая возвращает указатель на a

A* h();
A* h() { return &a; }

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

person Tobias    schedule 03.05.2017