Подтвердить что ты не робот

Оптимизатор С++ позволяет перемещать инструкции через вызов функции?

Примечание. Здесь нет многопоточности. Просто оптимизированный однопоточный код.

Функциональный вызов представляет точку последовательности. (Очевидно.)

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


Пояснительная предпосылка:

Теперь есть хорошая статья. класс сравнения для С++, где автор заявил:

Код, который мы не будем переустанавливать оптимизатором, всегда будет между этими вызовами start/end теперь(), поэтому мы можем гарантировать время будет действительным.

на который я спросил, как он может быть уверен, и nick ответил:

Вы можете проверить комментарий в этом ответе https://codereview.stackexchange.com/a/48884. Я цитирую: "Я бы внимательно следите за временем, которое не работает из-за оптимизация, которую разрешает компилятор. Я не уверен в требования к последовательности и понимание наблюдаемого поведения такой программы. При вызове функции компилятору не разрешается перемещать инструкции по точке вызова (они секвенированы до или после вызова).

Что мы делаем, это в основном абстрактное вызываемое (функция, лямбда, блок кода, окруженного лямбдой) и имеют вызов для подписки callable(factor) внутри структуры measure, которая действует как барьер (а не барьер в многопоточности, я полагаю, сообщение).

Я совершенно не уверен в этом, особенно цитата:

При вызове функции компилятору не разрешается перемещать инструкции по точке вызова (они секвенированы до или после вызова).

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

То есть, что касается языка/оптимизатора, эти два фрагмента абсолютно одинаковы:

void f() {
  // do stuff / Multiple statements
}

auto start = ...;
f();
auto stop = ...;

против.

auto start = ...;
  // do stuff / Multiple statements
auto stop = ...;
4b9b3361

Ответ 1

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

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

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

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

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

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

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

Ответ 2

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

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

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

Ответ 3

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

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

Чтобы проиллюстрировать, следующий код демонстрирует поведение undefined, поскольку он не учитывает порядок:

int a = 5;
int x = a++ + a;

Эта версия хорошо определена:

int a = 5;
a++;
int x = a + a;

Если нам гарантируется последовательность/порядок побочных эффектов и вычислений значений, то используется a x = a + a 6, а не 5. Поэтому компилятор не может переписать его:

int a = 5;
int x = a + a;
a++;

Однако, совершенно законно переписать его как:

int a = 5;
int x = (a+1) + (a+1);
a++;

Порядок выполнения между назначением x и назначением a не ограничен, потому что ни один из них не является volatile или atomic<T>, и они не являются внешними видимыми побочными эффектами.

Ответ 4

Стандарт оставляет окончательно свободную комнату для оптимизатора для последовательности операций по границе функции:

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

если существует правило as-if:

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

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

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

1.9/15 При вызове функции (независимо от того, является ли эта функция встроенной), вычисление каждого значения и побочный эффект, связанный с любым аргумент или с выражением postfix, обозначающим вызываемая функция, секвенирована до выполнения каждого выражения или выражение в теге вызываемой функции.

Таким образом, вы можете безопасно использовать выражение типа:

 my_timer_off(stop, f( my_timer_on(start) ) );  

Это "функциональное" письмо гарантирует, что:

  • my_timer_on() оценивается до выполнения любой инструкции f(),
  • f() вызывается до того, как выполняется тело my_timer_off()
  • обеспечивая тем самым таймер включения /f/timer -off (my_timer_xx принимает значение start/stop по значению).

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