Является ли хорошей идеей использовать vector<vector<double>>
(используя std) для формирования матричного класса для высокопроизводительного научного вычислительного кода?
Если ответ нет. Зачем? Благодарность
Является ли хорошей идеей использовать vector<vector<double>>
(используя std) для формирования матричного класса для высокопроизводительного научного вычислительного кода?
Если ответ нет. Зачем? Благодарность
Ответы:
Это плохая идея, потому что vector должен разместить столько объектов в пространстве, сколько строк в вашей матрице. Распределение стоит дорого, но в первую очередь это плохая идея, потому что данные вашей матрицы теперь существуют в ряде массивов, разбросанных по памяти, а не в одном месте, где кеш процессора может легко получить к нему доступ.
Это также расточительный формат хранения: std :: vector хранит два указателя, один на начало массива и один на конец, потому что длина массива гибкая. С другой стороны, чтобы это была правильная матрица, длины всех строк должны быть одинаковыми, и поэтому было бы достаточно сохранить количество столбцов только один раз, вместо того, чтобы каждая строка сохраняла свою длину независимо.
std::vector
самом деле хранит три указателя: начало, конец и конец выделенной области хранения (например, позволяет нам вызывать .capacity()
). Эта емкость может отличаться от размера, что значительно ухудшает ситуацию!
В дополнение к причинам, указанным Вольфгангом, если вы используете a vector<vector<double> >
, вам придется разыменовывать его дважды каждый раз, когда вы хотите извлечь элемент, что в вычислительном отношении обходится дороже, чем одна операция разыменования. Один из типичных подходов заключается в выделении одного массива (а vector<double>
или а double *
). Я также видел, как люди добавляли синтаксический сахар в классы матрицы, оборачивая вокруг этого единственного массива некоторые более интуитивные операции индексации, чтобы уменьшить количество «умственных издержек», необходимых для вызова правильных индексов.
Нет, используйте одну из бесплатных библиотек линейной алгебры. Обсуждение различных библиотек можно найти здесь: Рекомендации по быстрой и полезной матричной библиотеке C ++?
Это действительно такая плохая вещь?
@Wolfgang: в зависимости от размера плотной матрицы два дополнительных указателя на строку могут быть незначительными. Что касается разбросанных данных, можно подумать об использовании пользовательского распределителя, который гарантирует, что векторы находятся в непрерывной памяти. Пока память не используется повторно, даже стандартный распределитель будет использовать непрерывную память с разницей в два указателя.
@Geoff: если вы используете произвольный доступ и используете только один массив, вам все равно нужно рассчитать индекс. Не может быть быстрее.
Итак, давайте сделаем небольшой тест:
vectormatrix.cc:
#include<vector>
#include<iostream>
#include<random>
#include <functional>
#include <sys/time.h>
int main()
{
int N=1000;
struct timeval start, end;
std::cout<< "Checking differenz between last entry of previous row and first entry of this row"<<std::endl;
std::vector<std::vector<double> > matrix(N, std::vector<double>(N, 0.0));
for(std::size_t i=1; i<N;i++)
std::cout<< "index "<<i<<": "<<&(matrix[i][0])-&(matrix[i-1][N-1])<<std::endl;
std::cout<<&(matrix[0][N-1])<<" "<<&(matrix[1][0])<<std::endl;
gettimeofday(&start, NULL);
int k=0;
for(int j=0; j<100; j++)
for(std::size_t i=0; i<N;i++)
for(std::size_t j=0; j<N;j++, k++)
matrix[i][j]=matrix[i][j]*matrix[i][j];
gettimeofday(&end, NULL);
double seconds = end.tv_sec - start.tv_sec;
double useconds = end.tv_usec - start.tv_usec;
double mtime = ((seconds) * 1000 + useconds/1000.0) + 0.5;
std::cout<<"calc took: "<<mtime<<" k="<<k<<std::endl;
std::normal_distribution<double> normal_dist(0, 100);
std::mt19937 engine; // Mersenne twister MT19937
auto generator = std::bind(normal_dist, engine);
for(std::size_t i=1; i<N;i++)
for(std::size_t j=1; j<N;j++)
matrix[i][j]=generator();
}
А теперь используя один массив:
arraymatrix.cc
#include<vector>
#include<iostream>
#include<random>
#include <functional>
#include <sys/time.h>
int main()
{
int N=1000;
struct timeval start, end;
std::cout<< "Checking difference between last entry of previous row and first entry of this row"<<std::endl;
double* matrix=new double[N*N];
for(std::size_t i=1; i<N;i++)
std::cout<< "index "<<i<<": "<<(matrix+(i*N))-(matrix+(i*N-1))<<std::endl;
std::cout<<(matrix+N-1)<<" "<<(matrix+N)<<std::endl;
int NN=N*N;
int k=0;
gettimeofday(&start, NULL);
for(int j=0; j<100; j++)
for(double* entry =matrix, *endEntry=entry+NN;
entry!=endEntry;++entry, k++)
*entry=(*entry)*(*entry);
gettimeofday(&end, NULL);
double seconds = end.tv_sec - start.tv_sec;
double useconds = end.tv_usec - start.tv_usec;
double mtime = ((seconds) * 1000 + useconds/1000.0) + 0.5;
std::cout<<"calc took: "<<mtime<<" k="<<k<<std::endl;
std::normal_distribution<double> normal_dist(0, 100);
std::mt19937 engine; // Mersenne twister MT19937
auto generator = std::bind(normal_dist, engine);
for(std::size_t i=1; i<N*N;i++)
matrix[i]=generator();
}
На моей системе теперь явный победитель (компилятор gcc 4.7 с -O3)
время векторной матрицы печатает:
index 997: 3
index 998: 3
index 999: 3
0xc7fc68 0xc7fc80
calc took: 185.507 k=100000000
real 0m0.257s
user 0m0.244s
sys 0m0.008s
Мы также видим, что до тех пор, пока стандартный распределитель не перерабатывает освобожденную память, данные являются непрерывными. (Конечно, после некоторого освобождения это не гарантируется.)
матрица времени печатает:
index 997: 1
index 998: 1
index 999: 1
0x7ff41f208f48 0x7ff41f208f50
calc took: 187.349 k=100000000
real 0m0.257s
user 0m0.248s
sys 0m0.004s
Я не рекомендую это, но не из-за проблем с производительностью. Он будет немного менее производительным, чем традиционная матрица, которая обычно выделяется как большой кусок непрерывных данных, которые индексируются с использованием разыменования с одним указателем и целочисленной арифметики. Причиной снижения производительности являются главным образом различия в кешировании, но как только размер вашей матрицы станет достаточно большим, этот эффект будет амортизироваться, и если вы используете специальный распределитель для внутренних векторов, чтобы они были выровнены по границам кеша, то это еще больше смягчает проблему кеширования ,
Это само по себе не является достаточной причиной, чтобы не делать этого, по моему мнению. Причина для меня в том, что это создает много головной боли при кодировании. Вот список головных болей, это приведет к долгосрочному
Если вы хотите использовать большинство библиотек HPC, вам нужно будет выполнить итерацию по вашему вектору и поместить все их данные в непрерывный буфер, потому что большинство библиотек HPC ожидают этого явного формата. Вспоминаются BLAS и LAPACK, но использовать универсальную библиотеку HPC MPI будет гораздо сложнее.
std::vector
ничего не знает о своих записях. Если вы заполняете a std::vector
большим количеством std::vector
s, тогда ваша задача - убедиться, что все они имеют одинаковый размер, потому что помните, что мы хотим, чтобы матрица и матрицы не имели переменного числа строк (или столбцов). Таким образом, вам придется вызывать все правильные конструкторы для каждой записи вашего внешнего вектора, и любой, кто использует ваш код, должен сопротивляться искушению использовать std::vector<T>::push_back()
любой из внутренних векторов, что приведет к поломке всего следующего кода. Конечно, вы можете запретить это, если вы пишете свой класс правильно, но гораздо проще реализовать это просто с большим непрерывным распределением.
Программисты HPC просто ожидают данных низкого уровня. Если вы дадите им матрицу, существует ожидание, что если они захватят указатель на первый элемент матрицы и указатель на последний элемент матрицы, то все указатели между этими двумя являются действительными и указывают на элементы того же самого матрица. Это похоже на мой первый пункт, но отличается, потому что оно может быть связано не столько с библиотеками, сколько с членами команды или с кем-то, с кем вы делитесь своим кодом.
Переход к низкоуровневому представлению желаемой структуры данных облегчает вашу жизнь в долгосрочной перспективе для HPC. Использование таких инструментов, как perf
и vtune
, даст вам измерения счетчика производительности очень низкого уровня, которые вы попытаетесь объединить с традиционными результатами профилирования, чтобы повысить производительность вашего кода. Если в вашей структуре данных используется много модных контейнеров, будет трудно понять, что ошибки в кеше происходят из-за проблемы с контейнером или неэффективности самого алгоритма. Для более сложных контейнеров кода необходимы, но для матричной алгебры это на самом деле не так - вы можете обойтись просто 1
std::vector
для хранения данных, а не n
std::vector
s, так что продолжайте.
Я также пишу тест. Для матрицы малого размера (<100 * 100) производительность аналогична для вектора <вектор <двойной >> и обернутого 1D вектора. Для матрицы большого размера (~ 1000 * 1000) лучше обернуть 1D вектор. Собственная матрица ведет себя хуже. Меня удивляет, что Эйген - худший.
#include <iostream>
#include <iomanip>
#include <fstream>
#include <sstream>
#include <algorithm>
#include <map>
#include <vector>
#include <string>
#include <cmath>
#include <numeric>
#include "time.h"
#include <chrono>
#include <cstdlib>
#include <Eigen/Dense>
using namespace std;
using namespace std::chrono; // namespace for recording running time
using namespace Eigen;
int main()
{
const int row = 1000;
const int col = row;
const int N = 1e8;
// 2D vector
auto start = high_resolution_clock::now();
vector<vector<double>> vec_2D(row,vector<double>(col,0.));
for (int i = 0; i < N; i++)
{
for (int i=0; i<row; i++)
{
for (int j=0; j<col; j++)
{
vec_2D[i][j] *= vec_2D[i][j];
}
}
}
auto stop = high_resolution_clock::now();
auto duration = duration_cast<microseconds>(stop - start);
cout << "2D vector: " << duration.count()/1e6 << " s" << endl;
// 2D array
start = high_resolution_clock::now();
double array_2D[row][col];
for (int i = 0; i < N; i++)
{
for (int i=0; i<row; i++)
{
for (int j=0; j<col; j++)
{
array_2D[i][j] *= array_2D[i][j];
}
}
}
stop = high_resolution_clock::now();
duration = duration_cast<microseconds>(stop - start);
cout << "2D array: " << duration.count() / 1e6 << " s" << endl;
// wrapped 1D vector
start = high_resolution_clock::now();
vector<double> vec_1D(row*col, 0.);
for (int i = 0; i < N; i++)
{
for (int i=0; i<row; i++)
{
for (int j=0; j<col; j++)
{
vec_1D[i*col+j] *= vec_1D[i*col+j];
}
}
}
stop = high_resolution_clock::now();
duration = duration_cast<microseconds>(stop - start);
cout << "1D vector: " << duration.count() / 1e6 << " s" << endl;
// eigen 2D matrix
start = high_resolution_clock::now();
MatrixXd mat(row, col);
for (int i = 0; i < N; i++)
{
for (int j=0; j<col; j++)
{
for (int i=0; i<row; i++)
{
mat(i,j) *= mat(i,j);
}
}
}
stop = high_resolution_clock::now();
duration = duration_cast<microseconds>(stop - start);
cout << "2D eigen matrix: " << duration.count() / 1e6 << " s" << endl;
}
Как уже отмечали другие, не пытайтесь делать с этим математику или делать что-либо действенное.
Тем не менее, я использовал эту структуру как временную, когда код должен собрать двумерный массив, размеры которого будут определены во время выполнения и после того, как вы начали хранить данные. Например, сбор векторных выходных данных из какого-то дорогостоящего процесса, в котором непросто точно рассчитать, сколько векторов нужно сохранить при запуске.
Вы можете просто объединить все ваши векторные входные данные в один буфер по мере их поступления, но код будет более долговечным и более читабельным, если вы используете a vector<vector<T>>
.