производительность — необъяснимые затраты, связанные с извлечением данных из функции в оптимизированном и синхронизированном коде C ++

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

Весь код был скомпилирован со следующими флагами:

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 );
}
}

Вот некоторые важные детали о данных:

  • 20 миллионов пар читаются при стандартном вводе (перенаправляются из файла)
  • 20 миллионов звонков в c.doWork
  • 60 миллионов ИТОГО через внешний цикл в c.doWork
  • 180 миллионов ИТОГО по внутреннему циклу в c.doWork

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

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

  1. Внесение изменений в код внутри циклов меняет время.
  2. Исходное время составляет 5 минут 50 секунд, в то время как простое чтение пар из файла занимает 12 секунд, так что много является делается.
  3. Может быть, только операции, связанные с подсчетами, оптимизируются, но это кажется странно избирательной оптимизацией, учитывая, что если их оптимизировать, компилятор может понять, что поддержка вычислений в doWork также не нужна.
  4. Если операции, связанные с подсчетами, оптимизируются, почему они не оптимизированы в других случаях. Я на самом деле не использую их в основном.

Действительно ли doWork компилируется и оптимизируется независимо от main, и, таким образом, если функция имеет какое-либо обязательство возвращать данные в любой форме, она не может быть уверена в том, будут ли они использоваться или нет?

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

Я благодарен за любой свет, который вы можете пролить.

9

Решение

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

Но сначала я бы сделал паузы, просто чтобы быть уверенным.
После исправления я сделал бы это снова, чтобы увидеть, что будет дальше.

0

Другие решения

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

0

Есть моменты, когда неортодоксальные решения применимы, и это может быть один. Рассматривали ли вы сделать массив глобальным?

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

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

0
По вопросам рекламы [email protected]