उच्च प्रदर्शन वैज्ञानिक कंप्यूटिंग कोड के लिए मैट्रिक्स क्लास बनाने के लिए वेक्टर <वेक्टर <डबल >> का उपयोग करना एक अच्छा विचार है?


37

क्या vector<vector<double>>उच्च प्रदर्शन वैज्ञानिक कंप्यूटिंग कोड के लिए मैट्रिक्स क्लास बनाने के लिए (std का उपयोग करना) एक अच्छा विचार है ?

अगर जवाब नहीं है। क्यूं कर? धन्यवाद


2
-1 बेशक यह एक बुरा विचार है। आप इस तरह के भंडारण प्रारूप के साथ ब्लास, लैपैक या किसी अन्य मौजूदा मैट्रिक्स लाइब्रेरी का उपयोग करने में सक्षम नहीं होंगे। इसके अलावा, आप डेटा को गैर-स्थानीयता और अप्रत्यक्षता के साथ अक्षमता का परिचय देते हैं
थॉमस क्लिम्पेल

9
@ थोमस क्या वास्तव में एक अपमान का वारंट है?
अकीद

33
नीचा दिखाना नहीं है। यह एक गलत सवाल है भले ही यह एक गलत विचार है।
वोल्फगैंग बंगर्थ

3
std :: वेक्टर एक वितरित वेक्टर नहीं है, इसलिए आप इसके साथ ज्यादा समानांतर कंप्यूटिंग नहीं कर पाएंगे (साझा मेमोरी मशीनों को छोड़कर), इसके बजाय पेट्सक या ट्रिलीनो का उपयोग करें। इसके अलावा आम तौर पर विरल मैट्रिस के साथ एक सौदा होता है और आप पूर्ण सघन मैट्रीस का भंडारण करेंगे। विरल मैट्रिस के साथ खेलने के लिए आप एक std :: वेक्टर <std :: map> का उपयोग कर सकते हैं, लेकिन फिर, यह बहुत अच्छा प्रदर्शन नहीं करेगा, नीचे दिए गए @WolfgangBangerth पोस्ट देखें।
gnzlbg

3
std का उपयोग करने का प्रयास करें :: वेक्टर <std :: वेक्टर <डबल >> MPI के साथ और आप अपने स्व
pyCthon

जवाबों:


43

यह एक बुरा विचार है क्योंकि वेक्टर को अंतरिक्ष में कई वस्तुओं को आवंटित करने की आवश्यकता होती है क्योंकि आपके मैट्रिक्स में पंक्तियाँ होती हैं। आवंटन महंगा है, लेकिन मुख्य रूप से यह एक बुरा विचार है क्योंकि आपके मैट्रिक्स का डेटा अब मेमोरी के चारों ओर बिखरे हुए कई सरणियों में मौजूद है, बजाय सभी जगह एक जगह जहां प्रोसेसर कैश आसानी से इसे एक्सेस कर सकता है।

यह एक बेकार भंडारण प्रारूप भी है: एसटीडी :: वेक्टर दो बिंदुओं को संग्रहीत करता है, एक सरणी की शुरुआत में और एक अंत तक क्योंकि सरणी की लंबाई लचीली होती है। दूसरी ओर, इसके लिए एक उचित मैट्रिक्स होने के लिए, सभी पंक्तियों की लंबाई समान होनी चाहिए और इसलिए प्रत्येक पंक्ति को अपनी लंबाई को स्वतंत्र रूप से संग्रहीत करने की बजाय केवल एक बार स्तंभों की संख्या को संग्रहीत करना पर्याप्त होगा।


यह वास्तव में आपके कहने से भी बदतर है, क्योंकि std::vectorवास्तव में तीन बिंदुओं को संग्रहीत करता है: शुरुआत, अंत, और आवंटित भंडारण क्षेत्र का अंत (हमें कॉल करने की अनुमति देता है, उदाहरण के लिए .capacity())। वह क्षमता आकार से भिन्न हो सकती है जिससे स्थिति बहुत अधिक खराब हो जाती है!
user14717

18

वुल्फगैंग ने जिन कारणों का उल्लेख किया है, यदि आप एक का उपयोग करते हैं, तो इसके अलावा vector<vector<double> >, आपको हर बार एक तत्व को पुनः प्राप्त करना चाहते हैं, जो कि एक dereferencing ऑपरेशन की तुलना में अधिक कम्प्यूटेशनल रूप से महंगा है। एक विशिष्ट दृष्टिकोण इसके बजाय एक एकल सरणी ( vector<double>या ए double *) आवंटित करना है। मैंने यह भी देखा है कि लोग उचित सूचकांकों को लागू करने के लिए आवश्यक "मानसिक उपरि" की मात्रा को कम करने के लिए इस एकल सरणी के चारों ओर लपेटकर मैट्रिक्स कक्षाओं में सिंथैटिक शुगर जोड़ते हैं।


9

नहीं, मुफ्त उपलब्ध रैखिक बीजगणित पुस्तकालयों में से एक का उपयोग करें। विभिन्न पुस्तकालयों के बारे में एक चर्चा यहाँ मिल सकती है: एक प्रयोग करने योग्य, तेज़ C ++ मैट्रिक्स लाइब्रेरी के लिए सिफारिशें?


5

क्या वाकई इतनी बुरी बात है?

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

मेरे सिस्टम पर अब स्पष्ट विजेता (-ओ 3 के साथ कंपाइलर जीसीसी 4.7) है

समय 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

हम यह भी देखते हैं, कि जब तक मानक आवंटन मुक्त मेमोरी को रीसायकल नहीं करता है, तब तक डेटा सन्निहित है। (निश्चित रूप से कुछ सौदे के बाद इसके लिए कोई गारंटी नहीं है।)

समय सरणी मैट्रिक्स:

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

आप लिखते हैं "मेरे सिस्टम पर अब स्पष्ट विजेता है" - क्या आपका मतलब स्पष्ट विजेता नहीं था ?
अकीद

9
-1 hpc कोड के प्रदर्शन को समझना nontrivial हो सकता है। आपके मामले में, मैट्रिक्स का आकार केवल कैश आकार से अधिक है, जिससे आप अपने सिस्टम की मेमोरी बैंडविड्थ को माप रहे हैं। अगर मैं N को 200 में बदलता हूं और पुनरावृत्तियों की संख्या को 1000 तक बढ़ाता हूं, तो मुझे "calc लिया: 65" बनाम "calc लिया: 36" मिलता है। यदि मैं इसे और अधिक यथार्थवादी बनाने के लिए a = a * a + = a1 * a2 से प्रतिस्थापित करता हूं, तो मुझे "कैल्क लिया: 176" बनाम "कैल्क लिया: 84" मिलता है। तो ऐसा लगता है कि आप मैट्रिक्स के बजाय वेक्टर के वेक्टर का उपयोग करके प्रदर्शन में एक कारक दो को ढीला कर सकते हैं। वास्तविक जीवन अधिक जटिल होगा, लेकिन यह अभी भी एक बुरा विचार है।
थॉमस क्लिंपेल

हाँ, लेकिन std का उपयोग करने की कोशिश करें :: MPI के साथ वैक्टर, C ने हाथ नीचे किए
pyCthon

4

मैं इसकी अनुशंसा नहीं करता, लेकिन प्रदर्शन के मुद्दों के कारण नहीं। यह एक पारंपरिक मैट्रिक्स की तुलना में थोड़ा कम प्रदर्शन करने वाला होगा, जो आमतौर पर एक संकेतक सूचक और पूर्णांक अंकगणित का उपयोग करके अनुक्रमित डेटा के एक बड़े भाग के रूप में आवंटित किया जाता है। प्रदर्शन के हिट होने का कारण ज्यादातर कैशिंग मतभेद हैं, लेकिन एक बार जब आपके मैट्रिक्स का आकार काफी बड़ा हो जाता है तो यह प्रभाव बढ़ जाएगा और यदि आप आंतरिक वैक्टर के लिए एक विशेष आवंटन का उपयोग करते हैं ताकि वे कैश सीमाओं से जुड़ जाएं तो यह कैशिंग मुद्दे को कम कर देता है ।

मेरे विचार से ऐसा नहीं करना ही कारण है। मेरे लिए इसका कारण यह है कि यह बहुत सारे कोडिंग सिरदर्द पैदा करता है। यहां सिरदर्द की एक सूची दी गई है, जिससे यह दीर्घकालिक होगा

एचपीसी पुस्तकालयों का उपयोग

यदि आप अधिकांश एचपीसी पुस्तकालयों का उपयोग करना चाहते हैं, तो आपको अपने वेक्टर पर पुनरावृत्ति करने और अपने सभी डेटा को एक सन्निहित बफर में रखने की आवश्यकता होगी, क्योंकि अधिकांश एचपीसी पुस्तकालय इस स्पष्ट प्रारूप की अपेक्षा करते हैं। BLAS और LAPACK का ख्याल आता है, लेकिन सर्वव्यापी HPC लाइब्रेरी MPI का उपयोग करना बहुत कठिन होगा।

कोडिंग त्रुटि के लिए अधिक क्षमता

std::vectorइसकी प्रविष्टियों के बारे में कुछ भी नहीं पता है। यदि आप std::vectorअधिक std::vectorएस के साथ भरते हैं, तो यह पूरी तरह से आपका काम है कि यह सुनिश्चित करें कि उन सभी का आकार समान है, क्योंकि याद रखें कि हम एक मैट्रिक्स चाहते हैं और मैट्रिक्स में पंक्तियों की संख्या (या कॉलम) नहीं है। इस प्रकार आपको अपने बाहरी वेक्टर की हर प्रविष्टि के लिए सभी सही कंस्ट्रक्टरों को कॉल करना होगा, और आपके कोड का उपयोग std::vector<T>::push_back()करने वाले किसी भी व्यक्ति को किसी भी आंतरिक वैक्टर पर उपयोग करने के लिए प्रलोभन का विरोध करना होगा, जिससे निम्नलिखित सभी कोड टूट जाएंगे। यदि आप अपनी कक्षा को सही ढंग से लिखते हैं, तो बेशक आप इसे अस्वीकार कर सकते हैं, लेकिन इसे बड़े सन्निहित आवंटन के साथ लागू करना बहुत आसान है।

एचपीसी संस्कृति और अपेक्षाएं

एचपीसी प्रोग्रामर केवल निम्न स्तर के डेटा की उम्मीद करते हैं। यदि आप उन्हें एक मैट्रिक्स देते हैं, तो एक उम्मीद होती है कि यदि वे मैट्रिक्स के पहले तत्व के लिए पॉइंटर को पकड़ते हैं और मैट्रिक्स के अंतिम तत्व को एक पॉइंटर करते हैं, तो इन दोनों के बीच के सभी संकेत मान्य हैं और उसी के तत्वों को इंगित करते हैं मैट्रिक्स। यह मेरे पहले बिंदु के समान है, लेकिन अलग है क्योंकि यह पुस्तकालयों से संबंधित नहीं हो सकता है, बल्कि टीम के सदस्यों या किसी के साथ आप अपना कोड साझा करते हैं।

निचले स्तर के डेटा के प्रदर्शन के बारे में तर्क करना आसान है

आपके वांछित डेटा संरचना के निम्नतम स्तर के प्रतिनिधित्व को छोड़ने से एचपीसी के लिए लंबे समय में आपका जीवन आसान हो जाता है। जैसे टूल का उपयोग करना perfऔर vtuneआपको बहुत ही निम्न स्तर का प्रदर्शन काउंटर माप देगा जो आप अपने कोड के प्रदर्शन को बेहतर बनाने के लिए पारंपरिक प्रोफाइलिंग परिणामों के साथ संयोजन करने का प्रयास करेंगे। यदि आपकी डेटा संरचना बहुत सारे फैंसी कंटेनरों का उपयोग करती है, तो यह समझना मुश्किल होगा कि कैश की कमी कंटेनर या एल्गोरिथ्म में अक्षमता के साथ एक समस्या से आ रही है। अधिक जटिल कोड कंटेनरों के लिए आवश्यक हैं, लेकिन मैट्रिक्स बीजगणित के लिए वे वास्तव में नहीं हैं - आप केवल 1 std::vectorडेटा को स्टोर करने के बजाय n std::vectorएस के साथ मिल सकते हैं , इसलिए उसके साथ जाएं।


1

मैं एक बेंचमार्क भी लिखता हूं। छोटे आकार के मैट्रिक्स के लिए (<100 * 100), प्रदर्शन वेक्टर <वेक्टर <डबल >> और लिपटे 1 डी वेक्टर के लिए समान है। बड़े आकार के मैट्रिक्स के लिए (~ 1000 * 1000), लिपटे 1D वेक्टर बेहतर है। 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>>

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.