เป็นความคิดที่ดีหรือไม่ที่จะใช้ vector <vector <double>> เพื่อสร้างคลาสเมทริกซ์สำหรับรหัสการคำนวณทางวิทยาศาสตร์ที่มีประสิทธิภาพสูง?


36

มันเป็นความคิดที่ดีที่จะใช้vector<vector<double>>(ใช้ std) เพื่อสร้างคลาสเมทริกซ์สำหรับรหัสการคำนวณทางวิทยาศาสตร์ที่มีประสิทธิภาพสูงหรือไม่?

ถ้าคำตอบคือไม่ ทำไม? ขอบคุณ


2
-1 แน่นอนมันเป็นความคิดที่ไม่ดี คุณจะไม่สามารถใช้ blas, lapack หรือเมทริกซ์ไลบรารีอื่นที่มีอยู่ด้วยรูปแบบการจัดเก็บ นอกจากนี้คุณแนะนำความไร้ประสิทธิภาพโดยข้อมูลที่ไม่ใช่ท้องถิ่นและทางอ้อม
Thomas Klimpel

9
@ โทมัสจริงๆรับประกัน downvote หรือไม่?
Akid

33
อย่าลงคะแนน มันเป็นคำถามที่ถูกกฎหมายแม้ว่าจะเป็นความคิดที่เข้าใจผิด
Wolfgang Bangerth

3
std :: vector ไม่ใช่เวกเตอร์แบบกระจายดังนั้นคุณจะไม่สามารถทำการคำนวณแบบขนานได้ (ยกเว้นเครื่องหน่วยความจำที่ใช้ร่วมกัน) ให้ใช้ Petsc หรือ Trilinos แทน นอกจากนี้มักจะเกี่ยวข้องกับเมทริกซ์กระจัดกระจายและคุณจะเก็บเมทริกซ์หนาแน่นเต็ม สำหรับการเล่นกับเมทริกซ์กระจัดกระจายคุณสามารถใช้ std :: vector <std :: map> แต่อีกครั้งสิ่งนี้อาจทำงานได้ไม่ดีนักให้ดูที่ @ WolfgangBangerth โพสต์ด้านล่าง
gnzlbg

3
ลองใช้ std :: vector <std :: vector <double>> กับ MPI และคุณจะต้องการยิงตัวคุณเอง
pyCthon

คำตอบ:


42

เป็นความคิดที่ไม่ดีเพราะเวกเตอร์จำเป็นต้องจัดสรรวัตถุในอวกาศให้มากที่สุดเนื่องจากมีแถวในเมทริกซ์ของคุณ การจัดสรรมีราคาแพง แต่โดยหลักแล้วมันเป็นความคิดที่ไม่ดีเนื่องจากข้อมูลของเมทริกซ์ของคุณมีอยู่ในอาร์เรย์จำนวนหนึ่งที่กระจัดกระจายอยู่รอบ ๆ หน่วยความจำมากกว่าทั้งหมดในที่เดียวที่แคชโปรเซสเซอร์สามารถเข้าถึงได้ง่าย

นอกจากนี้ยังเป็นรูปแบบการจัดเก็บที่สิ้นเปลือง: std :: vector เก็บพอยน์เตอร์สองตัวตัวหนึ่งไว้ที่จุดเริ่มต้นของอาร์เรย์และอีกรูปแบบหนึ่งไปยังจุดสิ้นสุดเนื่องจากความยาวของอาร์เรย์นั้นมีความยืดหยุ่น ในทางกลับกันหากเป็นเมทริกซ์ที่เหมาะสมความยาวของแถวทั้งหมดจะต้องเท่ากันดังนั้นจึงเพียงพอที่จะเก็บจำนวนคอลัมน์ได้เพียงครั้งเดียวแทนที่จะปล่อยให้แต่ละแถวเก็บความยาวอย่างอิสระ


มันเลวร้ายยิ่งกว่าที่คุณพูดเพราะstd::vectorจริง ๆ แล้วเก็บสามพอยน์เตอร์: จุดเริ่มต้นจุดสิ้นสุดและจุดสิ้นสุดของพื้นที่เก็บข้อมูลที่ปันส่วน (เช่นให้เราโทรหา.capacity()) ความสามารถนั้นแตกต่างจากขนาดทำให้สถานการณ์แย่ลงมาก!
user14717

18

นอกเหนือจากเหตุผลที่ Wolfgang กล่าวถึงหากคุณใช้ a vector<vector<double> >คุณจะต้องทำการอ้างอิงซ้ำสองครั้งทุกครั้งที่คุณต้องการดึงองค์ประกอบซึ่งมีค่าใช้จ่ายในการคำนวณมากกว่าการดำเนินการประชุมแบบเดี่ยว วิธีการทั่วไปอย่างหนึ่งคือการจัดสรรอาเรย์เดียว (a vector<double>หรือ a double *) แทน ฉันเคยเห็นคนเพิ่มน้ำตาลวากยสัมพันธ์ลงในคลาสเมทริกซ์โดยล้อมรอบอาเรย์เดี่ยวนี้เพื่อให้การทำดัชนีง่ายขึ้นเพื่อลดจำนวน "ค่าใช้จ่ายทางจิต" ที่จำเป็นในการเรียกใช้ดัชนีที่เหมาะสม


9

ไม่ใช้ไลบรารีพีชคณิตเชิงเส้นที่มีอยู่ฟรีหนึ่งแห่ง การอภิปรายเกี่ยวกับห้องสมุดที่แตกต่างกันสามารถพบได้ที่นี่: คำแนะนำสำหรับห้องสมุดเมทริกซ์ C ++ ที่ใช้งานได้อย่างรวดเร็ว?


4

มันเป็นสิ่งที่เลวร้ายจริงๆเหรอ?

@ 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

คุณเขียน "ในระบบของฉันตอนนี้มีผู้ชนะชัดเจน" - คุณหมายถึงไม่มีผู้ชนะที่ชัดเจน?
Akid

9
-1 การทำความเข้าใจเกี่ยวกับประสิทธิภาพของรหัส hpc นั้นอาจไม่ใช่เรื่องง่าย ในกรณีของคุณขนาดของเมทริกซ์จะเกินขนาดแคชเพื่อให้คุณเพียงแค่วัดแบนด์วิดธ์หน่วยความจำของระบบของคุณ ถ้าฉันเปลี่ยน N เป็น 200 และเพิ่มจำนวนการทำซ้ำเป็น 1,000 ฉันจะได้รับ "Calc ใช้: 65" เทียบกับ "Calc ใช้: 36" หากฉันแทนที่ a = a * a ด้วย a + = a1 * a2 ต่อไปเพื่อให้เป็นจริงยิ่งขึ้นฉันจะได้รับ "calc เอา: 176" เทียบกับ "calc take: 84" ดังนั้นดูเหมือนว่าคุณจะหลวมประสิทธิภาพสองส่วนด้วยการใช้เวกเตอร์ของเวกเตอร์แทนเมทริกซ์ ชีวิตจริงจะมีความซับซ้อนมากขึ้น แต่ก็ยังเป็นความคิดที่ไม่ดี
Thomas Klimpel

ใช่ แต่ลองใช้ std :: เวกเตอร์กับ MPI, C ชนะมือลง
pyCthon

3

ฉันไม่แนะนำ แต่ไม่ใช่เพราะปัญหาเรื่องประสิทธิภาพ มันจะมีประสิทธิภาพน้อยกว่าเมทริกซ์แบบดั้งเดิมเล็กน้อยซึ่งโดยปกติจะถูกจัดสรรเป็นกลุ่มข้อมูลขนาดใหญ่ที่ต่อเนื่องกันซึ่งจัดทำดัชนีโดยใช้ตัวชี้เดี่ยวและการคำนวณเลขจำนวนเต็ม เหตุผลที่ทำให้ประสิทธิภาพการทำงานเกิดจากความแตกต่างของแคชเป็นส่วนใหญ่ แต่เมื่อขนาดเมทริกซ์ของคุณใหญ่พอที่เอฟเฟกต์นี้จะถูกตัดจำหน่ายและถ้าคุณใช้ตัวจัดสรรพิเศษสำหรับเวกเตอร์ภายในเพื่อให้สอดคล้องกับแคช .

โดยตัวของมันเองนั้นไม่มีเหตุผลเพียงพอที่จะทำในความคิดของฉัน เหตุผลสำหรับฉันคือมันสร้างความปวดหัวในการเขียนโค้ดจำนวนมาก นี่คือรายการของอาการปวดหัวนี้จะทำให้ระยะยาว

การใช้ไลบรารี HPC

หากคุณต้องการใช้ไลบรารี HPC ส่วนใหญ่คุณจะต้องทำซ้ำเวกเตอร์ของคุณและวางข้อมูลทั้งหมดไว้ในบัฟเฟอร์ที่ต่อเนื่องกันเพราะไลบรารี HPC ส่วนใหญ่คาดหวังรูปแบบที่ชัดเจนนี้ BLAS และ LAPACK นั้นเป็นที่สนใจ แต่ MPI ไลบรารี่ HPC ที่แพร่หลายจะใช้งานได้ยากกว่ามาก

มีโอกาสมากขึ้นสำหรับข้อผิดพลาดในการเข้ารหัส

std::vectorไม่รู้อะไรเลยเกี่ยวกับผลงานของมัน ถ้าคุณเติม a ลงstd::vectorไปมากกว่าstd::vectorนี้มันเป็นงานของคุณทั้งหมดเพื่อให้แน่ใจว่ามันมีขนาดเท่ากันเพราะจำไว้ว่าเราต้องการเมทริกซ์และเมทริกซ์ไม่มีจำนวนแถว (หรือคอลัมน์) จำนวนตัวแปร ดังนั้นคุณจะต้องเรียกตัวสร้างที่ถูกต้องทั้งหมดสำหรับทุก ๆ รายการของเวกเตอร์ด้านนอกของคุณและใครก็ตามที่ใช้รหัสของคุณจะต้องต่อต้านสิ่งล่อใจที่จะใช้std::vector<T>::push_back()กับเวกเตอร์ด้านในใด ๆ ซึ่งจะทำให้โค้ดต่อไปนี้พัง แน่นอนว่าคุณสามารถไม่อนุญาตสิ่งนี้หากคุณเขียนชั้นเรียนของคุณอย่างถูกต้อง แต่การบังคับใช้เรื่องนี้ง่ายกว่ามากด้วยการจัดสรรที่ต่อเนื่องกันเป็นจำนวนมาก

วัฒนธรรมและความคาดหวังของ HPC

โปรแกรมเมอร์ HPC คาดหวังข้อมูลระดับต่ำ หากคุณให้เมทริกซ์กับพวกเขามีความคาดหวังว่าถ้าพวกเขาจับตัวชี้ไปที่องค์ประกอบแรกของเมทริกซ์และตัวชี้ไปยังองค์ประกอบสุดท้ายของเมทริกซ์พอยน์เตอร์ทั้งหมดในระหว่างสองคนนี้จะถูกต้องและชี้ไปที่องค์ประกอบของเดียวกัน มดลูก สิ่งนี้คล้ายกับจุดแรกของฉัน แต่แตกต่างกันเพราะอาจไม่เกี่ยวข้องกับห้องสมุดมากนัก แต่ควรเป็นสมาชิกในทีมหรือใครก็ตามที่คุณแบ่งปันรหัสด้วย

ง่ายกว่าที่จะให้เหตุผลเกี่ยวกับประสิทธิภาพของข้อมูลระดับล่าง

การตกสู่ระดับต่ำสุดของโครงสร้างข้อมูลที่คุณต้องการทำให้ชีวิตของคุณง่ายขึ้นในระยะยาวสำหรับ HPC การใช้เครื่องมือที่ชอบperfและvtuneจะให้การวัดที่มีประสิทธิภาพในระดับต่ำมากซึ่งคุณจะพยายามรวมกับผลลัพธ์การทำโปรไฟล์แบบดั้งเดิมเพื่อปรับปรุงประสิทธิภาพของโค้ดของคุณ ถ้าโครงสร้างข้อมูลของคุณใช้คอนเทนเนอร์แฟนซีจำนวนมากมันจะยากที่จะเข้าใจว่าแคชที่หายไปนั้นมาจากปัญหาของคอนเทนเนอร์หรือความไร้ประสิทธิภาพในอัลกอริทึมนั้นเอง สำหรับการบรรจุโค้ดที่ซับซ้อนยิ่งขึ้นนั้นเป็นสิ่งจำเป็น แต่สำหรับพีชคณิตเมทริกซ์มันไม่ได้เป็นอย่างนั้น - คุณสามารถเข้าถึงได้โดยใช้เพียง1 std::vectorเพื่อเก็บข้อมูลแทนที่จะเป็นn std::vectors ดังนั้นไปกับสิ่งนั้น


0

ฉันยังเขียนมาตรฐาน สำหรับเมทริกซ์ที่มีขนาดเล็ก (<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;
}

0

อย่างที่คนอื่น ๆ ชี้ไปอย่าพยายามทำคณิตศาสตร์ด้วยมันหรือทำสิ่งใด ๆ

ที่กล่าวว่าฉันได้ใช้โครงสร้างนี้เป็นชั่วคราวเมื่อรหัสต้องการประกอบอาร์เรย์ 2 มิติซึ่งจะมีการกำหนดขนาดที่รันไทม์และหลังจากที่คุณเริ่มจัดเก็บข้อมูล ตัวอย่างเช่นการรวบรวมผลลัพธ์เวกเตอร์จากกระบวนการที่มีราคาแพงซึ่งไม่ใช่เรื่องง่ายที่จะคำนวณจำนวนเวกเตอร์ที่คุณจะต้องเก็บเมื่อเริ่มต้น

คุณก็สามารถเชื่อมทั้งหมดของปัจจัยการผลิตเวกเตอร์ของคุณเป็นหนึ่งในบัฟเฟอร์ที่พวกเขามา vector<vector<T>>แต่รหัสจะมีความทนทานมากขึ้นและอ่านได้มากขึ้นถ้าคุณใช้

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.