Является ли хорошей идеей использовать vector <vector <double >> для формирования матричного класса для высокопроизводительного научного вычислительного кода?

37

Является ли хорошей идеей использовать vector<vector<double>>(используя std) для формирования матричного класса для высокопроизводительного научного вычислительного кода?

Если ответ нет. Зачем? Благодарность

cfdgeek
источник
2
-1 Конечно, это плохая идея. Вы не сможете использовать blas, lapack или любую другую существующую матричную библиотеку с таким форматом хранения. Кроме того, вы вводите неэффективность данных нелокальности и косвенности данных
Томас Климпел
9
@ Томас Означает ли это, что стоит понизить голос?
AKID
33
Не отрицайте. Это законный вопрос, даже если это ошибочная идея.
Вольфганг Бангерт
3
std :: vector не является распределенным вектором, поэтому вы не сможете выполнять с ним много параллельных вычислений (за исключением машин с общей памятью), вместо этого используйте Petsc или Trilinos. Кроме того, каждый обычно имеет дело с разреженными матрицами, и вы будете хранить полные плотные матрицы. Для игры с разреженными матрицами вы можете использовать std :: vector <std :: map>, но опять же, это не очень хорошо работает, см. Пост @WolfgangBangerth ниже.
gnzlbg
3
попробуйте использовать std :: vector <std :: vector <double >> с MPI, и вам захочется снимать себя
pyCthon

Ответы:

43

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

Это также расточительный формат хранения: std :: vector хранит два указателя, один на начало массива и один на конец, потому что длина массива гибкая. С другой стороны, чтобы это была правильная матрица, длины всех строк должны быть одинаковыми, и поэтому было бы достаточно сохранить количество столбцов только один раз, вместо того, чтобы каждая строка сохраняла свою длину независимо.

Вольфганг Бангерт
источник
Это на самом деле хуже, чем вы говорите, потому что на std::vectorсамом деле хранит три указателя: начало, конец и конец выделенной области хранения (например, позволяет нам вызывать .capacity()). Эта емкость может отличаться от размера, что значительно ухудшает ситуацию!
user14717
18

В дополнение к причинам, указанным Вольфгангом, если вы используете a vector<vector<double> >, вам придется разыменовывать его дважды каждый раз, когда вы хотите извлечь элемент, что в вычислительном отношении обходится дороже, чем одна операция разыменования. Один из типичных подходов заключается в выделении одного массива (а vector<double>или а double *). Я также видел, как люди добавляли синтаксический сахар в классы матрицы, оборачивая вокруг этого единственного массива некоторые более интуитивные операции индексации, чтобы уменьшить количество «умственных издержек», необходимых для вызова правильных индексов.

Джефф Оксберри
источник
5

Это действительно такая плохая вещь?

@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
Маркус Блатт
источник
Вы пишете: «В моей системе теперь явный победитель» - вы не имели в виду явного победителя?
AKID
9
-1 Понимание производительности hpc-кода может быть нетривиальным. В вашем случае размер матрицы просто превышает размер кэша, так что вы просто измеряете пропускную способность памяти вашей системы. Если я изменю N на 200 и увеличу количество итераций до 1000, я получу «calc take: 65» против «calc взял: 36». Если я дополнительно заменим a = a * a на a + = a1 * a2, чтобы сделать его более реалистичным, я получу «calc взял: 176« против »calc взял: 84». Таким образом, похоже, что вы можете потерять фактор два в производительности, используя вектор векторов вместо матрицы. Реальная жизнь будет более сложной, но это все еще плохая идея.
Томас Климпел
да, но попробуйте использовать std :: vectors с MPI, C выигрывает
PyCthon
4

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

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

Использование библиотек HPC

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

Больше возможностей для ошибки кодирования

std::vectorничего не знает о своих записях. Если вы заполняете a std::vectorбольшим количеством std::vectors, тогда ваша задача - убедиться, что все они имеют одинаковый размер, потому что помните, что мы хотим, чтобы матрица и матрицы не имели переменного числа строк (или столбцов). Таким образом, вам придется вызывать все правильные конструкторы для каждой записи вашего внешнего вектора, и любой, кто использует ваш код, должен сопротивляться искушению использовать std::vector<T>::push_back()любой из внутренних векторов, что приведет к поломке всего следующего кода. Конечно, вы можете запретить это, если вы пишете свой класс правильно, но гораздо проще реализовать это просто с большим непрерывным распределением.

HPC культура и ожидания

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

Проще рассуждать о производительности данных более низкого уровня

Переход к низкоуровневому представлению желаемой структуры данных облегчает вашу жизнь в долгосрочной перспективе для HPC. Использование таких инструментов, как perfи vtune, даст вам измерения счетчика производительности очень низкого уровня, которые вы попытаетесь объединить с традиционными результатами профилирования, чтобы повысить производительность вашего кода. Если в вашей структуре данных используется много модных контейнеров, будет трудно понять, что ошибки в кеше происходят из-за проблемы с контейнером или неэффективности самого алгоритма. Для более сложных контейнеров кода необходимы, но для матричной алгебры это на самом деле не так - вы можете обойтись просто 1 std::vectorдля хранения данных, а не n std::vectors, так что продолжайте.

Reid.Atcheson
источник
1

Я также пишу тест. Для матрицы малого размера (<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;
}
Майкл
источник
0

Как уже отмечали другие, не пытайтесь делать с этим математику или делать что-либо действенное.

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

Вы можете просто объединить все ваши векторные входные данные в один буфер по мере их поступления, но код будет более долговечным и более читабельным, если вы используете a vector<vector<T>>.

Ченнинг Мур
источник