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

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

#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;

что гарантирует различный результат чем первоначальный код (нул против большего чем нул сообщенное время). Видеть мой другой вопрос для детального примера. Это поведение соответствует стандарту C++?

7 ответов


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

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

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


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

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

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

кроме того-реорганизация действительно влияет на результат вычисления, но если вы посмотрите на нее с точки зрения компилятора - она живет в int main() мир и при выполнении измерений времени-он выглядывает, просит ядро дать ему текущее время и возвращается в основной мир, где фактическое время внешнего мира на самом деле не имеет значения. часы() сам не повлияет на программу и переменные, а поведение программы не повлияет на эту функцию 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 не используется впоследствии и не повлияет на саму программу.


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


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

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

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


по крайней мере, по моему чтению, нет, это не разрешено. Требование стандарта (§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();

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


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

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

или, может быть, время контролирует шлифовка зеркала телескопа "Хаббл". ЛОЛ

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

ИМО.


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

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

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

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

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