Выбор наиболее эффективного контейнера (массива)
Это мой маленький большой вопрос о контейнерах, в частности, массивах.
Я пишу физический код, который в основном манипулирует большим ( > 1 000 000) набором "частиц" (с координатами 6 double
). Я ищу лучший способ (с точки зрения производительности) реализовать класс, который будет содержать контейнер для этих данных, и который предоставит примитивы манипуляции для этих данных (например, экземпляр, operator[]
и т.д.).
Существует несколько ограничений на использование этого набора:
- его размер считывается из файла конфигурации и не изменяется во время выполнения.
- его можно рассматривать как большой двумерный массив из N (например, 1 000 000) строк и 6 столбцов (каждый из которых хранит координату в одном измерении).
- массив обрабатывается в большом цикле, к каждой "частице/линии" обращаются, и вычисление происходит с его координатами, а результаты сохраняются для этой частицы и т.д. для каждой частицы и т.д. для каждого итерация большой петли.
- новые элементы не добавляются и не удаляются во время выполнения
Первый вывод, поскольку доступ к элементам по существу осуществляется путем доступа к каждому элементу один за другим с помощью []
, я думаю, что я должен использовать обычный динамический массив.
Я изучил несколько вещей, и я хотел бы получить ваше мнение о том, что может дать мне лучшие результаты.
Как я понимаю, нет преимущества использовать динамически выделенный массив вместо std::vector
, поэтому такие вещи, как double** array2d = new ..., loop of new, etc
, исключаются.
Так что неплохо использовать std::vector<double>
?
Если я использую std::vector
, должен ли я создать двумерный массив вроде std::vector<std::vector<double> > my_array
, который может быть проиндексирован как my_array[i][j]
, или это плохая идея, и было бы лучше использовать std::vector<double> other_array
и использовать его с other_array[6*i+j]
.
Может быть, это может обеспечить лучшую производительность, тем более, что количество столбцов фиксировано и известно с самого начала.
Если вы считаете, что это лучший вариант, можно ли обернуть этот вектор таким образом, чтобы к нему можно было обращаться с помощью оператора индекса, определенного как other_array[i,j] // same as other_array[6*i+j]
без накладных расходов (например, вызов функции при каждом доступе)?
Другим вариантом, который я использую до сих пор, является использование Blitz, в частности blitz::Array
:
typedef blitz::Array<double,TWO_DIMENSIONS> store_t;
store_t my_store;
Где доступны мои элементы: my_store(line, column);
.
Я думаю, что нет никакого преимущества использовать Blitz в моем случае, потому что я обращаюсь к каждому элементу один за другим, и что Блиц был бы интересен, если бы я использовал операции непосредственно с массивом (например, с матричным умножением), которым я не являюсь.
Считаете ли вы, что Blitz в порядке, или это бесполезно в моем случае?
Это те возможности, которые я рассмотрел до сих пор, но, возможно, лучший из них, я еще один, поэтому не стесняйтесь предлагать мне другие вещи.
Большое спасибо за вашу помощь по этой проблеме!
Edit:
Из очень интересных ответов и комментариев ниже хорошее решение выглядит следующим образом:
- Используйте структуру
particle
(содержащую 6 двойников) или статический массив из 6 удвоений (это позволяет избежать использования двумерных динамических массивов)
- Используйте
vector
или deque
этой структуры или массива particle
. Затем полезно перемещать их с помощью итераторов, и это позволит впоследствии перейти от одного к другому.
Кроме того, я могу использовать Blitz::TinyVector<double,6>
вместо структуры.
Ответы
Ответ 1
Прежде всего, вы не хотите разбросать координаты одной данной частицы по всему месту, поэтому я бы начал писать простой struct
:
struct Particle { /* coords */ };
Тогда мы можем сделать простой одномерный массив этих Particles
.
Я бы, вероятно, использовал deque
, потому что этот контейнер по умолчанию, но вы можете попробовать vector
, это всего лишь 1.000.000 частиц означает один кусок из нескольких МБ. Он должен держаться, но это может повредить вашу систему, если это когда-либо будет расти, а deque
выделит несколько кусков.
Внимание
Как заметил Alexandre C, если вы идете по дороге deque
, воздержитесь от использования operator[]
и предпочитаете использовать стиль итерации. Если вам действительно нужен произвольный доступ и он чувствителен к производительности, vector
должен быть быстрее.
Ответ 2
Так что неплохо использовать std::vector<double>
?
Обычно, std::vector
должен быть первым выбором контейнера. Вы можете использовать либо std::vector<>::reserve()
, либо std::vector<>::resize()
, чтобы избежать перераспределения при заполнении вектора. Лучше ли какой-либо другой контейнер можно найти с помощью измерения. И только путем измерения. Но сначала измерьте, все ли что-либо, в котором участвует контейнер (заполнение, доступ к элементам), стоит оптимизировать вообще.
Если я использую std::vector, должен ли я создать двухмерный массив, например std::vector<std::vector<double> >
[...]?
Нет. IIUC, вы получаете доступ к своим данным для каждой частицы, а не к каждой строке. Если это так, почему бы не использовать a std::vector<particle>
, где particle
- это структура, содержащая шесть значений? И даже если я неправильно понял, лучше написать двумерную оболочку вокруг одномерного контейнера. Затем выровняйте свои данные в строках или столбцах - что быстрее с вашими шаблонами доступа.
Считаете ли вы, что Blitz в порядке, или это бесполезно в моем случае?
У меня нет практических знаний о blitz ++ и областях, в которых он используется. Но разве blitz ++ не все о шаблонах выражений для разворачивания операций цикла и оптимизации временных времен при манипулировании матрицами? ICBWT.
Ответ 3
Первое правило при выборе из контейнеров - использовать std::vector
. Затем, только после того, как ваш код будет завершен, и вы сможете измерить производительность, вы можете попробовать другие контейнеры. Но сначала придерживайтесь вектора. (И используйте reserve()
с самого начала)
Тогда вы не должны использовать std::vector<std::vector<double> >
. Вы знаете размер ваших данных: он 6 удваивается. Не нужно, чтобы он был динамичным. Он постоянный и фиксированный. Вы можете определить структуру, которая удерживает вас в элементах частиц (шесть парных), или вы можете просто набрать ее: typedef double particle[6]
. Затем используйте вектор частиц: std::vector<particle>
.
Кроме того, поскольку ваша программа последовательно использует данные о частицах, содержащиеся в векторе, вы сможете воспользоваться преимуществами современной функции чтения кэш-памяти CPU с максимальной эффективностью.
Ответ 4
Вы можете пойти несколькими способами. Но в вашем случае не объявить std::vector<std::vector<double> >
. Вы выделяете vector
(и копируете его) на каждые 6 удвоений. Это слишком дорого.
Ответ 5
Если вы считаете, что это лучший вариант, можно ли обернуть этот вектор таким образом, чтобы к нему можно было обращаться с помощью оператора индекса, определенного как other_array [i, j]//так же, как other_array [6 * я + j] без накладных расходов (например, вызов функции при каждом доступе)?
(other_array[i,j]
не будет работать слишком хорошо, поскольку i, j использует оператор запятой для оценки значения "i", затем отбрасывает и вычисляет и возвращает "j", поэтому он эквивалентен other_array[i]
).
Вам нужно будет использовать один из:
other_array[i][j]
other_array(i, j) // if other_array implements operator()(int, int),
// but std::vector<> et al don't.
other_array[i].identifier // identifier is a member variable
other_array[i].identifier() // member function getting value
other_array[i].identifier(double) // member function setting value
Вы можете или не хотите ставить get_ и set_ или аналогичные по двум последним функциям, если вы найдете их полезными, но из вашего вопроса, я думаю, вы не будете: функции предпочтительны в API-интерфейсах между частями больших систем с участием многих разработчиков или когда элементы данных могут меняться, и вы хотите, чтобы алгоритмы, работающие с данными, были независимыми от них.
Итак, хороший тест: если вы обнаружите, что пишете код типа other_array[i][3]
, где вы решили, что "3" - это двойное число со скоростью в нем и other_array[i][5]
, потому что "5" - это ускорение, тогда прекратите делать это и дайте им правильные идентификаторы, чтобы вы могли сказать other_array[i].speed
и .acceleration
. Тогда другие разработчики могут читать и понимать это, и вы гораздо менее склонны совершать случайные ошибки. С другой стороны, если вы повторяете эти 6 элементов, которые делают одинаковые вещи для каждого, тогда вы, вероятно, захотите, чтобы Particle удерживал двойную [6] или предоставлял operator[](int)
. Там нет проблем с выполнением обоих:
struct Particle
{
double x[6];
double& speed() { return x[3]; }
double speed() const { return x[3]; }
double& acceleration() { return x[5]; }
...
};
BTW/причина, по которой vector<vector<double> >
может быть слишком дорогостоящей, состоит в том, что каждый набор из 6 удваивается будет распределяться в куче, а для быстрого выделения и освобождения многих реализаций кучи используют ведра фиксированного размера, поэтому ваш небольшой запрос будет округляется до следующего размера: это может быть значительным накладным расходами. Внешнему вектору также потребуется записать дополнительный указатель на эту память. Кроме того, распределение кучи и освобождение относительно медленное - в вашем случае вы будете делать это только при запуске и завершении работы, но нет особого смысла в том, чтобы сделать вашу программу медленнее без причины. Еще важнее то, что области в куче могут иметь место только в памяти, поэтому ваш оператор [] может иметь ошибки кэша, занимая более важные страницы памяти, чем необходимо, замедляя всю программу. Другими словами, векторы сохраняют элементы смежно, но направленные к векторам могут не быть смежными.