고성능 과학 컴퓨팅 코드를위한 행렬 클래스를 구성하기 위해 vector <vector <double >>을 사용하는 것이 좋은 생각입니까?


36

vector<vector<double>>고성능 과학 컴퓨팅 코드를위한 매트릭스 클래스를 형성 하기 위해 (std 를 사용 하여) 사용하는 것이 좋은 생각 입니까?

대답이 아니오 인 경우. 왜? 감사


2
물론 나쁜 생각입니다. 이러한 저장 형식으로 blas, lapack 또는 기타 기존 매트릭스 라이브러리를 사용할 수 없습니다. 또한 비 지역 및 간접 데이터에 의한 비 효율성을 소개합니다.
Thomas Klimpel

9
@ 토마스 그게 실제로 downvote를 보증합니까?
AKID

33
공감하지 마십시오. 잘못 인도 된 생각이더라도 합법적 인 질문입니다.
Wolfgang Bangerth

3
std :: vector는 분산 벡터가 아니므로 공유 메모리 시스템을 제외하고 병렬 컴퓨팅을 많이 수행 할 수 없으므로 Petsc 또는 Trilinos를 대신 사용하십시오. 또한 일반적으로 희소 행렬을 다루므로 전체 밀도 행렬을 저장하게됩니다. 희소 행렬로 재생하려면 std :: vector <std :: map>을 사용할 수 있지만 다시는 성능이 좋지 않습니다 (아래 @WolfgangBangerth 게시물 참조).
gnzlbg

3
사용하려고 표준 : MPI와 당신이 당신의 자기 촬영하고자하는 것입니다 >> 벡터 <표준 : : 벡터 <더블
pyCthon

답변:


42

벡터는 행렬에 행이있는만큼 공간에 많은 수의 객체를 할당해야하므로 나쁜 생각입니다. 할당은 비싸지 만, 주로 행렬의 데이터가 프로세서 캐시가 쉽게 액세스 할 수있는 한곳이 아닌 메모리 주위에 흩어져있는 여러 배열에 존재하기 때문에 나쁜 생각입니다.

또한 낭비적인 저장 형식입니다. std :: vector는 배열의 길이가 유연하기 때문에 하나는 배열의 시작 부분과 끝 부분에 두 개의 포인터를 저장합니다. 반면, 이것이 적절한 행렬이 되려면 모든 행의 길이가 같아야하므로 각 행의 길이를 독립적으로 저장하지 않고 열 수를 한 번만 저장하면 충분합니다.


std::vector할당 된 저장 영역의 시작, 끝 및 끝 (예 :을 호출 할 수 있도록)이라는 세 개의 포인터를 실제로 저장 하기 때문에 실제로 말한 것보다 더 나쁩니다 .capacity(). 용량이 크기와 다를 수 있으므로 상황이 훨씬 악화됩니다!
user14717

18

Wolfgang이 언급 한 이유 외에도을 사용하는 경우 vector<vector<double> >요소를 검색 할 때마다 요소를 두 번 역 참조해야하는데 이는 단일 역 참조 작업보다 계산 비용이 많이 듭니다. 일반적인 방법 중 하나는 단일 배열 (a vector<double>또는 a double *)을 대신 할당하는 것입니다. 또한 사람들이이 단일 배열을 좀 더 직관적 인 인덱싱 작업으로 감싸서 적절한 인덱스를 호출하는 데 필요한 "정신 오버 헤드"의 양을 줄임으로써 매트릭스 클래스에 구문 설탕을 추가하는 것을 보았습니다.



4

정말 나쁜 일입니까?

@ 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

당신은 "내 시스템에는 분명한 승자가있다"라고 씁니다-당신은 명확한 승자가 아님 을 의미 합니까?
akid

9
-1 hpc 코드의 성능을 이해하는 것은 쉽지 않습니다. 귀하의 경우, 매트릭스의 크기는 단순히 캐시 크기를 초과하므로 시스템의 메모리 대역폭 만 측정하면됩니다. N을 200으로 변경하고 반복 횟수를 1000으로 늘리면 "calc take : 65"와 "calc take : 36"이 표시됩니다. 더 현실적으로 만들기 위해 a = a * a를 a + = a1 * a2로 더 바꾸면 "calc take : 176"대 "calc take : 84"가 표시됩니다. 따라서 행렬 대신 벡터로 구성된 벡터를 사용하면 성능에서 요소 2를 잃을 수 있습니다. 실제 생활은 더 복잡해 지지만 여전히 나쁜 생각입니다.
토마스 클리 펠

그래하지만 MPI와 표준 : : 벡터를 사용하여 시도, C는 아래로 손을 승리
pyCthon

3

성능 문제로 인해 권장되지는 않지만 권장하지는 않습니다. 일반적으로 단일 포인터 역 참조 및 정수 산술을 사용하여 인덱싱되는 연속 된 데이터의 큰 청크로 할당되는 기존 매트릭스보다 성능이 약간 떨어집니다. 성능 저하의 원인은 대부분 캐싱 차이에 기인하지만, 일단 매트릭스 크기가 충분히 커지면이 효과가 상실되고 내부 벡터에 대해 특수 할당자를 사용하여 경계를 캐시하도록 정렬하면 캐싱 문제가 더 완화됩니다. .

내 생각으로는 그 자체로는 충분하지 않습니다. 나에게 이유는 코딩 두통이 많이 발생하기 때문입니다. 장기적으로 발생할 수있는 두통 목록은 다음과 같습니다.

HPC 라이브러리 사용

대부분의 HPC 라이브러리를 사용하려면 대부분의 HPC 라이브러리가이 명시 적 형식을 기대하기 때문에 벡터를 반복하고 모든 데이터를 연속 버퍼에 배치해야합니다. BLAS와 LAPACK이 떠 올랐지 만 유비쿼터스 HPC 라이브러리 MPI는 사용하기가 훨씬 어려울 것입니다.

코딩 오류 가능성

std::vector항목에 대해 아무것도 모릅니다. std::vector를 더 많이 채운다면 std::vector행렬과 행렬에 가변적 인 수의 행 (또는 열)이 없기 때문에 모두 같은 크기를 갖는 것이 전적으로 당신의 임무입니다. 따라서 외부 벡터의 모든 항목에 대해 올바른 생성자를 모두 호출해야하며 코드를 사용하는 다른 사람은 std::vector<T>::push_back()내부 벡터 중 하나에서 사용하려는 유혹에 저항해야하므로 다음 코드가 모두 중단됩니다. 물론 클래스를 올바르게 작성하면 이것을 허용하지 않을 수 있지만 큰 연속 할당으로 간단히 적용하는 것이 훨씬 쉽습니다.

HPC 문화와 기대

HPC 프로그래머는 단순히 낮은 수준의 데이터를 기대합니다. 행렬을 주면 행렬의 첫 번째 요소에 대한 포인터와 행렬의 마지막 요소에 대한 포인터를 잡으면이 두 요소 사이의 모든 포인터가 유효하고 동일한 요소를 가리킬 것으로 기대됩니다 매트릭스. 이것은 첫 번째 요점과 비슷하지만 라이브러리와 관련이 많지 않고 팀 구성원이나 코드를 공유하는 사람과 관련이있을 수 있기 때문에 다릅니다.

하위 수준 데이터의 성능에 대한 추론이 쉬움

원하는 데이터 구조를 최하위 수준으로 떨어 뜨려 장기적으로 HPC의 수명을 연장 할 수 있습니다. 같은 도구를 사용 perf하고하는 것은 vtune당신이 코드의 성능을 향상시키기 위해 기존의 프로파일 링 결과와 결합하려고합니다 매우 낮은 수준의 성능 카운터 측정을 제공 할 것입니다. 데이터 구조가 멋진 컨테이너를 많이 사용하는 경우 컨테이너의 문제 또는 알고리즘 자체의 비효율로 인해 캐시 누락이 발생한다는 것을 이해하기 어렵습니다. 더 복잡한 코드 컨테이너가 필요하지만 행렬 대수의 경우 실제로 필요하지 않습니다 .s 1 std::vector대신 데이터를 저장하는 것만 으로 얻을 수 n std::vector있으므로 그렇게하십시오.


0

또한 벤치 마크를 작성합니다. 작은 크기 (<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;
}

0

다른 사람들이 지적했듯이 수학으로 수학을 시도하거나 공연을하지 마십시오.

즉, 코드가 런타임에 그리고 데이터 저장을 시작한 후에 치수가 결정되는 2 차원 배열을 조립해야 할 때이 구조를 임시로 사용했습니다. 예를 들어, 시작할 때 저장해야하는 벡터 수를 정확하게 계산하는 것이 간단하지 않은 비싼 프로세스에서 벡터 출력을 수집합니다.

들어오는 모든 벡터 입력을 하나의 버퍼로 연결할 수 있지만를 사용하면 코드가 더 견고하고 읽기 쉽습니다 vector<vector<T>>.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.