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
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
Câu trả lời:
Đó 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ó.
std::vector
thự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!
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.
Không, sử dụng một trong những thư viện đại số tuyến tính có sẵn miễn phí. Một cuộc thảo luận về các thư viện khác nhau có thể được tìm thấy ở đây: Các khuyến nghị cho một thư viện ma trận C ++ có thể sử dụng nhanh?
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
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
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.
std::vector
không biết gì về các mục của nó. Nếu bạn điền std::vector
nhiều hơn std::vector
thì đó 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.
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.
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ư perf
và vtune
sẽ 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::vector
lưu trữ dữ liệu thay vì n
std::vector
s, vì vậy hãy đi với điều đó.
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;
}
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>>
.