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

<фон>

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

</ фон>

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

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

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

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

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

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

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


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

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

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

Martin York 12.01.2009 19:38

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

Martin York 12.01.2009 19:41
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
7
2
3 342
11
Перейти к ответу Данный вопрос помечен как решенный

Ответы 11

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

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

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

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

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

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

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

Из любопытства, какую задачу вы решаете?

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

ascobol 12.01.2009 19:29

У вас есть большой контроль над оптимизацией вашей компиляции. -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.

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

Konrad Rudolph 13.01.2009 00:22

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

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

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

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

#pragma optimize

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

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

+1 - Итог: найдите наименее опасный фрагмент кода, который вы можете добавить в цикл тестирования, чтобы заставить компилятор поверить в то, что он действительно что-то делает. Например, добавьте индекс итерации и т. д. К фиктивной переменной контрольной суммы, которая выводится / передается.

Ates Goral 12.01.2009 18:51

Разве это не меняет проблему? не мог ли компилятор по-прежнему думать, что что-то не имеет отношения к вычислению возвращаемого значения, и оптимизировать его?

Paolo Tedesco 12.01.2009 19:12

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

#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% ...

Мне кажется, это хорошая оптимизация! coords [] никогда не используется и также не имеет побочных эффектов при назначении. Просто потому, что компилятор умнее, чем вы не жалуетесь (он НИКОГДА не удалит то, что вам нужно, используется или имеет побочный эффект).

Martin York 12.01.2009 19:35

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

ascobol 12.01.2009 19:44
#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;
}

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

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

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

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

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

Ответ принят как подходящий

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

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

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

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