бенчмаркинг, изменение порядка кода, volatile

Я решаю, что хочу протестировать конкретную функцию, поэтому наивно пишу такой код:

#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 не казалось правильным решением; не является ли изменчивым только для ввода-вывода с отображением памяти?

Тем не менее, я добавил volatile и обнаружил, что эталонный тест не только занимает значительно больше времени, но и крайне непостоянен от запуска к запуску. Без volatile (и если вам повезло убедиться, что код не был переупорядочен), эталонный тест постоянно занимал 600-700 мс. С volatile это часто занимало 1200 мс, а иногда и более 5000 мс. Листинги дизассемблирования для двух версий не показали практически никакой разницы, кроме разного выбора регистров. Это заставляет меня задаться вопросом, есть ли другой способ избежать переупорядочения кода, который не имеет таких подавляющих побочных эффектов.

Мой вопрос:

Каков наилучший способ предотвратить переупорядочивание кода в таком коде бенчмаркинга?

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

Обновление: похоже, ответ заключается в том, что мой коллега ошибся и такое изменение порядка не соответствует стандарту. Я проголосовал за всех, кто так сказал, и присуждаю награду Максиму.

Я видел один случай (на основе кода в этот вопрос ), где Visual Studio 2010 переупорядочил вызовы часов, как я проиллюстрировал (только в 64-разрядных сборках). Я пытаюсь сделать минимальный случай, чтобы проиллюстрировать это, чтобы я мог зарегистрировать ошибку в Microsoft Connect.

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


person Adrian McCarthy    schedule 23.02.2013    source источник
comment
@juanchopanza: Что, если компилятор знает, что SlowCalculation не имеет побочных эффектов?   -  person Oliver Charlesworth    schedule 23.02.2013
comment
@OliCharlesworth хорошая мысль. Я должен сделать некоторые мысли сейчас.   -  person juanchopanza    schedule 23.02.2013
comment
volatile просто означает, что доступ к памяти не может быть оптимизирован, и он не может быть переупорядочен в отношении других наблюдаемых побочных эффектов вашего кода (включая другие изменчивые доступы). Если SlowCalculation не имеет побочных эффектов, то я не уверен, что volatile сделает это безопаснее.   -  person Oliver Charlesworth    schedule 23.02.2013
comment
Вы должны иметь возможность посмотреть на сборку и увидеть, имеет ли значение volatile.   -  person Kerrek SB    schedule 23.02.2013
comment
volatile не имеет ничего общего с переупорядочением в ISO C++, но в MSVC все по-другому. В MSVC без специального флага это предотвращает изменение порядка.   -  person ixSci    schedule 23.02.2013
comment
Операции с памятью с volatile обрабатываются как операции ввода-вывода ЦП и никогда не исключаются, не переупорядочиваются и не предполагаются.   -  person Maxim Egorushkin    schedule 25.02.2013
comment
Хм, использовать настоящий профилировщик, если это возможно? :)   -  person Michael Dorgan    schedule 25.02.2013
comment
Есть ли какая-то причина не использовать здесь обычный asm volatile ("":::"memory");?   -  person jmetcalfe    schedule 25.02.2013
comment
Скорее всего, замедление с volatile связано с тем, что ЦП вынужден фактически читать память, когда что-то помечено как volatile, и то, что вы наблюдаете, не имеет ничего общего с переупорядочением.   -  person Jack Aidley    schedule 25.02.2013
comment
Я с @MichaelDorgan — почему бы не использовать настоящий профайлер?   -  person Nik Bougalis    schedule 25.02.2013
comment
компилятор, который переупорядочивает такой код, не работает   -  person BЈовић    schedule 25.02.2013
comment
@Kerrick SB: Как я уже говорил в вопросе, я сравнил разборку с volatile и без него. С тех пор я также пробовал 64-битную сборку, и в 64-битной компилятор фактически переупорядочивает второй вызов часов перед медленным вычислением. Несколько человек предположили, что это ошибка компилятора.   -  person Adrian McCarthy    schedule 26.02.2013
comment
@JackAidley: я ожидал бы, что он будет медленнее с volatile, но я не ожидал, что он будет настолько намного медленнее (8x), и я не ожидал, что он будет настолько непоследовательным, как есть.   -  person Adrian McCarthy    schedule 26.02.2013
comment
@AdrianMcCarthy: меня даже отдаленно не удивляют замедления такого масштаба. Вы заставляете его каждый раз эффективно промахиваться в кеше, поэтому процессор становится строго привязанным к скорости памяти.   -  person Jack Aidley    schedule 26.02.2013
comment
ответ также должен быть изменчивым.   -  person Öö Tiib    schedule 04.03.2013
comment
Связано: Избегайте оптимизации переменной с помощью встроенного asm для использования внутри тесных циклов.   -  person Peter Cordes    schedule 19.01.2019
comment
@JackAidley Вы вынуждаете его эффективно пропускать кэш Как вы это делаете?   -  person curiousguy    schedule 29.01.2019


Ответы (8)


Коллега указал, что я должен объявить переменные start и stop как volatile, чтобы избежать переупорядочения кода.

Извините, но ваш коллега не прав.

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

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

В вашем коде вызовы std::clock заканчиваются вызовом функции, определение которой недоступно.

Я не могу рекомендовать достаточно просмотра atomic Weapons: The C++ Memory Model and Modern Hardware, потому что в нем обсуждаются неверные представления о барьерах памяти (во время компиляции) и volatile среди многих других полезных вещей.

Тем не менее, я добавил volatile и обнаружил, что тест не только занимает значительно больше времени, но и крайне непостоянен от запуска к запуску. Без volatile (и если вам повезло убедиться, что код не был переупорядочен), эталонный тест постоянно занимал 600-700 мс. С volatile это часто занимало 1200 мс, а иногда и более 5000 мс.

Не уверен, что здесь виноват volatile.

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

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

person Maxim Egorushkin    schedule 25.02.2013
comment
Если вы правы, то мой компилятор (MSVC++ 2010 в 64-битном режиме) неисправен, потому что я обнаружил случай, когда он переупорядочил вызовы часов точно так, как я показал. Думаю, я зарегистрирую ошибку. Что касается непостоянства времени выполнения с volatile, я знаю о внешних факторах и свел их к минимуму. Странно то, что время очень последовательно несовместимо с volatile и постоянно согласовано без volatile, так что я не думаю, что это что-то настолько случайное, как запуск файлового индексатора. , Спасибо за ссылку на видео, оно уже было в моем списке просмотра. - person Adrian McCarthy; 26.02.2013
comment
Вы можете запустить свой код в Linux под Valgrind, чтобы увидеть построчное время выполнения и эффекты кеша. Однако у них должно быть что-то подобное для Windows. Тем не менее, я хотел бы увидеть код, в котором он переупорядочивает код так, как вы описываете. - person Maxim Egorushkin; 26.02.2013
comment
Он не переупорядочивает вызовы std::clock(), но может встраивать и перемещать вызовы SlowCalculation() куда угодно (и часто это делает). Почему еще люди используют барьеры? - person Öö Tiib; 04.03.2013
comment
Я читал это. Что там было читать? Когда у вас есть 3 записи в volatile переменные подряд, компилятор не может изменить их порядок. Даже если все 3 вычисления могут быть встроены. - person Öö Tiib; 04.03.2013
comment
Опасно предполагать, что компилятор не знает чего-то, что он на самом деле может знать. Например, std::clock — это функция, определенная в стандартной библиотеке, которую предоставляет компилятор. Пользователь не может определять что-либо в пространстве имен std, поэтому компилятор знает, что вы вызываете его версию std::clock, так что это не причина, по которой это не разрешено. Даже если SlowCalculation определено в какой-либо другой единице трансляции, это также не отключает оптимизацию, поскольку Visual Studio, clang и gcc поддерживают оптимизацию во время компоновки. - person David Stone; 08.04.2016
comment
@DavidStone Я не думаю, что компилятор что-либо знает о функциях, если только это не встроенная функция. - person Maxim Egorushkin; 08.04.2016
comment
@MaximEgorushkin: Ничто не мешает поставщику предоставить своему компилятору специальные знания о любой функции в пространстве имен std, если пользователь не может специализировать или перегрузить ее. - person David Stone; 09.04.2016
comment
Причина проблемная. std::clock не обязательно должна быть библиотечной функцией ввода-вывода, поэтому сама по себе не свободна от переупорядочения. Даже верно то, что ISO C++ предписывает std::clock реализовать как функцию (а не макрос), и ни одна разумная реализация не должна переупорядочивать ее вызовы, не гарантируется, что она будет вести себя так, как вы сказали, с любыми соответствующими реализациями. И рассматривать встраивание как барьер времени компиляции в целом неправильно. Реализации могут устранить последующие вызовы полностью, если они могут доказать, что функция не имеет побочных эффектов, например. при объявлении с __attribute__((__const__)) в G++. - person FrankHB; 16.06.2016
comment
@FrankHB Правильно, с точки зрения стандарта любая функция, не отмеченная __attribute__((__const__)), является функцией ввода-вывода. - person Maxim Egorushkin; 16.06.2016
comment
Это взгляд на реализацию, которая является консервативной, но практичной стратегией. Однако я не считаю, что стандарт требует, чтобы это была одна из функций ввода-вывода, вызов которой будет иметь побочные эффекты, которые являются изменениями в состоянии среды выполнения. И по крайней мере на данный момент в стандарте нет ничего эквивалентного __attribute__((__const__)) и т.д. - person FrankHB; 17.06.2016
comment
@FrankHB Можно было бы придумать другую реализацию всей стандартной библиотеки или только std::clock, где std::clock выполняет ввод-вывод, и это все равно будет соответствовать стандарту. - person Maxim Egorushkin; 17.06.2016
comment
Это статус-кво. Однако, если я хочу, чтобы мой код соответствовал стандарту (то есть не полагался на какие-либо конкретные детали реализации), я должен приложить усилия, чтобы самостоятельно гарантировать, что компилятор никогда не изменит порядок код. Между тем (возможно) единственный выбор состоит в том, чтобы кодировать очень осторожно: добавить volatile к как переменной-счетчику, так и к промежуточному результату, чтобы сохранить код, который нужно измерить. Возможно, так думал коллега оп, и, к сожалению, в чем-то это было правильно. - person FrankHB; 18.06.2016
comment
gcc и другие могут/будут переупорядочивать вызовы функций часов (не относительно друг друга, а относительно тестируемого кода) таким образом, что сделает результаты тестов недействительными. volatile до сих пор является единственным способом предотвратить это. - person old_timer; 19.01.2019
comment
@old_timer Покажите пример. - person Maxim Egorushkin; 19.01.2019
comment
@MaximEgorushkin Компилятор может что-то знать о написанной пользователем функции, когда программист использует (специфичные для компилятора) атрибуты: Например, вы можете использовать атрибуты, чтобы указать, что функция никогда не возвращает (noreturn), возвращает значение, зависящее только от значения его аргументов (const) или имеет аргументы в стиле printf (format).6.32 Объявление атрибутов функций - person curiousguy; 29.01.2019

Обычный способ предотвратить переупорядочение - это барьер компиляции, т.е. asm volatile ("":::"memory"); (с gcc). Это ассемблерная инструкция, которая ничего не делает, но мы сообщаем компилятору, что она будет засорять память, поэтому не разрешается переупорядочивать код в ней. Стоимость этого - это только фактическая стоимость удаления повторного заказа, что, очевидно, не относится к изменению уровня оптимизации и т. Д., Как предлагается в другом месте.

Я считаю, что _ReadWriteBarrier эквивалентен материалам Microsoft.

Однако, согласно ответу Максима Егорушкина, изменение порядка вряд ли будет причиной ваших проблем.

person jmetcalfe    schedule 25.02.2013
comment
это затрет память Какую именно память? Вы имеете в виду внешне доступные объекты? - person curiousguy; 29.01.2019

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

Я нигде не мог найти это, поэтому добавил свой собственный ответ через 11 лет после того, как вопрос был задан;).

Использование volatile для переменных - это не то, что вам нужно для этого. Это заставит компилятор загружать и сохранять эту переменную из ОЗУ и в ОЗУ каждый раз (при условии, что есть побочный эффект того, что необходимо сохранить: иначе - хорошо для регистров ввода-вывода). Когда вы проводите бенчмаркинг, вас не интересует измерение того, сколько времени требуется, чтобы получить что-то из памяти или записать это туда. Часто вы просто хотите, чтобы ваша переменная находилась в регистрах процессора.

volatile можно использовать, если вы назначаете ему один раз вне цикла, который не оптимизируется (например, суммирование массива), в качестве альтернативы печати результата. (Как и долговременная функция в вопросе). Но не внутри крошечной петли; это представит инструкции сохранения/перезагрузки и задержку переадресации хранилища.


Я думаю, что ЕДИНСТВЕННЫЙ способ заставить ваш компилятор не оптимизировать код теста — это использовать asm. Это позволяет вам обмануть компилятор, заставив его думать, что он ничего не знает о содержимом или использовании ваших переменных, поэтому он должен делать все каждый раз, так часто, как ваш цикл просит его об этом.

Например, если бы я хотел сравнить m & -m, где m — это некоторое uint64_t, я мог бы попробовать:

uint64_t const m = 0x0000080e70100000UL;
for (int i = 0; i < loopsize; ++i)
{
  uint64_t result = m & -m;
}

Компилятор, очевидно, сказал бы: я даже не буду вычислять это; так как вы не используете результат. Ака, на самом деле это было бы так:

for (int i = 0; i < loopsize; ++i)
{
}

Тогда вы можете попробовать:

uint64_t const m = 0x0000080e70100000UL;
static uint64_t volatile result;
for (int i = 0; i < loopsize; ++i)
{
  result = m & -m;
}

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

uint64_t const m = 0x0000080e70100000UL;
uint64_t tmp = m & -m;
static uint64_t volatile result;
for (int i = 0; i < loopsize; ++i)
{
  result = tmp;
}

Тратить много времени на запись в адрес памяти result loopsize раз, как вы и просили.

Наконец, вы также можете сделать m volatile, но в ассемблере результат будет выглядеть так:

507b:   ba e8 03 00 00          mov    $0x3e8,%edx
  # top of loop
5080:   48 8b 05 89 ef 20 00    mov    0x20ef89(%rip),%rax        # 214010 <m_test>
5087:   48 8b 0d 82 ef 20 00    mov    0x20ef82(%rip),%rcx        # 214010 <m_test>
508e:   48 f7 d8                neg    %rax
5091:   48 21 c8                and    %rcx,%rax
5094:   48 89 44 24 28          mov    %rax,0x28(%rsp)
5099:   83 ea 01                sub    $0x1,%edx
509c:   75 e2                   jne    5080 <main+0x120>

Дважды чтение из памяти и один раз запись в нее, помимо запрошенного расчета с регистрами.

Поэтому правильный способ сделать это:

for (int i = 0; i < loopsize; ++i)
{
  uint64_t result = m & -m;
  asm volatile ("" : "+r" (m) : "r" (result));
}

результаты которого в коде сборки (из gcc8.2 в обозревателе компиляторов Godbolt):

 # gcc8.2 -O3 -fverbose-asm
    movabsq $8858102661120, %rax      #, m
    movl    $1000, %ecx     #, ivtmp_9     # induction variable tmp_9
.L2:
    mov     %rax, %rdx      # m, tmp91
    neg     %rdx            # tmp91
    and     %rax, %rdx      # m, result
       # asm statement here,  m=%rax   result=%rdx
    subl    $1, %ecx        #, ivtmp_9
    jne     .L2
    ret     

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

Хитрость здесь в том, что используя asm volatile1 и сообщая компилятору

  1. Входной операнд "r": он использует значение result в качестве входных данных, поэтому компилятор должен материализовать его в регистре.
  2. "+r" операнд ввода/вывода: m остается в том же регистре, но (потенциально) изменяется.
  3. volatile: имеет какой-то таинственный побочный эффект и/или не является чистой функцией входных данных; компилятор должен выполнить его столько же раз, сколько и исходный код. Это заставляет компилятор оставить ваш тестовый фрагмент в покое и внутри цикла. См. раздел руководства gcc по расширенному Asm#Volatile.

сноска 1: здесь требуется volatile, иначе компилятор превратит это в пустой цикл. Энергонезависимый asm (с любыми выходными операндами) считается чистой функцией своих входов, которую можно оптимизировать, если результат не используется. Или CSEd для запуска только один раз, если используется несколько раз с одним и тем же вводом.


Все, что ниже, не мое, и я не обязательно с этим согласен. --Карло Вуд

Если бы вы использовали asm volatile ("" : "=r" (m) : "r" (result)); (с выходом "=r" только для записи), компилятор мог бы выбрать один и тот же регистр для m и result, создав циклическую цепочку зависимостей, которая проверяет задержку, а не пропускную способность расчет.

Из этого вы получите этот asm:

5077:   ba e8 03 00 00          mov    $0x3e8,%edx
507c:   0f 1f 40 00             nopl   0x0(%rax)    # alignment padding
  # top of loop
5080:   48 89 e8                mov    %rbp,%rax    # copy m
5083:   48 f7 d8                neg    %rax         # -m
5086:   48 21 c5                and    %rax,%rbp    # m &= -m   instead of using the tmp as the destination.
5089:   83 ea 01                sub    $0x1,%edx
508c:   75 f2                   jne    5080 <main+0x120>

Это будет выполняться с частотой 1 итерация за 2 или 3 цикла (в зависимости от того, есть ли у вашего ЦП устранение перемещений или нет). Версия без циклической зависимости может выполняться с частотой 1 итерация за такт на Haswell и более поздних версиях, а также на Ryzen. Эти процессоры имеют пропускную способность ALU для выполнения не менее 4 операций за такт.

Этот ассемблер соответствует C++, который выглядит так:

for (int i = 0; i < loopsize; ++i)
{
  m = m & -m;
}

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

Вы можете захотеть провести микротестирование задержки, чтобы было легче определить преимущества компиляции с -mbmi или -march=haswell, чтобы позволить компилятору использовать blsi %rax, %rax и вычислить m &= -m; в одной инструкции. Но легче отслеживать, что вы делаете, если исходный код C++ имеет ту же зависимость, что и asm, вместо того, чтобы обманывать компилятор, вводя новую зависимость.

person Carlo Wood    schedule 17.01.2019
comment
ОП говорит о назначении конечного результата всего медленного вычисления volatile int answer, а не об использовании volatile внутри горячего цикла. Вы правы в том, что никогда не должны этого делать, потому что это приводит к задержке при переадресации в хранилище. Но назначение конечного результата volatile, например, его печать или возврат из main, является хорошим способом использования результата, чтобы компилятор не оптимизировал весь цикл суммирования-массива или что-то в этом роде. - person Peter Cordes; 18.01.2019
comment
В вашем случае вы можете просто скрыть постоянное значение времени компиляции m от компиляции вне цикла вместо использования asm(), чтобы заставить компилятор материализовать каждый шаг result именно так, как вы его написали. (т. е. вы преодолели возможность оптимизации всего цикла до popcnt, если вы выполняли result += m & -m.) Повторение крошечного выражения в цикле, который компилируется в пару инструкций, имеет ограниченную ценность. Вы измеряете только пропускную способность, а не задержку, и у вас нет возможности оптимизировать окружающий код. - person Peter Cordes; 18.01.2019
comment
И самое главное, ваш оператор asm сообщает компилятору неверную вещь: "=r" говорит ему, что m является выходом только для записи. Используйте "+r" (m) для операнда ввода/вывода чтения-записи. Вам повезло, что компилятор выбрал тот же выходной регистр, в котором уже был m, так что результирующий ассемблер все еще имел смысл. Но при любом раскручивании его может и не быть. - person Peter Cordes; 18.01.2019
comment
Я не могу следовать вашему аргументу об использовании +r, все, что я хочу, это чтобы компилятор думал, что переменная C++ m может иметь другое значение, поэтому он будет пересчитывать каждую итерацию цикла. Я согласен с тем, что теоретически он может использовать другой регистр для «нового» m, но это работает только с развертыванием цикла. Когда нет развертывания цикла, компилятор все равно вынужден использовать тот же регистр (или он очень плохо справился с оптимизацией, потому что впоследствии ему придется переместить этот регистр в регистр, используемый для m в начале цикла) . - person Carlo Wood; 18.01.2019
comment
Что касается вашего второго комментария, скрытие значения m не работает; когда m не изменяется (и это не так), и я не имитирую его изменение с помощью asm(), тогда вычисление 'm & -m' будет перемещено за пределы цикла, в то время как это именно тот фрагмент кода, который я хочу протестировать. - person Carlo Wood; 18.01.2019
comment
Ваше первое замечание совершенно верно :/. Я разместил свой ответ с неправильным вопросом. Я боролся с тем, как помешать компилятору перемещать тестируемый код за пределы цикла (без дополнительных накладных расходов). Я много гуглил и не мог найти ответ; как только я это понял, я выбрал этот ТАК вопрос, основываясь на заголовке, когда искал мою тему в Google - и я все еще думаю, что его, вероятно, найдут люди, у которых такая же проблема, как у меня, - но фактический вопрос отличается :(. Возможно Я должен был сначала создать свой собственный вопрос, а затем ответить на него. - person Carlo Wood; 18.01.2019
comment
Использование неправильного ограничения и его работа всегда являются плохим планом и плохим примером для будущих читателей SO. Это может сломаться из-за более сложного цикла (больше окружающего кода), а также из-за разворачивания цикла (что clang делает по умолчанию, в отличие от gcc). В любом случае возможная опасность заключается в создании ложной зависимости, например, при выборе того же регистра, что и вход "r"(result), или в неточном отражении дополнительных mov затрат, необходимых для вычисления чего-либо из m без уничтожения исходного значения. - person Peter Cordes; 19.01.2019
comment
Внутри крошечного цикла вы в основном увидите накладные расходы цикла и внешние эффекты, такие как Снижается ли производительность при выполнении циклов, количество операций которых не является кратно ширине процессора?, например одна дополнительная инструкция может снизить производительность вдвое на Sandybridge/IvyBridge, если она занимает цикл с 4 до 5 мкп. Таким образом, этот тест крошечного цикла дает вам очень узкое и искаженное представление о стоимости выражения C как части более крупного блока кода. например на Haswell вы не можете обнаружить ускорение от BMI1 blsr %rax, %rbp - person Peter Cordes; 19.01.2019
comment
О, подождите, на самом деле вы можете, ваш цикл здесь уничтожает исходный m из-за вашего фиктивного ограничения, поэтому ваш цикл asm ограничен задержкой mov+neg+and (2 или 3 цикла), а не пропускная способность (1 итерация за цикл на Haswell, если бы не эта цепочка зависимостей, переносимая циклом). Именно потому, что вы использовали ограничение, которое солгало компилятору о том, что вы хотели, или, другими словами, написали ассемблерный оператор, который имел выходную зависимость. (Конечно, оставление регистра без изменений создает зависимость от старого значения, чего gcc не ожидал). - person Peter Cordes; 19.01.2019
comment
Я исправил ваш ответ, чтобы объяснить зависимость, которую gcc вводит для "=r"(m), и правильно показать asm. Я также добавил заголовок, чтобы объяснить, чем этот ответ отличается от заданного вопроса. Вероятно, было бы лучше, если бы были отдельные вопросы и ответы, но избегайте оптимизации переменной с помощью встроенного asm уже существует, и, вероятно, есть еще один вопрос, еще более близкий к тому, что ты пытаешься сделать. - person Peter Cordes; 19.01.2019
comment
@PeterCordes Что ж, спасибо за тяжелую работу. Но вы либо не в ладах, либо ошибаетесь - так или иначе, у меня нет времени разбираться дальше, поэтому я просто добавил оговорку, что та часть, которую вы добавили, не моя, и оставил все для остального ( без войн правок). - person Carlo Wood; 21.01.2019
comment
Это нормально, похоже, это хороший способ отделить мое, возможно, слишком большое редактирование: P. Если у вас есть процессор Haswell / Broadwell / Skylake или Ryzen, вы сможете оценить разницу, если у вас будет время. Моя версия должна выполняться с частотой 1 итерация за 1 такт, с узким местом по пропускной способности (как я думаю, вы пытались это сделать), в то время как ваша версия должна работать с 1 итерацией за 2 или 3 такта, с задержкой m &= -m;, если только вы не компилируете с -mbmi или -march=haswell . Или просто посмотрите на ассемблер от фактического написания m &= -m; и обратите внимание, что это то же самое, что и ваша версия. - person Peter Cordes; 21.01.2019
comment
загружать и сохранять эти переменные из ОЗУ и в ОЗУ Вы имеете в виду из адресуемой памяти, доступной из текущего процессорного блока, а не из физической ОЗУ, верно? - person curiousguy; 29.01.2019

Вы можете создать два файла C: SlowCalculation, скомпилированный с g++ -O3 (высокий уровень оптимизации), и тестовый, скомпилированный с g++ -O1 (более низкий уровень, все еще оптимизированный - этого может быть достаточно для этой части бенчмаркинга).

Согласно справочной странице, изменение порядка кода происходит на уровнях оптимизации -O2 и -O3.

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

Предполагая, что вы используете g++, но в другом компиляторе должно быть что-то эквивалентное.

person Breaking not so bad    schedule 23.02.2013
comment
Это интересная идея. Скорее всего, это предотвратит встраивание SlowCalculation непосредственно в тест, и это значительно уменьшит вероятность изменения порядка кода. Но я не уверен, что это надежно. - person Adrian McCarthy; 24.02.2013
comment
Поскольку оптимизация происходит во время компиляции, а не компоновки (1) существует такая вещь, как глобальная оптимизация (2) если невозможна поздняя оптимизация, поскольку компоновка выполняется на чистом исполняемом коде без семантической информации, или сделано слишком поздно, чтобы оптимизировать что-либо (связывание во время выполнения), вопрос (1) является спорным. Но тогда ваше предположение о том, что изменение порядка может произойти на каком-то уровне оптимизации в отдельно скомпилированном тестовом коде: код эталонного теста, который вызывает отдельно скомпилированный код, не может ничего предполагать об этом коде, поэтому он не может переупорядочивать вызовы к нему. - person curiousguy; 29.01.2019

Правильный способ сделать это в C++ — использовать класс, например. что-то типа

class Timer
{
    std::clock_t startTime;
    std::clock_t* targetTime;

public:
    Timer(std::clock_t* target) : targetTime(target) { startTime = std::clock(); }
    ~Timer() { *target = std::clock() - startTime; }
};

и используйте его так:

std::clock_t slowTime;
{
    Timer timer(&slowTime);
    int answer = SlowCalculation(42);
}

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

person Jack Aidley    schedule 25.02.2013

Volatile гарантирует одно и только одно: чтение из volatile переменной будет считываться из памяти каждый раз — компилятор не будет предполагать, что значение может быть кэшировано в регистре. Точно так же записи будут записываться в память. Компилятор не будет хранить его в регистре «некоторое время, прежде чем записать его в память».

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

_ReadWriteBarrier() - полное ограждение

_ReadBarrier() - двустороннее ограждение для грузов

_WriteBarrier() - двухсторонний забор для магазинов

ICC включает полное ограждение __memory_barrier().

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

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

Предложит прочитать http://preshing.com/20120625/memory-ordering-at-compile-time, чтобы увидеть потенциальные проблемы, с которыми мы можем столкнуться при переупорядочивании компилятора и т. д.

person Saqlain    schedule 26.02.2013
comment
volatile также гарантирует, что значение записывается так, как ABI определяет представление значения этого объекта; и что любое допустимое представление значения ABI может быть прочитано обратно, и что компилятор ничего не предполагает относительно значения, полученного из такого чтения, даже если непосредственно перед записью было чтение или запись. - person curiousguy; 29.01.2019

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

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

Другая возможность состоит в том, чтобы сделать функцию SlowCalculation(42); функцией extern (определить эту функцию в отдельном файле .c/.cpp и связать файл с вашей основной программой) и объявить start и stop глобальными переменными. Я не знаю, какие оптимизации предлагает оптимизатор времени компоновки/межпроцедурный вашего компилятора.

Кроме того, если вы компилируете в O1 или O0, компилятор, скорее всего, не будет переупорядочивать инструкции. Ваш вопрос несколько связан с (временными барьерами компиляции - переупорядочением кода компилятора - gcc и pthreads)

person A. K.    schedule 28.02.2013

Переупорядочивание, описанное вашим коллегой, просто ломает 1.9/13

Последовательность перед представляет собой асимметричное, транзитивное, попарное отношение между оценками, выполняемыми одним потоком (1.10), что приводит к частичному порядку среди этих оценок. При любых двух оценках A и B, если A упорядочивается до B, то выполнение A должно предшествовать выполнению B. Если A не упорядочивается до B, а B не упорядочивается до A, то A и B не упорядочиваются. [Примечание: выполнение непоследовательных оценок может перекрываться. —конец примечания] Оценки A и B расположены в неопределенной последовательности, когда либо A упорядочивается до B, либо B упорядочивается до A, но не указано, что именно. [Примечание: неопределенная последовательность оценок не может перекрываться, но любая из них может выполняться первой. -конец примечания]

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

person ixSci    schedule 25.02.2013
comment
Более того, любая программа на C++ гарантированно будет последовательно непротиворечивой, если нет гонок данных. . Гонка данных — это когда несколько потоков обращаются к одному и тому же объекту, и по крайней мере один поток является записывающим. - person Maxim Egorushkin; 25.02.2013
comment
Этот ответ занял второе место в награде. - person Adrian McCarthy; 02.03.2013
comment
Я должен был отметить, что этот ответ неверен. Здесь действует одно из правил так называемой абстрактной машинной семантики, которое можно обойти при фактической реализации из-за правило "как если бы". Однако volatile является одним из исключений. - person FrankHB; 18.06.2016
comment
Ваше утверждение, что вы не должны думать о переупорядочении, пока вы не используете потоки, неверно. В однопоточных программах переупорядочивание по-прежнему может иметь большое значение, и его можно не ожидать. - person FrankHB; 18.06.2016
comment
@FrankHB, поскольку у вас гарантированно будет последовательное поведение (как есть или как если бы - не имеет значения), вам не нужно об этом заботиться. - person ixSci; 18.06.2016
comment
В идеале ваша претензия должна иметь место. Однако вопрос раскрывает темную сторону стандарта С++: на самом деле не гарантируется, что он будет работать как ваше воображение. Это может быть дефект. Подробнее см. здесь. обсуждение. - person FrankHB; 19.06.2016
comment
@MaximEgorushkin любая программа на C++ гарантированно будет последовательно согласованной, если нет гонок данных 1) нет, и 2) здесь это не имеет значения Гонка данных — это когда есть более одного потока, обращающегося к одному и тому же объекту, и по крайней мере один поток является писателем 3) Это не определение гонки данных и 4) гонка данных вызывает UB, поэтому 5) вы в основном говорите, что все программы которые имеют семантику, которая каким-либо образом ограничена, имеют исполнения SC, что неверно - person curiousguy; 29.01.2019