Я написал оптимизированный код для алгоритма, который вычисляет вектор величин. Я рассчитал это до и после различных попыток получить данные, вычисленные в функции, из функции. Я думаю, что конкретный характер вычисления или характер вектора величин не имеет значения. Краткое описание кода, сроки и детали приведены ниже.
Весь код был скомпилирован со следующими флагами:
g ++ -Wall -Wextra -Werror -std = c ++ 11 -pedantic -O3
У меня есть такой класс:
#ifndef C_H
#define C_H
#include <iostream>
#include <iterator>
#include <vector>
Class c {
public:
void doWork( int param1, int param2 ) const {
std::array<unsigned long,40> counts = {{0}};
// LOTS of branches and inexpensive operations:
// additions, subtractions, incrementations, and dereferences
for( /* loop 1 */ ) {
// LOTS MORE branches and inexpensive operations
counts[ /* data dependent */ ] += /* data dependent */;
for( /* loop 2 */ ) {
// YET MORE branches inexpensive operations
counts[ /* data dependent */ ] += /* data dependent */;
}
}
counts [ /* data dependent */ ] = /* data dependent */;
/* exclude for profiling
std::copy( &counts[0], &counts[40], std::ostream_iterator( std::cout, "," ) );
std::cout << "\n";
*/
}
private:
// there is private data here that is processed above
// the results get added into the array/vector as they are computed
};
#endif
А главное вот так:
#include <iostream>
#include "c.h"int main( int argc, char * argv ) {
Class c( //set the private data of c by passing data in );
int param1;
int param2;
while( std::cin >> param1 >> param2 ) {
c.doWork( int param1, int param2 );
}
}
Вот некоторые важные детали о данных:
Все это требует ровно 5 минут и 48 секунд для запуска. Естественно, я могу напечатать массив внутри функции класса, и это то, что я делал, но я собираюсь опубликовать код публично, и некоторые варианты использования могут включать желание сделать что-то, кроме печати вектора. В этом случае мне нужно изменить сигнатуру функции, чтобы фактически получить данные для пользователя. Вот где возникает проблема. Вещи, которые я пробовал:
Создание вектора в main и передача его по ссылке:
std::vector<unsigned long> counts( 40 );
while( std::cin >> param1 >> param2 ) {
c.doWork( param1, param2, counts );
std::fill( counts.begin(), counts.end(), 0 );
}
Это требует 7 минут 30 секунд. Удаление вызова std :: fill только уменьшает это на 15 секунд, так что это не учитывает расхождение.
Создание вектора в функции doWork и его возврат с использованием семантики перемещения.
Поскольку это требует динамического распределения для каждого результата, я не ожидал, что это будет быстро. Странно это не намного медленнее. 7 минут 40 секунд.
Возвращение std :: array в настоящее время в doWork по значению.
Естественно, это должно копировать данные по возвращении, так как массив стека не поддерживает семантику перемещения. 7 минут 30 секунд
Передача std :: array в виде ссылки.
while( std::cin >> param1 >> param2 ) {
std::array<unsigned long,40> counts = {{0}};
c.doWork( param1, param2, counts )
}
Я ожидаю, что это будет примерно эквивалентно оригиналу. Данные помещаются в стек в главной функции и передаются по ссылке в doWork, который заполняет их. 7 минут 20 секунд. Этот действительно блокирует меня.
Я не пробовал передавать указатели в doWork, потому что это должно быть эквивалентно передаче по ссылке.
Одно из решений, естественно, состоит в том, чтобы иметь две версии функции: одну, которая печатает локально, а другую, которая возвращает. Проблема состоит в том, что мне придется дублировать ВСЕ код, потому что вся проблема в том, что я не могу эффективно получить результаты из функции.
Так что я озадачен. Я понимаю, что любое из этих решений требует дополнительной разыменования для каждого доступа к массиву / вектору внутри doWork, но эти дополнительные разыменования очень тривиальны по сравнению с огромным количеством других быстрых операций и более проблемных ветвей, зависящих от данных.
Я приветствую любые идеи, чтобы объяснить это. Я думал только о том, что код оптимизируется компилятором, поэтому некоторые исходные компоненты вычисления в исходном случае опускаются, потому что компилятор понимает, что в этом нет необходимости. Но это кажется противопоказанным по нескольким причинам:
Действительно ли doWork компилируется и оптимизируется независимо от main, и, таким образом, если функция имеет какое-либо обязательство возвращать данные в любой форме, она не может быть уверена в том, будут ли они использоваться или нет?
Является ли мой метод профилирования без печати, который должен был избежать затрат на печать, чтобы подчеркнуть относительные различия в различных методах, ошибочным?
Я благодарен за любой свет, который вы можете пролить.
То, что я сделал бы, это приостановить это несколько раз и посмотреть, что он делает большую часть времени. Глядя на ваш код, я бы заподозрил, что больше всего времени занимает либо а) самый внутренний цикл, особенно вычисление индекса, либо 2) распределение std::array
,
Если размер counts
всегда 40, я бы просто
long counts[40];
memset(counts, 0, sizeof(counts));
Это выделяет в стеке, который не занимает много времени, и memset
не занимает много времени по сравнению с тем, что вы делаете.
Если размер изменяется во время выполнения, то я делаю статическое распределение, например:
void myRoutine(){
/* this does not claim to be pretty. it claims to be efficient */
static int nAlloc = 0;
static long* counts = NULL;
/* this initially allocates the array, and makes it bigger if necessary */
if (nAlloc < /* size I need */){
if (counts) delete counts;
nAlloc = /* size I need */;
counts = new long[nAlloc];
}
memset(counts, 0, sizeof(long)*nAlloc);
/* do the rest of the stuff */
}
Сюда, counts
всегда достаточно большой, и
дело в том, чтобы 1) сделать new
как можно меньше, и 2) сохранить индексацию в counts
как можно проще.
Но сначала я бы сделал паузы, просто чтобы быть уверенным.
После исправления я сделал бы это снова, чтобы увидеть, что будет дальше.
Оптимизация компилятора — это одно место, на которое стоит обратить внимание, но есть еще одно место, на которое нужно обратить внимание. Изменения, которые вы сделали в коде, могут нарушить структуру кэша. Если память, выделенная для массива, каждый раз находится в другой части памяти, количество пропусков кеша в вашей системе может увеличиться, что, в свою очередь, снижает производительность. Вы можете взглянуть на аппаратные счетчики производительности вашего ЦП, чтобы лучше об этом догадаться.
Есть моменты, когда неортодоксальные решения применимы, и это может быть один. Рассматривали ли вы сделать массив глобальным?
Тем не менее, одним из важнейших преимуществ локальных переменных является то, что оптимизатор может найти весь доступ к ним, используя информацию только из функции. Это делает регистрацию намного проще.
static
переменная внутри функции почти то же самое, но в вашем случае адрес этого массива стека исчезнет, снова обойдя оптимизатор.