Có phải là một ý tưởng tốt để sử dụng vectơ <vector <double >> để tạo thành một lớp ma trận cho mã máy tính khoa học hiệu suất cao?


37

Có phải là một ý tưởng tốt để sử dụng vector<vector<double>>(sử dụng tiêu chuẩn) để tạo thành một lớp ma trận cho mã máy tính khoa học hiệu suất cao?

Nếu câu trả lời là không. Tại sao? Cảm ơn


2
-1 Tất nhiên đó là một ý tưởng tồi. Bạn sẽ không thể sử dụng blas, lapack hoặc bất kỳ thư viện ma trận hiện có nào khác với định dạng lưu trữ như vậy. Ngoài ra, bạn giới thiệu sự thiếu hiệu quả của dữ liệu không cục bộ và thiếu quyết đoán
Thomas Klimpel

9
@Thomas Điều đó có thực sự đảm bảo một downvote?
akid

33
Đừng downvote. Đó là một câu hỏi chính đáng ngay cả khi đó là một ý tưởng sai lầm.
Wolfgang Bangerth

3
std :: vector không phải là một vectơ phân tán, do đó bạn sẽ không thể thực hiện nhiều tính toán song song với nó (ngoại trừ các máy nhớ dùng chung), thay vào đó hãy sử dụng Thú cưng hoặc Trilinos. Hơn nữa, người ta thường xử lý các ma trận thưa thớt và bạn sẽ lưu trữ các Ma trận dày đặc. Để chơi với ma trận thưa thớt, bạn có thể sử dụng std :: vector <std :: map> nhưng một lần nữa, điều này sẽ không hoạt động tốt, xem bài đăng @WolfgangBangerth bên dưới.
gnzlbg

3
hãy thử sử dụng std :: vector <std :: vector <double >> với MPI và bạn sẽ muốn tự bắn mình
pyCthon

Câu trả lời:


43

Đó là một ý tưởng tồi vì vectơ cần phân bổ càng nhiều đối tượng trong không gian cũng như có các hàng trong ma trận của bạn. Phân bổ là tốn kém, nhưng chủ yếu là một ý tưởng tồi vì dữ liệu ma trận của bạn hiện tồn tại trong một số mảng nằm rải rác xung quanh bộ nhớ, thay vì tất cả ở một nơi mà bộ đệm của bộ xử lý có thể dễ dàng truy cập.

Đây cũng là một định dạng lưu trữ lãng phí: std :: vector lưu trữ hai con trỏ, một đến đầu mảng và một đến cuối vì độ dài của mảng là linh hoạt. Mặt khác, để đây là một ma trận thích hợp, độ dài của tất cả các hàng phải giống nhau và do đó chỉ đủ để lưu trữ số lượng cột một lần, thay vì để mỗi hàng lưu trữ độc lập độ dài của nó.


Nó thực sự tồi tệ hơn bạn nói, bởi vì std::vectorthực sự lưu trữ ba con trỏ: Phần đầu, phần cuối và phần cuối của vùng lưu trữ được phân bổ (ví dụ, cho phép chúng tôi gọi .capacity()). Năng lực đó có thể khác với kích thước làm cho tình hình tồi tệ hơn nhiều!
user14717

18

Ngoài các lý do Wolfgang đã đề cập, nếu bạn sử dụng a vector<vector<double> >, bạn sẽ phải hủy đăng ký hai lần mỗi lần bạn muốn truy xuất một phần tử, chi phí tính toán cao hơn so với thao tác hội nghị đơn lẻ. Một cách tiếp cận điển hình là phân bổ một mảng duy nhất (a vector<double>hoặc a double *) thay thế. Tôi cũng đã thấy mọi người thêm đường cú pháp vào các lớp ma trận bằng cách bao quanh mảng đơn này một số thao tác lập chỉ mục trực quan hơn, để giảm lượng "chi phí tinh thần" cần thiết để gọi các chỉ số thích hợp.



5

Có thực sự là một điều xấu như vậy?

@Wolfgang: Tùy thuộc vào kích thước của ma trận dày đặc, hai con trỏ bổ sung trên mỗi hàng có thể không đáng kể. Liên quan đến dữ liệu phân tán, người ta có thể nghĩ đến việc sử dụng bộ cấp phát tùy chỉnh để đảm bảo rằng các vectơ nằm trong bộ nhớ liền kề. Miễn là bộ nhớ không được tái chế, ngay cả bộ cấp phát tiêu chuẩn sẽ cho chúng ta bộ nhớ liền kề với khoảng cách hai con trỏ.

@Geoff: Nếu bạn đang thực hiện truy cập ngẫu nhiên và chỉ sử dụng một mảng, bạn vẫn phải tính toán chỉ số. Có thể không được nhanh hơn.

Vì vậy, hãy để chúng tôi làm một bài kiểm tra nhỏ:

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();
}

Và bây giờ sử dụng một mảng:

mảng

    #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();
}

Trên hệ thống của tôi hiện đã có người chiến thắng rõ ràng (Trình biên dịch gcc 4.7 với -O3)

thời gian in vectormatrix:

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

Chúng ta cũng thấy rằng, miễn là bộ cấp phát tiêu chuẩn không tái chế bộ nhớ đã giải phóng, dữ liệu sẽ liền kề nhau. (Tất nhiên sau một số thỏa thuận, không có gì đảm bảo cho việc này.)

bản in mảng thời gian:

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

Bạn viết "Trên hệ thống của tôi bây giờ đã có người chiến thắng rõ ràng" - ý bạn là không có người chiến thắng rõ ràng?
akid

9
-1 Hiểu về hiệu suất của mã hpc có thể không cần thiết. Trong trường hợp của bạn, kích thước của ma trận chỉ đơn giản là vượt quá kích thước bộ đệm, do đó bạn chỉ đang đo băng thông bộ nhớ của hệ thống. Nếu tôi thay đổi N thành 200 và tăng số lần lặp lên 1000, tôi nhận được "calc mất: 65" so với "calc mất: 36". Nếu tôi tiếp tục thay thế a = a * a bằng a + = a1 * a2 để làm cho nó thực tế hơn, tôi nhận được "calc lấy: 176" so với "calc mất: 84". Vì vậy, có vẻ như bạn có thể mất một yếu tố hai trong hiệu suất bằng cách sử dụng một vectơ vectơ thay vì ma trận. Cuộc sống thực sẽ phức tạp hơn, nhưng đó vẫn là một ý tưởng tồi.
Thomas Klimpel

vâng, nhưng hãy thử sử dụng std :: vectơ với MPI, C thắng tay
pyCthon

4

Tôi không khuyên bạn, nhưng không phải vì vấn đề hiệu suất. Nó sẽ ít hiệu suất hơn một ma trận truyền thống, thường được phân bổ dưới dạng một khối lớn dữ liệu liền kề được lập chỉ mục bằng cách sử dụng một con trỏ duy nhất và số học số nguyên. Lý do cho hiệu năng đạt được chủ yếu là sự khác biệt về bộ nhớ đệm, nhưng một khi kích thước ma trận của bạn đủ lớn, hiệu ứng này sẽ được khấu hao và nếu bạn sử dụng một cấp phát đặc biệt cho các vectơ bên trong để chúng được căn chỉnh theo ranh giới bộ đệm thì điều này sẽ giảm nhẹ vấn đề bộ đệm .

Điều đó tự nó không đủ lý do để không làm điều đó, theo ý kiến ​​của tôi. Lý do cho tôi là nó tạo ra rất nhiều đau đầu về mã hóa. Dưới đây là danh sách đau đầu này sẽ gây ra lâu dài

Sử dụng thư viện HPC

Nếu bạn muốn sử dụng hầu hết các thư viện HPC, bạn sẽ cần lặp lại trên vectơ của mình và đặt tất cả dữ liệu của chúng vào một bộ đệm liền kề, bởi vì hầu hết các thư viện HPC đều mong muốn định dạng rõ ràng này. BLAS và LAPACK xuất hiện trong tâm trí, nhưng MPI thư viện HPC phổ biến sẽ khó sử dụng hơn nhiều.

Nhiều khả năng xảy ra lỗi mã hóa

std::vectorkhông biết gì về các mục của nó. Nếu bạn điền std::vectornhiều hơn std::vectorthì đó hoàn toàn là công việc của bạn để đảm bảo rằng tất cả chúng đều có cùng kích thước, bởi vì hãy nhớ rằng chúng ta muốn một ma trận và ma trận không có số lượng hàng (hoặc cột) khác nhau. Do đó, bạn sẽ phải gọi tất cả các hàm tạo chính xác cho mọi mục nhập của vectơ ngoài của bạn và bất kỳ ai khác sử dụng mã của bạn phải chống lại sự cám dỗ để sử dụng std::vector<T>::push_back()trên bất kỳ vectơ bên trong nào, điều này sẽ khiến tất cả các mã sau bị phá vỡ. Tất nhiên bạn có thể không cho phép điều này nếu bạn viết chính xác lớp của mình, nhưng việc thực thi điều này đơn giản hơn rất nhiều với sự phân bổ liền kề lớn.

Văn hóa và kỳ vọng của HPC

Lập trình viên HPC chỉ đơn giản là mong đợi dữ liệu cấp thấp. Nếu bạn cung cấp cho họ một ma trận, có một kỳ vọng rằng nếu họ chộp lấy con trỏ đến phần tử đầu tiên của ma trận và một con trỏ đến phần tử cuối cùng của ma trận, thì tất cả các con trỏ ở giữa hai phần tử này đều hợp lệ và trỏ đến các phần tử giống nhau ma trận. Điều này tương tự với điểm đầu tiên của tôi, nhưng khác bởi vì nó có thể không liên quan nhiều đến các thư viện mà là các thành viên trong nhóm hoặc bất kỳ ai bạn chia sẻ mã của mình.

Dễ dàng hơn để lý do về hiệu suất của dữ liệu cấp thấp hơn

Giảm xuống mức đại diện thấp nhất của cấu trúc dữ liệu mong muốn của bạn giúp cuộc sống của bạn dễ dàng hơn trong thời gian dài đối với HPC. Sử dụng các công cụ như perfvtunesẽ cung cấp cho bạn các phép đo bộ đếm hiệu suất rất thấp mà bạn sẽ cố gắng kết hợp với các kết quả định hình truyền thống để cải thiện hiệu suất mã của mình. Nếu cấu trúc dữ liệu của bạn sử dụng nhiều bộ chứa ưa thích, sẽ khó hiểu rằng các lỗi bộ nhớ cache xuất phát từ một vấn đề với bộ chứa hoặc sự không hiệu quả trong chính thuật toán. Đối với các bộ chứa mã phức tạp hơn là cần thiết, nhưng đối với đại số ma trận chúng thực sự không có - bạn có thể nhận được chỉ bằng cách 1 std::vectorlưu trữ dữ liệu thay vì n std::vectors, vì vậy hãy đi với điều đó.


1

Tôi cũng viết một điểm chuẩn. Đối với ma trận có kích thước nhỏ (<100 * 100), hiệu suất tương tự đối với vectơ <vector <double >> và vectơ 1D. Đối với ma trận có kích thước lớn (~ 1000 * 1000), bọc vector 1D sẽ tốt hơn. Ma trận Eigen hành xử tồi tệ hơn. Điều ngạc nhiên với tôi là Eigen là tồi tệ nhất.

#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

Như những người khác đã chỉ ra, đừng cố gắng làm toán với nó hoặc làm bất cứ điều gì hiệu quả.

Điều đó nói rằng, tôi đã sử dụng cấu trúc này tạm thời khi mã cần lắp ráp một mảng 2 chiều có kích thước sẽ được xác định khi chạy và sau khi bạn bắt đầu lưu trữ dữ liệu. Ví dụ: thu thập kết quả đầu ra của vectơ từ một số quy trình đắt tiền, nơi không đơn giản để tính toán chính xác có bao nhiêu vectơ bạn sẽ cần lưu trữ khi khởi động.

Bạn chỉ có thể nối tất cả các đầu vào vector của mình vào một bộ đệm khi chúng vào, nhưng mã sẽ bền hơn và dễ đọc hơn nếu bạn sử dụng a vector<vector<T>>.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.