มันเป็นความคิดที่ดีที่จะใช้vector<vector<double>>
(ใช้ std) เพื่อสร้างคลาสเมทริกซ์สำหรับรหัสการคำนวณทางวิทยาศาสตร์ที่มีประสิทธิภาพสูงหรือไม่?
ถ้าคำตอบคือไม่ ทำไม? ขอบคุณ
มันเป็นความคิดที่ดีที่จะใช้vector<vector<double>>
(ใช้ std) เพื่อสร้างคลาสเมทริกซ์สำหรับรหัสการคำนวณทางวิทยาศาสตร์ที่มีประสิทธิภาพสูงหรือไม่?
ถ้าคำตอบคือไม่ ทำไม? ขอบคุณ
คำตอบ:
เป็นความคิดที่ไม่ดีเพราะเวกเตอร์จำเป็นต้องจัดสรรวัตถุในอวกาศให้มากที่สุดเนื่องจากมีแถวในเมทริกซ์ของคุณ การจัดสรรมีราคาแพง แต่โดยหลักแล้วมันเป็นความคิดที่ไม่ดีเนื่องจากข้อมูลของเมทริกซ์ของคุณมีอยู่ในอาร์เรย์จำนวนหนึ่งที่กระจัดกระจายอยู่รอบ ๆ หน่วยความจำมากกว่าทั้งหมดในที่เดียวที่แคชโปรเซสเซอร์สามารถเข้าถึงได้ง่าย
นอกจากนี้ยังเป็นรูปแบบการจัดเก็บที่สิ้นเปลือง: std :: vector เก็บพอยน์เตอร์สองตัวตัวหนึ่งไว้ที่จุดเริ่มต้นของอาร์เรย์และอีกรูปแบบหนึ่งไปยังจุดสิ้นสุดเนื่องจากความยาวของอาร์เรย์นั้นมีความยืดหยุ่น ในทางกลับกันหากเป็นเมทริกซ์ที่เหมาะสมความยาวของแถวทั้งหมดจะต้องเท่ากันดังนั้นจึงเพียงพอที่จะเก็บจำนวนคอลัมน์ได้เพียงครั้งเดียวแทนที่จะปล่อยให้แต่ละแถวเก็บความยาวอย่างอิสระ
std::vector
จริง ๆ แล้วเก็บสามพอยน์เตอร์: จุดเริ่มต้นจุดสิ้นสุดและจุดสิ้นสุดของพื้นที่เก็บข้อมูลที่ปันส่วน (เช่นให้เราโทรหา.capacity()
) ความสามารถนั้นแตกต่างจากขนาดทำให้สถานการณ์แย่ลงมาก!
นอกเหนือจากเหตุผลที่ Wolfgang กล่าวถึงหากคุณใช้ a vector<vector<double> >
คุณจะต้องทำการอ้างอิงซ้ำสองครั้งทุกครั้งที่คุณต้องการดึงองค์ประกอบซึ่งมีค่าใช้จ่ายในการคำนวณมากกว่าการดำเนินการประชุมแบบเดี่ยว วิธีการทั่วไปอย่างหนึ่งคือการจัดสรรอาเรย์เดียว (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();
}
ในระบบของฉันตอนนี้มีผู้ชนะที่ชัดเจน (Compiler gcc 4.7 พร้อม -O3)
เวลา 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
เราเห็นด้วยว่าตราบใดที่ตัวจัดสรรมาตรฐานไม่ได้รีไซเคิลหน่วยความจำที่ว่างแล้วข้อมูลก็จะต่อเนื่องกัน (แน่นอนหลังจากการยกเลิกการจัดสรรบางส่วนไม่มีการรับประกันสำหรับสิ่งนี้)
เวลา arraymatrix พิมพ์:
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 นั้นเป็นที่สนใจ แต่ MPI ไลบรารี่ HPC ที่แพร่หลายจะใช้งานได้ยากกว่ามาก
std::vector
ไม่รู้อะไรเลยเกี่ยวกับผลงานของมัน ถ้าคุณเติม a ลงstd::vector
ไปมากกว่าstd::vector
นี้มันเป็นงานของคุณทั้งหมดเพื่อให้แน่ใจว่ามันมีขนาดเท่ากันเพราะจำไว้ว่าเราต้องการเมทริกซ์และเมทริกซ์ไม่มีจำนวนแถว (หรือคอลัมน์) จำนวนตัวแปร ดังนั้นคุณจะต้องเรียกตัวสร้างที่ถูกต้องทั้งหมดสำหรับทุก ๆ รายการของเวกเตอร์ด้านนอกของคุณและใครก็ตามที่ใช้รหัสของคุณจะต้องต่อต้านสิ่งล่อใจที่จะใช้std::vector<T>::push_back()
กับเวกเตอร์ด้านในใด ๆ ซึ่งจะทำให้โค้ดต่อไปนี้พัง แน่นอนว่าคุณสามารถไม่อนุญาตสิ่งนี้หากคุณเขียนชั้นเรียนของคุณอย่างถูกต้อง แต่การบังคับใช้เรื่องนี้ง่ายกว่ามากด้วยการจัดสรรที่ต่อเนื่องกันเป็นจำนวนมาก
โปรแกรมเมอร์ HPC คาดหวังข้อมูลระดับต่ำ หากคุณให้เมทริกซ์กับพวกเขามีความคาดหวังว่าถ้าพวกเขาจับตัวชี้ไปที่องค์ประกอบแรกของเมทริกซ์และตัวชี้ไปยังองค์ประกอบสุดท้ายของเมทริกซ์พอยน์เตอร์ทั้งหมดในระหว่างสองคนนี้จะถูกต้องและชี้ไปที่องค์ประกอบของเดียวกัน มดลูก สิ่งนี้คล้ายกับจุดแรกของฉัน แต่แตกต่างกันเพราะอาจไม่เกี่ยวข้องกับห้องสมุดมากนัก แต่ควรเป็นสมาชิกในทีมหรือใครก็ตามที่คุณแบ่งปันรหัสด้วย
การตกสู่ระดับต่ำสุดของโครงสร้างข้อมูลที่คุณต้องการทำให้ชีวิตของคุณง่ายขึ้นในระยะยาวสำหรับ HPC การใช้เครื่องมือที่ชอบperf
และvtune
จะให้การวัดที่มีประสิทธิภาพในระดับต่ำมากซึ่งคุณจะพยายามรวมกับผลลัพธ์การทำโปรไฟล์แบบดั้งเดิมเพื่อปรับปรุงประสิทธิภาพของโค้ดของคุณ ถ้าโครงสร้างข้อมูลของคุณใช้คอนเทนเนอร์แฟนซีจำนวนมากมันจะยากที่จะเข้าใจว่าแคชที่หายไปนั้นมาจากปัญหาของคอนเทนเนอร์หรือความไร้ประสิทธิภาพในอัลกอริทึมนั้นเอง สำหรับการบรรจุโค้ดที่ซับซ้อนยิ่งขึ้นนั้นเป็นสิ่งจำเป็น แต่สำหรับพีชคณิตเมทริกซ์มันไม่ได้เป็นอย่างนั้น - คุณสามารถเข้าถึงได้โดยใช้เพียง1
std::vector
เพื่อเก็บข้อมูลแทนที่จะเป็นn
std::vector
s ดังนั้นไปกับสิ่งนั้น
ฉันยังเขียนมาตรฐาน สำหรับเมทริกซ์ที่มีขนาดเล็ก (<100 * 100) ประสิทธิภาพจะคล้ายกับเวกเตอร์ <vector <double >> และเวกเตอร์ 1D ที่หุ้ม สำหรับเมทริกซ์ที่มีขนาดใหญ่ (~ 1,000 * 1,000) เวกเตอร์ที่พัน 1D จะดีกว่า เมทริกซ์ Eigen ทำงานแย่ลง ฉันประหลาดใจมากที่ Eigen นั้นเลวร้ายที่สุด
#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 มิติซึ่งจะมีการกำหนดขนาดที่รันไทม์และหลังจากที่คุณเริ่มจัดเก็บข้อมูล ตัวอย่างเช่นการรวบรวมผลลัพธ์เวกเตอร์จากกระบวนการที่มีราคาแพงซึ่งไม่ใช่เรื่องง่ายที่จะคำนวณจำนวนเวกเตอร์ที่คุณจะต้องเก็บเมื่อเริ่มต้น
คุณก็สามารถเชื่อมทั้งหมดของปัจจัยการผลิตเวกเตอร์ของคุณเป็นหนึ่งในบัฟเฟอร์ที่พวกเขามา vector<vector<T>>
แต่รหัสจะมีความทนทานมากขึ้นและอ่านได้มากขึ้นถ้าคุณใช้