vector<vector<double>>
고성능 과학 컴퓨팅 코드를위한 매트릭스 클래스를 형성 하기 위해 (std 를 사용 하여) 사용하는 것이 좋은 생각 입니까?
대답이 아니오 인 경우. 왜? 감사
vector<vector<double>>
고성능 과학 컴퓨팅 코드를위한 매트릭스 클래스를 형성 하기 위해 (std 를 사용 하여) 사용하는 것이 좋은 생각 입니까?
대답이 아니오 인 경우. 왜? 감사
답변:
벡터는 행렬에 행이있는만큼 공간에 많은 수의 객체를 할당해야하므로 나쁜 생각입니다. 할당은 비싸지 만, 주로 행렬의 데이터가 프로세서 캐시가 쉽게 액세스 할 수있는 한곳이 아닌 메모리 주위에 흩어져있는 여러 배열에 존재하기 때문에 나쁜 생각입니다.
또한 낭비적인 저장 형식입니다. std :: vector는 배열의 길이가 유연하기 때문에 하나는 배열의 시작 부분과 끝 부분에 두 개의 포인터를 저장합니다. 반면, 이것이 적절한 행렬이 되려면 모든 행의 길이가 같아야하므로 각 행의 길이를 독립적으로 저장하지 않고 열 수를 한 번만 저장하면 충분합니다.
std::vector
할당 된 저장 영역의 시작, 끝 및 끝 (예 :을 호출 할 수 있도록)이라는 세 개의 포인터를 실제로 저장 하기 때문에 실제로 말한 것보다 더 나쁩니다 .capacity()
. 용량이 크기와 다를 수 있으므로 상황이 훨씬 악화됩니다!
Wolfgang이 언급 한 이유 외에도을 사용하는 경우 vector<vector<double> >
요소를 검색 할 때마다 요소를 두 번 역 참조해야하는데 이는 단일 역 참조 작업보다 계산 비용이 많이 듭니다. 일반적인 방법 중 하나는 단일 배열 (a vector<double>
또는 a double *
)을 대신 할당하는 것입니다. 또한 사람들이이 단일 배열을 좀 더 직관적 인 인덱싱 작업으로 감싸서 적절한 인덱스를 호출하는 데 필요한 "정신 오버 헤드"의 양을 줄임으로써 매트릭스 클래스에 구문 설탕을 추가하는 것을 보았습니다.
아니요, 사용 가능한 무료 선형 대수 라이브러리 중 하나를 사용하십시오. 다른 라이브러리에 대한 논의는 여기에서 찾을 수 있습니다 : 유용하고 빠른 C ++ 매트릭스 라이브러리에 대한 권장 사항?
정말 나쁜 일입니까?
@ Wolffgang : 고밀도 매트릭스의 크기에 따라 행당 두 개의 추가 포인터는 무시할 수 있습니다. 분산 된 데이터와 관련하여 벡터가 연속 메모리에 있는지 확인하는 사용자 지정 할당자를 사용하는 것을 생각할 수 있습니다. 메모리가 재활용되지 않는 한 표준 할당 자조차도 포인터 크기 차이가 두 개인 연속 메모리를 사용할 수 있습니다.
@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
항목에 대해 아무것도 모릅니다. std::vector
를 더 많이 채운다면 std::vector
행렬과 행렬에 가변적 인 수의 행 (또는 열)이 없기 때문에 모두 같은 크기를 갖는 것이 전적으로 당신의 임무입니다. 따라서 외부 벡터의 모든 항목에 대해 올바른 생성자를 모두 호출해야하며 코드를 사용하는 다른 사람은 std::vector<T>::push_back()
내부 벡터 중 하나에서 사용하려는 유혹에 저항해야하므로 다음 코드가 모두 중단됩니다. 물론 클래스를 올바르게 작성하면 이것을 허용하지 않을 수 있지만 큰 연속 할당으로 간단히 적용하는 것이 훨씬 쉽습니다.
HPC 프로그래머는 단순히 낮은 수준의 데이터를 기대합니다. 행렬을 주면 행렬의 첫 번째 요소에 대한 포인터와 행렬의 마지막 요소에 대한 포인터를 잡으면이 두 요소 사이의 모든 포인터가 유효하고 동일한 요소를 가리킬 것으로 기대됩니다 매트릭스. 이것은 첫 번째 요점과 비슷하지만 라이브러리와 관련이 많지 않고 팀 구성원이나 코드를 공유하는 사람과 관련이있을 수 있기 때문에 다릅니다.
원하는 데이터 구조를 최하위 수준으로 떨어 뜨려 장기적으로 HPC의 수명을 연장 할 수 있습니다. 같은 도구를 사용 perf
하고하는 것은 vtune
당신이 코드의 성능을 향상시키기 위해 기존의 프로파일 링 결과와 결합하려고합니다 매우 낮은 수준의 성능 카운터 측정을 제공 할 것입니다. 데이터 구조가 멋진 컨테이너를 많이 사용하는 경우 컨테이너의 문제 또는 알고리즘 자체의 비효율로 인해 캐시 누락이 발생한다는 것을 이해하기 어렵습니다. 더 복잡한 코드 컨테이너가 필요하지만 행렬 대수의 경우 실제로 필요하지 않습니다 .s 1
std::vector
대신 데이터를 저장하는 것만 으로 얻을 수 n
std::vector
있으므로 그렇게하십시오.
또한 벤치 마크를 작성합니다. 작은 크기 (<100 * 100)의 행렬의 경우 성능은 vector <vector <double >> 및 wrap 1D vector와 비슷합니다. 큰 크기 (~ 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;
}