Является ли законным для оптимизатора С++ изменять порядок вызовов на clock()?

Язык программирования С++ 4-е издание, стр. 225: Компилятор может изменить порядок кода для повышения производительности, если результат идентичен это простой порядок исполнения. Некоторые компиляторы, например. Visual С++ в режиме деблокирования изменит этот код:

#include <time.h>
...
auto t0 = clock();
auto r  = veryLongComputation();
auto t1 = clock();

std::cout << r << "  time: " << t1-t0 << endl;

в эту форму:

auto t0 = clock();
auto t1 = clock();
auto r  = veryLongComputation();

std::cout << r << "  time: " << t1-t0 << endl;

который гарантирует отличный результат, чем исходный код (ноль и большее, чем нулевое время). Подробнее см. мой другой вопрос. Является ли это поведение совместимым со стандартом С++?

Ответ 1

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

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

Помните, что этот ответ зависит от стандарта С++, не налагающего ограничений на компиляторы.

Ответ 2

Ну, есть что-то, называемое Subclause 5.1.2.3 of the C Standard [ISO/IEC 9899:2011], которое гласит:

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

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

Кроме того, реорганизация действительно влияет на результат вычисления, но если вы посмотрите на нее с точки зрения компилятора - она ​​живет в мире int main(), а при выполнении измерений времени - она ​​заглядывает, просит ядро ​​дать это текущее время, и возвращается в основной мир, где фактическое время внешнего мира не имеет большого значения. Сам clock() не будет влиять на программу, а переменные и поведение программы не повлияют на функцию clock().

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

Это, однако, не меняет того факта, что описанное поведение очень неприятно.

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

Рассмотрим следующий код, взятый из этого сайта:

void GetData(char *MFAddr) {
    char pwd[64];
    if (GetPasswordFromUser(pwd, sizeof(pwd))) {
        if (ConnectToMainframe(MFAddr, pwd)) {
              // Interaction with mainframe
        }
    }
    memset(pwd, 0, sizeof(pwd));
}

При компиляции обычно все в порядке, но если применяются оптимизации, вызов memset будет оптимизирован, что может привести к серьезному недостатку безопасности. Почему он оптимизирован? Это очень просто; компилятор снова думает в своем мире main() и считает, что memset является мертвым хранилищем, поскольку переменная pwd не используется впоследствии и не повлияет на саму программу.

Ответ 3

Да, это законно - , если компилятор может видеть весь код, который встречается между вызовами clock().

Ответ 4

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

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

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

Ответ 5

По крайней мере, по моему чтению, нет, это не разрешено. Требование от стандарта (§1.9/14):

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

Степень, в которой компилятор может свободно изменять порядок, определяется правилом "как есть" (§1.9/1):

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

Это оставляет вопрос о том, является ли поведение, о котором идет речь (вывод, написанный cout), является официально наблюдаемым поведением. Короткий ответ: да, это (§1.9/8):

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

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

Если, однако, вы хотели предпринять дополнительные шаги для обеспечения правильного поведения, вы могли бы воспользоваться еще одним положением (также §1.9/8):

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

Чтобы воспользоваться этим, вы немного измените свой код, чтобы стать чем-то вроде:

auto volatile t0 = clock();
auto volatile r  = veryLongComputation();
auto volatile t1 = clock();

Теперь, вместо того, чтобы основывать вывод на трех отдельных разделах стандарта и все еще иметь только довольно определенный ответ, мы можем посмотреть только одно предложение и иметь абсолютно определенный ответ - с этим кодом, re -ограничение использования clock против, длинные вычисления явно запрещены.

Ответ 6

Предположим, что последовательность находится в цикле, а veryLongComput() случайным образом генерирует исключение. Затем сколько будет вычисляться t0s и t1s? Предварительно ли вычисляет случайные переменные и переупорядочивает на основе предварительного расчета - иногда переупорядочивание, а иногда и не?

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

Или, может быть, время контролирует измельчение зеркала телескопа Хаббла. LOL

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

ММО.

Ответ 7

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

Оба t0 и t1 используются при выводе t1-t0. Поэтому выражения инициализатора для t0 и t1 должны выполняться, и это должно соответствовать всем стандартным правилам. Результат функции используется, поэтому невозможно оптимизировать вызов функции, хотя он напрямую не зависит от t1 или наоборот, поэтому можно было бы наивно полагать, что это законно перемещать его, почему нет. Может быть, после инициализации t1, которая не зависит от вычисления?
Косвенно, однако, результат t1 , конечно, зависит от побочных эффектов от veryLongComputation() (в частности, время вычисления, если ничего больше), что является одной из причин, по которым существует такая вещь, как "точка последовательности".

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

Составители иногда имеют "особые знания" о библиотечных функциях, например, некоторые функции не возвращаются или могут вернуться дважды (подумайте exit или setjmp).
Однако, поскольку каждая непустая нетривиальная функция (и veryLongComputation довольно нетривиальна из ее имени) будет потреблять время, компилятор, обладающий "специальным знанием" об остаточной непрозрачной библиотечной функции clock, фактически имел бы чтобы быть явно запрещенным от переупорядочения вызовов вокруг этого, зная, что это не только может, но и повлияет на результаты.

Теперь интересный вопрос: почему компилятор вообще это делает? Я могу представить себе две возможности. Возможно, ваш код запускает эвристику "похож на эталон", и компилятор пытается обмануть, кто знает. Это будет не первый раз (подумайте SPEC2000/179.art, или SunSpider для двух исторических примеров). Другая возможность заключалась бы в том, что где-то внутри veryLongComputation() вы непреднамеренно вызываете поведение undefined. В этом случае поведение компилятора будет даже законным.