Как правильно протестировать [шаблонированную] программу на C++

StackOverflow https://stackoverflow.com/questions/435627

  •  22-07-2019
  •  | 
  •  

Вопрос

<фон>

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

</фон>

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

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

Очевидно, мне нужно включить оптимизацию, потому что без этого g++ (и, возможно, и другие компиляторы) будут сохранять некоторые ненужные операции в объектном коде.Мне также нужно активно использовать новую функцию в тесте, потому что разница в 1e-3 секунды может иметь значение между хорошим и плохим дизайном (в реальной программе эта функция будет вызываться миллион раз).

Проблема в том, что g++ иногда «слишком умен» при оптимизации и может удалить целый цикл, если учесть, что результат вычисления никогда не используется.Я уже видел это однажды, глядя на выходной ассемблерный код.

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

Итак, как я могу сделать правильный эталон небольшой функции, извлеченной из библиотеки?Связанный вопрос:это правильный подход к такому виду in vitro тесты на небольшом модуле или мне нужен весь контекст?

Спасибо за советы!


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

Я думаю, что попробую все это.Большое спасибо за все ваши ответы!

Это было полезно?

Решение

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

template<typename T> void sink(T const& t) {
   volatile T sinkhole = t;
}

Никаких накладных расходов на iostream, просто копия, которая должна оставаться в сгенерированном коде.Теперь, если вы собираете результаты множества операций, лучше не отбрасывать их один за другим.Эти копии все равно могут добавить некоторые накладные расходы.Вместо этого каким-то образом соберите все результаты в одном энергонезависимом объекте (поэтому нужны все отдельные результаты), а затем назначьте этот объект результата изменчивому объекту.Например.если все ваши отдельные операции создают строки, вы можете принудительно вычислить, сложив все значения символов вместе по модулю 1<<32.Это практически не добавляет накладных расходов;строки, скорее всего, будут в кеше.Результат сложения впоследствии будет присвоен летучему значению, поэтому каждый символ в каждой строке фактически должен быть рассчитан, никаких сокращений не допускается.

Другие советы

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

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

Компиляторам разрешено исключать только те ветки кода, которые не могут произойти.Пока он не может исключить необходимость выполнения ветки, он не устранит ее.Пока где-то существует некоторая зависимость данных, код будет там и будет выполняться.Компиляторы не слишком умны в оценке того, какие аспекты программы не будут запущены, и не пытаются это сделать, потому что это проблема NP, и ее трудно вычислить.У них есть несколько простых проверок, таких как if (0), но это все.

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

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

Чтобы ответить на ваш вопрос об тестировании in vitro:Да, сделай это.Если ваше приложение настолько критично ко времени, сделайте это.С другой стороны, ваше описание намекает на другую проблему:если ваши дельты находятся в интервале 1e-3 секунды, то это звучит как проблема вычислительной сложности, поскольку рассматриваемый метод должен вызываться очень и очень часто (для нескольких запусков 1e-3 секунды можно пренебречь).

Проблемная область, которую вы моделируете, кажется ОЧЕНЬ сложной, а наборы данных, вероятно, огромны.Такие вещи всегда интересны.Однако сначала убедитесь, что у вас есть абсолютно правильные структуры данных и алгоритмы, а затем микрооптимизируйте все, что захотите. Итак, я бы сказал, сначала посмотрите на весь контекст. ;-)

Ради интереса, в чем задачу вы рассчитываете?

У вас есть большой контроль над оптимизацией вашей компиляции.-O1, -O2 и т. д. — это всего лишь псевдонимы группы переключателей.

Со страниц руководства

       -O2 turns on all optimization flags specified by -O.  It also turns
       on the following optimization flags: -fthread-jumps -falign-func‐
       tions  -falign-jumps -falign-loops  -falign-labels -fcaller-saves
       -fcrossjumping -fcse-follow-jumps  -fcse-skip-blocks
       -fdelete-null-pointer-checks -fexpensive-optimizations -fgcse
       -fgcse-lm -foptimize-sibling-calls -fpeephole2 -fregmove -fre‐
       order-blocks  -freorder-functions -frerun-cse-after-loop
       -fsched-interblock  -fsched-spec -fschedule-insns  -fsched‐
       ule-insns2 -fstrict-aliasing -fstrict-overflow -ftree-pre
       -ftree-vrp

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

       ...
       Alternatively you can discover which binary optimizations are
       enabled by -O3 by using:

               gcc -c -Q -O3 --help=optimizers > /tmp/O3-opts
               gcc -c -Q -O2 --help=optimizers > /tmp/O2-opts
               diff /tmp/O2-opts /tmp/O3-opts Φ grep enabled

Как только вы найдете виновную оптимизацию, вам не понадобятся cout.

Если это возможно для вас, вы можете попробовать разделить свой код на:

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

В противном случае вы можете указать другой уровень оптимизации (похоже, вы используете gcc...) для функции тестирования с атрибутом оптимизации (см. http://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html#Function-Attributes).

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

#include <iostream>

// Mark coords as extern.
// Compiler is now NOT allowed to optimise away coords
// This it can not remove the loop where you initialise it.
// This is because the code could be used by another compilation unit
extern double coords[500][3];
double coords[500][3];

int main()
{

//perform a simple initialization of all coordinates:
for (int i=0; i<500; ++i)
 {
   coords[i][0] = 3.23;
   coords[i][1] = 1.345;
   coords[i][2] = 123.998;
 }


std::cout << "hello world !"<< std::endl;
return 0;
}

редактировать:самое простое, что вы можете сделать, это просто использовать данные каким-либо ложным способом после запуска функции и за пределами ваших тестов.Нравиться,

StartBenchmarking(); // ie, read a performance counter
for (int i=0; i<500; ++i)
 {
   coords[i][0] = 3.23;
   coords[i][1] = 1.345;
   coords[i][2] = 123.998;
 }
StopBenchmarking(); // what comes after this won't go into the timer

// this is just to force the compiler to use coords
double foo;
for (int j = 0 ; j < 500 ; ++j )
{
  foo += coords[j][0] + coords[j][1] + coords[j][2]; 
}
cout << foo;

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

void test1( volatile double *coords )
{
  //perform a simple initialization of all coordinates:
  for (int i=0; i<1500; i+=3)
  {
    coords[i+0] = 3.23;
    coords[i+1] = 1.345;
    coords[i+2] = 123.998;
  }
}

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

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

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

Я не знаю, есть ли у GCC подобная функция, но с VC++ вы можете использовать:

#pragma optimize

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

Просто небольшой пример нежелательной оптимизации:

#include <vector>
#include <iostream>

using namespace std;

int main()
{
double coords[500][3];

//perform a simple initialization of all coordinates:
for (int i=0; i<500; ++i)
 {
   coords[i][0] = 3.23;
   coords[i][1] = 1.345;
   coords[i][2] = 123.998;
 }


cout << "hello world !"<< endl;
return 0;
}

Если вы прокомментируете код «double coords[500][3]» до конца цикла for, он сгенерирует точно такой же ассемблерный код (только что попробовал с g++ 4.3.2).Я знаю, что этот пример слишком прост, и мне не удалось продемонстрировать такое поведение с помощью std::vector простой структуры «Координаты».

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

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

при запуске читать из файла.в вашем коде, скажем, if(input == "x") cout<< result_of_benchmark;

Компилятор не сможет исключить вычисление, и если вы убедитесь, что входные данные не равны «x», вы не сможете протестировать iostream.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top