«Наблюдаемое поведение» и свобода компилятора для исключения / преобразования элементов кода c ++

Прочитав эту дискуссию, я понял, что почти полностью не понимаю этого вопроса 🙂

Поскольку описание абстрактной машины C ++ недостаточно строго (сравнивая, например, с спецификацией JVM), и если точный ответ невозможен, я предпочел бы получить неофициальные разъяснения о правилах, которые разумные «хорошие» (не вредоносные ).

Ключевая концепция части 1.9 свободы реализации Стандартной адресации называется так: если правило:

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

Термин «наблюдаемое поведение», согласно стандарту (I cite n3092), означает следующее:

– Доступ к неустойчивым объектам оценивается строго в соответствии с правилами абстрактной машины.

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

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

Итак, грубо говоря, порядок и операнды операций нестабильного доступа и операций io должны быть сохранены; реализация может вносить произвольные изменения в программу, которые сохраняют эти инварианты (по сравнению с некоторым допустимым поведением абстрактной машины c ++)

  1. Разумно ли ожидать, что не-вредоносная реализация значительно расширит операции io (например, любой системный вызов из кода пользователя рассматривается как таковая операция)? (Например, блокировка / разблокировка мьютекса RAII не будет выбрасываться компилятором, если shell RAII не содержит летучих)

  2. Насколько глубоко «поведенческое наблюдение» должно опускаться с пользовательского уровня на уровне C ++ в библиотечные / системные вызовы? Вопрос состоит в том, что, конечно, речь идет только о вызовах библиотек, которые не предназначены для использования io / volatile доступа с точки зрения пользователя (например, в качестве новых операций / удаления), но могут (и обычно) получать доступ к летуатилям или io в реализации библиотеки / системы. Должен ли компилятор обрабатывать такие вызовы с точки зрения пользователя (и рассматривать такие побочные эффекты как не наблюдаемые ) или с точки зрения «библиотеки» (и рассматривать побочные эффекты как наблюдаемые )?

  3. Если мне нужно предотвратить компилятор из-за исключения кода, рекомендуется ли не задавать все вышеперечисленные вопросы и просто добавлять (возможно, поддельные) операции с изменчивым доступом (переносить действия, необходимые для волатильных методов, и вызывать их на летучих экземплярах моего собственные classы) в любом случае, что кажется подозрительным?

  4. Или я полностью ошибаюсь, и компилятор не разрешает удалить любой код c ++, за исключением случаев, явно упомянутых стандартом (как удаление копии)

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

Что касается глубины этого, то это зависит от реализации библиотеки. В gcc стандартная библиотека C использует атрибуты компилятора для информирования компилятора о потенциальных побочных эффектах (или их отсутствии). Например, strlen помечен как чистый атрибут, который позволяет компилятору преобразовать этот код:

 char p[] = "Hi there\n"; for ( int i = 0; i < strlen(p); ++i ) std::cout << p[i]; 

в

 char * p = get_string(); int __length = strlen(p); for ( int i = 0; i < __length; ++i ) std::cout << p[i]; 

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

То есть, в общем, компилятор не будет удалять код, если он не может доказать, что он не имеет побочных эффектов, то есть не повлияет на результат программы. Обратите внимание, что это относится не только к volatile и io, поскольку любое изменение переменной может иметь наблюдаемое поведение в более позднее время.

Что касается вопроса 3, компилятор удалит ваш код только в том случае, если программа ведет себя точно так, как если бы код присутствовал (исключение копии - исключение), поэтому вам даже не нужно заботиться о том, удаляет его компилятор или нет. Что касается вопроса 4, то правило as-if : Если результат неявного рефакторинга, сделанный компилятором, дает тот же результат, тогда он может свободно выполнить изменение. Рассматривать:

 unsigned int fact = 1; for ( unsigned int i = 1; i < 5; ++i ) fact *= i; 

Компилятор может свободно заменить этот код:

 unsigned int fact = 120; // I think the math is correct... imagine it is 

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

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

РЕДАКТИРОВАТЬ

@Konrad поднимает действительно хороший момент в отношении исходного примера, который у меня был с strlen : как компилятор знает, что вызовы strlen могут быть отменены? И ответ заключается в том, что в исходном примере он не может, и, следовательно, он не может преодолеть вызовы. Ничто не говорит компилятору, что указатель, возвращаемый get_string() , не относится к памяти, которая изменяется в другом месте. Я исправил пример использования локального массива.

В модифицированном примере массив является локальным, и компилятор может проверить, нет ли других указателей, которые ссылаются на одну и ту же память. strlen принимает указатель на const и поэтому обещает не изменять содержащуюся память, а функция чиста, поэтому обещает не изменять какое-либо другое состояние. Массив не модифицируется внутри конструкции цикла и собирает всю эту информацию, которую компилятор может определить, что достаточно одного вызова для strlen . Без чистого спецификатора компилятор не может знать, будет ли результат strlen отличаться в разных вызовах и должен его вызвать.

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

Наиболее важным из них является, вероятно, тот факт, что если программа имеет неопределенное поведение, компилятор может делать абсолютно все. Все ставки сделаны. Компиляторы могут и могут использовать потенциальное неопределенное поведение для оптимизации: например, если код содержит что-то вроде *p = (*q) ++ , компилятор может заключить, что p и q не являются псевдонимами для одной и той же переменной.

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

Что касается volatile , то стаднард говорит, что доступ к неустойчивым объектам является наблюдаемым поведением, но он оставляет смысл «доступа» до реализации. На практике вы не можете рассчитывать на volatile наши дни; фактический доступ к изменчивым объектам может появиться внешнему наблюдателю в другом порядке, чем в программе. (Это, возможно, нарушает намерение стандарта, по крайней мере. Это, однако, фактическая ситуация с большинством современных компиляторов, работающая на современной архитектуре.)

Большинство реализаций обрабатывают все системные вызовы как «IO». Что касается мьютексов, то, конечно же, что касается C ++ 03, то, как только вы начинаете второй stream, у вас есть неопределенное поведение (с точки зрения C ++ – Posix или Windows определяют его) и в C ++ 11, примитивы синхронизации являются частью языка и ограничивают набор возможных выходов. (Компилятор, конечно, может исключить синхронизацию, если он может доказать, что они не нужны.)

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

Что касается вашего третьего вопроса: я думаю, вы смотрите на него с неправильного угла. Компиляторы не «исключают» код, и никакой конкретный оператор в программе не привязан к определенному блоку кода. Ваша программа (полная программа) определяет определенную семантику, а компилятор должен делать то, что создает исполняемую программу, имеющую семантику. Наиболее очевидным решением для писателя-компилятора является принятие каждого оператора отдельно и генерация кода для него, но это точка зрения автора компилятора, а не ваша. Вы вставляете исходный код и получаете исполняемый файл; но множество заявлений не приводят к какому-либо коду, и даже для тех, кто это делает, необязательно отношения один к одному. В этом смысле идея «предотвращения исключения кода» не имеет смысла: ваша программа имеет семантику, указанную стандартом, и все, что вы можете попросить (и все, что вас должно заинтересовать), состоит в том, что окончательный исполняемые имеют эти семантики. (Ваша четвертая точка похожа: компилятор не «удаляет» любой код.)

Я не могу говорить о том, что должны делать компиляторы, но вот что делают некоторые компиляторы

 #include  int main() { std::array a; for(size_t p = 0; p<5; ++p) a[p] = 2*p; } 

assembly с gcc 4.5.2:

 main: xorl %eax, %eax ret 

замена массива на вектор показывает, что new / delete не подлежат устранению:

 #include  int main() { std::vector a(5); for(size_t p = 0; p<5; ++p) a[p] = 2*p; } 

assembly с gcc 4.5.2:

 main: subq $8, %rsp movl $20, %edi call _Znwm # operator new(unsigned long) movl $0, (%rax) movl $2, 4(%rax) movq %rax, %rdi movl $4, 8(%rax) movl $6, 12(%rax) movl $8, 16(%rax) call _ZdlPv # operator delete(void*) xorl %eax, %eax addq $8, %rsp ret 

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

1. Разумно ли ожидать, что не-вредоносная реализация значительно расширит операции io

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

2. Насколько глубоко «поведенческое наблюдение» должно опускаться с пользовательского уровня на уровне C ++ в библиотечные / системные вызовы?

Как можно глубже. Используя текущий стандарт C ++, компилятор не может смотреть за библиотекой со значением static library , то есть вызовами, которые нацелены на функцию внутри некоторых вызовов «.a-» или «.lib» , поэтому предполагаются побочные эффекты.

Используя традиционную модель компиляции с несколькими объектными файлами, компилятор даже не может смотреть внешние вызовы. Однако оптимизация по единицам компиляции может быть выполнена во время ссылки .

Btw, некоторые компиляторы имеют расширение, чтобы рассказать об чистых функциях. Из документации gcc :

Многие функции не имеют эффектов, кроме возвращаемого значения, и их возвращаемое значение зависит только от параметров и / или глобальных переменных. Такая функция может быть подвержена общему исключению подвыражения и оптимизации цикла, как и в случае с арифметическим оператором. Эти функции должны быть объявлены с атрибутом pure. Например,

  int square (int) __attribute__ ((pure)); 

говорит, что гипотетический квадрат функции безопасен для вызова меньше раз, чем говорит программа. Некоторые из общих примеров чистых функций – strlen или memcmp. Интересными нечистыми функциями являются функции с бесконечными циклами или те, которые зависят от энергозависимой памяти или другого системного ресурса, которые могут меняться между двумя последовательными вызовами (например, feof в многоstreamовой среде).

Размышление о том, что представляет интересный вопрос для меня: если какой-то fragment кода мутирует нелокальную переменную и вызывает неинтроспективную функцию, предположим ли, что эта функция extern может зависеть от этой нелокальной переменной?

компиляционная единица A :

 int foo() { extern int x; return x; } 

компиляционная единица B :

 int x; int bar() { for (x=0; x<10; ++x) { std::cout << foo() << '\n'; } } 

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

3. Если мне нужно предотвратить компилятор от исключения кода

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

И если вы не можете судить, чем это не эквивалентно невозможности написания кода, который зависит от его (не) удаления?

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

Существует ли дерево, если его никто не может наблюдать? Et hop, мы находимся в квантовой механике.

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

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

Одно отличие состоит в том, что Java предназначен для работы только на одной платформе – JVM. Это значительно упрощает «строгость» в спецификации, так как есть только платформа для рассмотрения, и вы можете точно документировать, как она работает.

C ++ предназначен для работы на широком спектре платформ и делает это самостоятельно, без промежуточного уровня абстракции, но напрямую использует базовые аппаратные функции. Поэтому он решил разрешить функциональность, которая фактически существует на разных платформах. Например, результат некоторых операций сдвига, таких как int(1) << 33 может быть разным в разных системах, потому что это то, как работает оборудование.

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

Например, на мэйнфрейме IBM никто не ожидает, что с плавающей точкой будет совместимость с IEEE, потому что серия мэйнфреймов намного старше, чем стандарт IEEE. Тем не менее C ++ позволяет использовать базовое оборудование, а Java - нет. Является ли это преимуществом или дискомфортом для любого языка? Это зависит!


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

Если вы звоните в стандартную библиотеку, компилятор может точно знать , что делает вызов, как это описано в стандарте. Затем он имеет возможность действительно вызвать функцию, заменить ее каким-либо другим кодом или полностью пропустить ее, если она не действует. Например, std::strlen("Hello world!") Можно заменить на 12 . Некоторые компиляторы делают это, и вы не заметите.

  • Что означает {0} при инициализации объекта?
  • Рандомизировать список
  • Оператор копирования и оператор присваивания
  • 'cout' не называет тип
  • что такое «выравнивание стека»?
  • Зависит ли бит-сдвиг от конъюнктуры?
  • Является ли утечка памяти, если MemoryStream в .NET не закрыт?
  • Как передать параметры другому процессу в c #
  • Как вы получаете все свойства classа и его базовых classов (вверх по иерархии) с помощью Reflection? (С #)
  • Убедитесь, что приложение не работает в течение определенного периода времени и блокирует его
  • Параметрированный запрос для MySQL с C #
  • Interesting Posts

    Как использовать .htaccess в WAMP Server?

    Java String – Смотрите, содержит ли строка только числа, а не буквы

    Перехват ссылок из браузера, чтобы открыть приложение для Android

    selenium.common.exceptions.InvalidSelectorException с “span: contains (‘string’)”

    WPF C # Путь: как получить из строки с данными пути в геометрию в коде (не в XAML)

    Как сравнить две переменные объекта в языке выражений EL?

    OwnCloud MySQL таблица «oc_filecache» повреждена, могу ли я ее восстановить?

    Построение трехмерного участка поверхности с наложением карты контура, используя R

    Как я могу предоставить AntiForgeryToken при публикации данных JSON с использованием $ .ajax?

    Замена чисел в диапазоне с коэффициентом

    Запуск процесса мониторинга в системе

    Угловые параметры SIS

    Что означают следующие слова в C ++: нуль, инициализация по умолчанию и значение?

    функция члена шаблона classа шаблона, вызванная из функции шаблона

    Как проверить, есть ли дубликаты в плоском списке?

    Давайте будем гением компьютера.