Я решаю, что хочу сравнить определенную функцию, поэтому я наивно пишу код следующим образом:
#include <ctime>
#include <iostream>
int SlowCalculation(int input) { ... }
int main() {
std::cout << "Benchmark running..." << std::endl;
std::clock_t start = std::clock();
int answer = SlowCalculation(42);
std::clock_t stop = std::clock();
double delta = (stop - start) * 1.0 / CLOCKS_PER_SEC;
std::cout << "Benchmark took " << delta << " seconds, and the answer was "
<< answer << '.' << std::endl;
return 0;
}
Коллега отметил, что я должен объявить переменные start
и stop
как volatile
, чтобы избежать переупорядочения кода. Он предположил, что оптимизатор может, например, эффективно изменить порядок кода следующим образом:
std::clock_t start = std::clock();
std::clock_t stop = std::clock();
int answer = SlowCalculation(42);
Сначала я скептически относился к такому крайнему переупорядочению, но после некоторых исследований и экспериментов я узнал, что это так.
Но волатильность не чувствовала правильного решения; не является волатильным действительно только для ввода/вывода с отображением памяти?
Тем не менее, я добавил volatile
и обнаружил, что контрольный тест не только значительно увеличился, но и был безвозвратно несовместим с запуском для запуска. Без волатильности (и повезло, чтобы код не был переупорядочен), эталон последовательно составлял 600-700 мс. При изменчивости часто принималось 1200 мс, а иногда и более 5000 мс. Списки разборки для двух версий практически не отличались от других регистров. Это заставляет меня задаться вопросом, есть ли другой способ избежать переупорядочения кода, у которого нет таких подавляющих побочных эффектов.
Мой вопрос:
Каков наилучший способ предотвратить переупорядочение кода при использовании эталонного кода?
Мой вопрос похож на этот (который касался использования volatile, чтобы избежать элиции, а не переупорядочения), этот (который не ответил, как предотвратить переупорядочение), и этот (который обсуждал, была ли проблема переупорядочением кода или устранением кода). Хотя все три находятся на этой точной теме, никто на самом деле не отвечает на мой вопрос.
Обновить. Ответ заключается в том, что мой коллега ошибся и что такое переупорядочение не соответствует стандарту. Я поддержал всех, кто это сказал, и награждаю награду Максима.
Я видел один случай (на основе кода в этом вопросе), где Visual Studio 2010 переупорядочивает тактовые вызовы, как я иллюстрировал (только в 64-битных сборках). Я пытаюсь сделать минимальный случай, чтобы проиллюстрировать это, чтобы я мог сообщить об ошибке в Microsoft Connect.
Для тех, кто сказал, что volatile должен быть намного медленнее, потому что он заставляет читать и записывать в память, это не совсем согласуется с испускаемым кодом. В моем ответе на этот вопрос, я показываю разборку кода с изменением и без него. Внутри цикла все хранится в регистрах. Единственным существенным отличием является выбор регистров. Я не очень хорошо разбираюсь в сборке x86, чтобы узнать, почему производительность энергонезависимой версии постоянно быстро, а волатильная версия несовместима (а иногда и резко) медленнее.