Я искал много вопросов, которые задавали связанную информацию, но ответы не совпали точно с тем, что я хотел получить для ответа. Я постараюсь объяснить проблему как можно лучше.
В основном при запуске кода в режиме выпуска компилятор, похоже, удаляет большую часть кода, который является избыточным или мертвым кодом. Так что в итоге ничего не проверяется. Некоторые исправления заключались в том, чтобы сделать код сохраненным для некоторой переменной, но затем компиляция просто удаляет цикл и сохраняет последнее приращение, которое кажется.
Теперь я хочу, чтобы были сделаны оптимизации, улучшающие используемый код, но я все еще хочу все, что изначально делалось, например. Если я заставлю его зацикливаться на коде 100 000 раз, я ожидаю, что он действительно выполнит код 100 000 раз. Я не уверен, как изменить компилятор в Visual Studio 2010, чтобы он выполнял минимальные оптимизации при компиляции в режиме выпуска. Я бы очень хотел точно рассчитать время, но я не уверен, как точно рассчитать время.
Сначала я подумал, что запуск в режиме отладки без отладки может решить проблему, и, похоже, это очень похоже на то, что результаты совпадают с результатами приложения Java, но при работе в режиме выпуска результаты оказываются безумно быстрыми, что меня смущает. Я не уверен, что C ++ намного лучше в оптимизации или большой объем кода был изменен.
Есть ли способ также разобрать код и посмотреть, во что компилируется код? Это было бы еще одним испытанием, которое я хотел бы увидеть, но я не знаю много об этом, и что-нибудь в правильном направлении будет с благодарностью. Хорошо, спасибо всем, кто может понять, что я прошу. Я буду рад ответить на любые вопросы, касающиеся недопонимания или неопределенности по рассматриваемому вопросу.
Поэтому, чтобы компилятор не оптимизировал весь ваш код, вам нужно убедиться, что вы «используете» результат того, что вы делаете в своем коде.
Другая хитрость заключается в том, чтобы поместить тестируемый код в отдельный файл, чтобы компилятор не мог встроить вашу «функцию вне файла» (если вы не включили «оптимизацию всей программы»).
Я часто использую указатели на функции — не так много, потому что это предотвращает оптимизацию [хотя это часто происходит], а потому, что это дает мне хорошую основу для проведения нескольких тестов с одной и той же базовой «мерой, сколько времени потребовалось и распечатка результатов», путем имея стол, выглядит примерно так:
typedef void (*funcptr)(void);
#define FUNC(f) { f, #f }
struct func_entry
{
funcptr func;
const char *name;
};
func_entry func_table[] =
{
FUNC(baseline),
FUNC(better1),
FUNC(worse1),
};
void do_benchmark()
{
for(int i = 0; i < sizeof(func_table)/sizeof(func_table[0]); i++)
{
timestamp t = now();
func_table[i].func();
t = now() - t;
printf("function %s took %8.5fs\n", func_table[i].name,
timestamp_to_seconds(t));
}
}
Очевидно, вам нужно заменить now()
с некоторой подходящей функцией извлечения времени, и timestamp
с соответствующим типом для этой функции, и timestamp_to_seconds
с чем-то, что работает …
Вам нужно не только использовать результат вызова, который вы синхронизируете в цикле, но вы также должны использовать результат из каждый итерация. Здесь вы пытаетесь убедиться, что используется результат каждого цикла, но не налагать слишком много накладных расходов на то, что вы пытаетесь протестировать.
Типичным подходом было бы накопление суммы всех вызовов метода для чего-то, что возвращает целое значение. Это можно распространить на методы, которые не возвращают int, вызывая какой-либо другой метод, который возвращает int. Например, если ваш метод создает std::string
с, звоните size()
на возвращенную строку, которая должна быть очень быстрой. В C ++ вы можете использовать адрес оператора &
как быстрый способ превратить почти все в целое число.
В некоторых случаях компилятор все еще может видеть ваши трюки и выводить фактический метод из цикла, который вырождается в добавление набора значений или даже одного большого умножения.
Вы можете избежать этого, перебирая какой-то ввод, сгенерированный во время выполнения — компилятор не сможет постоянно сворачивать ваш цикл. Использование указателей на функции также может работать, но добавляет дополнительные издержки, и некоторые компиляторы (и больше в будущем), вероятно, все еще могут их просматривать.
Все время, пока вы делаете это, вы должны спросить себя, что вы на самом деле измеряете. Очень маленькие методы, измеряемые в цикле, не обязательно дают хорошее представление о том, как они будут работать в реальной жизни, когда циклы более сложные. Это применимо к обеим сторонам спектра оптимизации — например, «подъём», которого вы пытаетесь избежать в своем бенчмарке, может на самом деле происходить в реальном коде, поэтому ваш бенчмарк слишком пессимистичен. И наоборот, такие вещи, как таблицы поиска 8K, которые всегда попадают в L1 в вашем тесте, могут повлечь за собой множество ошибок в реальном коде.
В итоге — микробенчмарки инструмент, который нужно использовать осторожно — вы можете определенно измерить что-то, как только вы поймете, как предотвратить оптимизацию, которую вы считаете нереалистичной на практике, — но вы всегда должны оценивать некоторые реальные случаи использования как проверку работоспособности (понимая, что огромные различия в микробенчмарках неизменно приводят к гораздо меньшим улучшениям в большой программе, где тестируется Метод — это меньшая часть времени выполнения для начала).
То, что я делаю, это есть benchmark()
функция, которая находится в DLL, и передает на нее указатель на функцию. Лучший способ помешать компилятору оптимизировать цикл тестирования — это сделать его невозможным, а размещение его в отдельной DLL абсолютно предотвращает это. Отдельная единица перевода больше не будет работать, LTCG становится обычным явлением.
Первое: немного быстрой настройки. Установите привязку потока к одному ядру и дайте ему высокий приоритет. Это предотвратит множество возможных изменений из-за переключения контекста или перебора кэша. И не забудьте использовать высокоточные таймеры, такие как QueryPerformanceCounter
для времени — вещи, которые не зависят от системного времени.
Затем вызовите указатель на функцию в цикле, пока не пройдет две секунды. Это согреет код / данные в кеше, даст вам приблизительное представление о его скорости и позволит автоматически получить разумное число циклов. Я выбираю счетчик циклов, который закончится через 1 секунду.
Далее, фактический бенчмаркинг: поддерживать счетчик, который увеличивается каждый раз, когда цикл не улучшает скорость предыдущего цикла. Когда счетчик достигнет некоторого установленного числа, остановитесь и предположите, что мы нашли лучшее время. Когда скорость улучшается, счетчик сбрасывается.
Обратите внимание, что это не скажет вам, что оптимизировать, как будет делать правильный профилировщик, но оно должно дать довольно точные результаты, если ваша единственная цель — сравнить один фрагмент кода с другим.
Это зависит от того, что ваш код пытается сделать и как вы ожидаете, что он будет себя вести.
Как правило, вы хотите работать в режиме выпуска и использовать профилировщик, чтобы увидеть, как ваш код работает в допустимом / тестовом случае. Даже тогда вам нужно знать какой профилировщик использовать и опять же, это зависит от того, какую проблему вы пытаетесь решить.
Самое главное, не ищите проблему, когда ее нет. Доверяйте своему компилятору. Попытка перехитрить ваш компилятор в эти дни — довольно бесполезное (и глупое) упражнение. Ваш профилировщик будет только выкладывать данные. Это зависит от вас, чтобы знать, что искать и интерпретировать это.
Предполагая, что вы используете Visual Studio (Visual C ++), установите для него профиль Debug, щелкните правой кнопкой мыши проект -> свойства и перейдите к C / C ++ -> Оптимизация. Убедитесь, что он отключен.
Затем вы можете просто использовать секундомер или программу Unix time
посчитать, как долго работает ваша программа.
Конечно, есть более сложные способы анализа производительности, такие как использование профилировщика.