Чи корисно використовувати вектор <vector <double>> для формування матричного класу для високоефективного наукового обчислювального коду?


37

Чи корисно використовувати vector<vector<double>>(використовуючи std) для формування матричного класу для високоефективного наукового обчислювального коду?

Якщо відповідь - ні. Чому? Спасибі


2
-1 Звичайно, це погана ідея. Ви не зможете використовувати blas, lapack або будь-яку іншу існуючу матричну бібліотеку з таким форматом зберігання. Крім того, ви впроваджуєте неефективність щодо не локалізації даних та непрямості
Томас Клімпель,

9
@Thomas Чи справді це є підставою для протистояння?
акід

33
Не зволікайте. Це законне питання, навіть якщо це помилкова ідея.
Вольфганг Бангерт

3
std :: vector не є розподіленим вектором, тому ви не зможете робити багато паралельних обчислень з ним (крім машин спільної пам'яті), використовуйте замість них Petsc або Trilinos. Крім того, одна зазвичай має справу з рідкими матрицями, і ви зберігаєте повні щільні матриці. Для гри з розрідженими матрицями ви можете використовувати std :: vector <std :: map>, але знову ж таки це буде не дуже добре, див. Пост @WolfgangBangerth нижче.
gnzlbg

3
спробуйте використовувати std :: vector <std :: vector <double>> з MPI, і ви захочете зняти себе
pyCthon

Відповіді:


43

Це погана ідея, оскільки вектор повинен виділити стільки об'єктів у просторі, скільки є рядків у вашій матриці. Виділення коштує дорого, але, в першу чергу, це погана ідея, оскільки тепер дані вашої матриці існують у ряді масивів, розкиданих по пам'яті, а не в одному місці, де кеш процесора може легко отримати до неї доступ.

Це також марнотратний формат зберігання: std :: vector зберігає два покажчики, один на початок масиву і один до кінця, оскільки довжина масиву гнучка. З іншого боку, щоб це була правильна матриця, довжина всіх рядків повинна бути однаковою, і тому було б достатньо зберегти кількість стовпців лише один раз, а не дозволяти кожному рядку зберігати свою довжину незалежно.


Це насправді гірше, ніж ви говорите, адже std::vectorнасправді зберігається три покажчики: початок, кінець та кінець виділеної області зберігання (що дозволяє нам дзвонити, наприклад .capacity()). Ця ємність може бути різною, ніж розмір, робить ситуацію набагато гіршою!
користувач14717

18

На додаток до причин, які Вольфганг згадував, якщо ви користуєтесь vector<vector<double> >символом a , вам доведеться його скинути двічі кожен раз, коли ви бажаєте отримати елемент, що обчислюється затратніше, ніж одна операція з перенаправлення. Один типовий підхід - замість цього виділити один масив (a vector<double>або a 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

Ви пишете "У моїй системі зараз очевидний переможець" - ти мав на увазі, що немає чіткого переможця?
акід

9
-1 Розуміння продуктивності коду hpc може бути нетривіальним. У вашому випадку розмір матриці просто перевищує розмір кешу, так що ви просто вимірюєте пропускну здатність пам'яті вашої системи. Якщо я змінити N на 200 і збільшити кількість ітерацій до 1000, я отримаю "calc взяв: 65" проти "calc взяв: 36". Якщо я додатково заміню a = a * a на + = a1 * a2, щоб зробити його більш реалістичним, я отримаю "calc взяв: 176" vs "calc взяв: 84". Таким чином, схоже, що ви можете втратити коефіцієнт два в продуктивності, використовуючи вектор векторів замість матриці. Справжнє життя буде складнішим, але це все-таки погана ідея.
Томас Клімпель

так, але спробуйте використовувати std ::
vectors

4

Я не рекомендую цього, але не через проблеми з продуктивністю. Це буде трохи менш ефективно, ніж традиційна матриця, яка зазвичай виділяється у вигляді великого відрізка суміжних даних, який індексується за допомогою одиночної затримки покажчика та цілої арифметики. Причиною удару в продуктивності є, головним чином, різниці кешування, але як тільки розмір матриці стане достатньо великим, цей ефект буде амортизований, і якщо ви будете використовувати спеціальний розподільник для внутрішніх векторів, щоб вони були вирівняні до меж кешування, то це ще більше пом'якшує проблему кешування .

Це само по собі не є достатньою причиною, щоб цього не робити. Причиною для мене є те, що це створює багато кодуючих головних болів. Ось перелік головних болів, які це спричинить довгострокові

Використання бібліотек HPC

Якщо ви хочете використовувати більшість бібліотек HPC, вам потрібно буде переглядати вектор і розміщувати всі їх дані у суміжному буфері, оскільки більшість бібліотек HPC очікують такого явного формату. BLAS та LAPACK приходять до тями, але також і всюдисущу бібліотеку HPI MPI було б набагато важче використовувати.

Більше потенціалу для помилки кодування

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

Культура та очікування HPC

Програмісти HPC просто очікують, що дані низького рівня. Якщо ви даєте їм матрицю, є очікування, що якщо вони схопили вказівник на перший елемент матриці та вказівник на останній елемент матриці, то всі покажчики між цими двома є дійсними і вказують на елементи того самого матриця. Це схоже на мій перший пункт, але відрізняється тим, що воно може бути пов'язане не стільки з бібліотеками, скільки з членами команди або з ким, з ким ви ділитесь своїм кодом.

Простіше міркувати про ефективність даних нижчого рівня

Перехід до найнижчого рівня потрібної структури даних полегшує ваше життя в довгостроковій перспективі для HPC. Використання таких інструментів як perfі vtuneдасть вам дуже низькі показники лічильника продуктивності, які ви спробуєте поєднати з традиційними результатами профілювання, щоб покращити продуктивність вашого коду. Якщо у вашій структурі даних використовується багато фантазійних контейнерів, важко буде зрозуміти, що помилки кешу виникають через проблему з контейнером або неефективність самого алгоритму. Для більш складних контейнерів коду необхідні, але для матричної алгебри вони насправді не є - ви можете обійтись лише 1 std::vectorдля зберігання даних, а не n std::vectors, так що продовжуйте це.


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

Як вже вказували інші, не намагайтеся з цим займатися математикою і не робіть нічого виконавського.

Це означає, що я використовував цю структуру як тимчасову, коли коду потрібно зібрати 2-D масив, розміри якого будуть визначені під час виконання та після того, як ви почали зберігати дані. Наприклад, збираючи векторні виходи з якогось дорогого процесу, коли не просто вирахувати, скільки саме векторів потрібно буде зберігати при запуску.

Ви можете просто об'єднати всі свої вхідні вектори в один буфер, коли вони надходять, але код буде більш міцним і читабельним, якщо ви використовуєте vector<vector<T>>.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.