Чи корисно використовувати vector<vector<double>>
(використовуючи std) для формування матричного класу для високоефективного наукового обчислювального коду?
Якщо відповідь - ні. Чому? Спасибі
Чи корисно використовувати vector<vector<double>>
(використовуючи std) для формування матричного класу для високоефективного наукового обчислювального коду?
Якщо відповідь - ні. Чому? Спасибі
Відповіді:
Це погана ідея, оскільки вектор повинен виділити стільки об'єктів у просторі, скільки є рядків у вашій матриці. Виділення коштує дорого, але, в першу чергу, це погана ідея, оскільки тепер дані вашої матриці існують у ряді масивів, розкиданих по пам'яті, а не в одному місці, де кеш процесора може легко отримати до неї доступ.
Це також марнотратний формат зберігання: std :: vector зберігає два покажчики, один на початок масиву і один до кінця, оскільки довжина масиву гнучка. З іншого боку, щоб це була правильна матриця, довжина всіх рядків повинна бути однаковою, і тому було б достатньо зберегти кількість стовпців лише один раз, а не дозволяти кожному рядку зберігати свою довжину незалежно.
std::vector
насправді зберігається три покажчики: початок, кінець та кінець виділеної області зберігання (що дозволяє нам дзвонити, наприклад .capacity()
). Ця ємність може бути різною, ніж розмір, робить ситуацію набагато гіршою!
На додаток до причин, які Вольфганг згадував, якщо ви користуєтесь vector<vector<double> >
символом a , вам доведеться його скинути двічі кожен раз, коли ви бажаєте отримати елемент, що обчислюється затратніше, ніж одна операція з перенаправлення. Один типовий підхід - замість цього виділити один масив (a vector<double>
або a 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 приходять до тями, але також і всюдисущу бібліотеку HPI MPI було б набагато важче використовувати.
std::vector
нічого не знає про його записи. Якщо ви заповнили 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;
}
Як вже вказували інші, не намагайтеся з цим займатися математикою і не робіть нічого виконавського.
Це означає, що я використовував цю структуру як тимчасову, коли коду потрібно зібрати 2-D масив, розміри якого будуть визначені під час виконання та після того, як ви почали зберігати дані. Наприклад, збираючи векторні виходи з якогось дорогого процесу, коли не просто вирахувати, скільки саме векторів потрібно буде зберігати при запуску.
Ви можете просто об'єднати всі свої вхідні вектори в один буфер, коли вони надходять, але код буде більш міцним і читабельним, якщо ви використовуєте vector<vector<T>>
.