2016-06-06 7 views
35

जो मैंने ईजिन (here) के बारे में पढ़ा है, ऐसा लगता है कि operator=() आलसी मूल्यांकन के लिए "बाधा" के रूप में कार्य करता है - उदा। यह ईजिन को अभिव्यक्ति टेम्पलेट्स को लौटने से रोकने और वास्तव में (अनुकूलित) गणना करने का कारण बनता है, जिसके परिणामस्वरूप परिणाम = के बाएं हाथ में होता है।ईजिन: प्रदर्शन पर कोडिंग शैली का प्रभाव

ऐसा लगता है कि किसी की "कोडिंग शैली" का प्रदर्शन पर असर पड़ता है - यानि इंटरमीडिएट कंप्यूटेशंस के परिणाम को स्टोर करने के लिए नामित चर का उपयोग करके गणना के कुछ हिस्सों का मूल्यांकन करके प्रदर्शन पर नकारात्मक प्रभाव पड़ सकता है "बहुत जल्दी"।

मेरे अंतर्ज्ञान सत्यापित करने का प्रयास करने के लिए, मैं एक उदाहरण (full code here) लिखा और परिणाम पर हैरान था:

using ArrayXf = Eigen::Array <float, Eigen::Dynamic, Eigen::Dynamic>; 
using ArrayXcf = Eigen::Array <std::complex<float>, Eigen::Dynamic, Eigen::Dynamic>; 

float test1(const MatrixXcf & mat) 
{ 
    ArrayXcf arr = mat.array(); 
    ArrayXcf conj = arr.conjugate(); 
    ArrayXcf magc = arr * conj; 
    ArrayXf mag = magc.real(); 
    return mag.sum(); 
} 

float test2(const MatrixXcf & mat) 
{ 
    return (mat.array() * mat.array().conjugate()).real().sum(); 
} 

float test3(const MatrixXcf & mat) 
{ 
    ArrayXcf magc = (mat.array() * mat.array().conjugate()); 

    ArrayXf mag  = magc.real(); 
    return mag.sum(); 
} 

ऊपर एक में परिमाण के गुणांक के लिहाज से राशि की गणना के 3 अलग अलग तरीके देता है जटिल मूल्यवान मैट्रिक्स।

  1. test1 गणना के प्रत्येक हिस्से को "एक समय में एक कदम" लेता है।
  2. test2 एक अभिव्यक्ति में पूरी गणना करता है।
  3. test3 एक "मिश्रित" दृष्टिकोण लेता है - कुछ मध्यवर्ती चर के साथ।

मैं उम्मीद है कि के बाद से एक अभिव्यक्ति में test2 पैक पूरे गणना, Eigen और उस का लाभ लेने में सक्षम होगा विश्व स्तर पर की तरह पूरे गणना का अनुकूलन, सबसे अच्छा प्रदर्शन प्रदान करते हैं।

हालांकि, परिणाम थे आश्चर्य की बात (नहीं दिखाया गया संख्या प्रत्येक परीक्षा के 1000 फांसी भर में कुल माइक्रोसेकंड में हैं): (। यह जी ++ -O3 साथ संकलित किया गया था - पूर्ण विवरण के लिए the gist देख)

test1_us: 154994 
test2_us: 365231 
test3_us: 36613 

संस्करण जो मुझे सबसे तेज़ होने की उम्मीद है (test2) वास्तव में सबसे धीमा था। साथ ही, संस्करण जो मुझे धीमा होने की उम्मीद है (test1) वास्तव में बीच में था।

तो, मेरे सवाल कर रहे हैं:

  1. क्यों test3 विकल्पों से इतना बेहतर प्रदर्शन करता है?
  2. क्या कोई ऐसी तकनीक है जो ईजिन वास्तव में आपके कंप्यूटेशंस को कार्यान्वित करने में कुछ दृश्यता प्राप्त करने के लिए उपयोग कर सकती है (असेंबली कोड में डाइविंग से कम)?
  3. क्या आपके ईजिन कोड में प्रदर्शन और पठनीयता (इंटरमीडिएट चर के उपयोग) के बीच एक अच्छा व्यापारिक हमला करने के लिए दिशानिर्देशों का एक सेट है?

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

+3

मैं एक अनुकूलक विशेषज्ञ नहीं हूँ, लेकिन मैं दिया आप '-O3' साथ संकलन किया गया, और के परिणामों में से किसी पर कब्जा नहीं अपने परिणाम पर शक होगा गणना यह पूरी तरह से व्यवहार्य है कि अनुकूलक यह पहचान लेगा कि 'funcN()' का कोई दुष्प्रभाव नहीं है और संपूर्ण गणना को अनुकूलित करें। मेरा मानना ​​है कि आप माइक्रो बेंचमार्किंग की सहायता के लिए 'अस्थिर' का उपयोग कर सकते हैं। [प्रासंगिक तो सवाल] (http://stackoverflow.com/questions/6130100/using-volatile-to-prevent-compiler-optimization-in-benchmarking-code) –

+0

ध्यान दें कि हाल ही में एक संकलक के साथ, कार्यक्रम हर समय aborts । यह केवल पुराने compilers के साथ गुजरता है कि कहा जाता है 'abs' के संस्करण पूर्णांक संस्करण है ... –

उत्तर

13

यह जीसीसी की समस्या की तरह दिखता है। इंटेल कंपाइलर अपेक्षित परिणाम देता है।

$ g++ -I ~/program/include/eigen3 -std=c++11 -O3 a.cpp -o a && ./a 
test1_us: 200087 
test2_us: 320033 
test3_us: 44539 

$ icpc -I ~/program/include/eigen3 -std=c++11 -O3 a.cpp -o a && ./a 
test1_us: 214537 
test2_us: 23022 
test3_us: 42099 

icpc संस्करण की तुलना में, gcc समस्या अपने test2 के अनुकूलन के लिए लगता है।

अधिक सटीक परिणाम के लिए, आप here दिखाए गए अनुसार -DNDEBUG द्वारा डीबग दावे को बंद करना चाहते हैं।

संपादित

प्रश्न 1

@ggael के लिए एक शानदार जवाब है कि gcc राशि पाश vectorizing विफल रहता है देता है। मेरे प्रयोग में यह भी पाया गया है कि test2gcc और icc दोनों के साथ हैंड-लिखित बेवकूफ के लिए जितना तेज़ है, यह सुझाव देता है कि वेक्टरनाइज़ेशन कारण है, और नीचे उल्लिखित विधि से test2 में कोई अस्थायी स्मृति आवंटन नहीं मिला है, यह बताते हुए कि Eigen अभिव्यक्ति का सही ढंग से मूल्यांकन करें।

मध्यवर्ती स्मृति से बचना 2

प्रश्न के लिए मुख्य उद्देश्य है कि Eigen अभिव्यक्ति टेम्पलेट का उपयोग है। तो Eigen एक मैक्रो EIGEN_RUNTIME_NO_MALLOC प्रदान करता है और एक सरल फ़ंक्शन आपको यह जांचने में सक्षम बनाता है कि अभिव्यक्ति की गणना के दौरान मध्यवर्ती स्मृति आवंटित की जाती है या नहीं। आप एक नमूना कोड here पा सकते हैं। कृपया ध्यान दें कि यह केवल डीबग मोड में काम कर सकता है।

EIGEN_RUNTIME_NO_MALLOC - अगर परिभाषित, एक नया स्विच शुरू की है जो set_is_malloc_allowed (bool) को फोन करके पर और बंद किया जा सकता। malloc अनुमति नहीं है तो और Eigen स्मृति को आबंटित करने गतिशील वैसे भी, एक अभिकथन विफलता परिणाम कोशिश करता है। डिफ़ॉल्ट रूप से परिभाषित नहीं है।

प्रश्न के लिए मध्यवर्ती चर का उपयोग करने के लिए, और प्रदर्शन में सुधार एक ही समय में आलसी मूल्यांकन/अभिव्यक्ति टेम्पलेट्स द्वारा शुरू की पाने के लिए एक तरीका है।

तरह से सही डेटा प्रकार के साथ मध्यवर्ती चर का उपयोग करने के लिए है। Eigen::Matrix/Array, जो निर्देश देता है अभिव्यक्ति का मूल्यांकन किया जाना का उपयोग कर के बजाय, आप अभिव्यक्ति प्रकार Eigen::MatrixBase/ArrayBase/DenseBase ताकि अभिव्यक्ति केवल बफ़र लेकिन मूल्यांकन नहीं किया जाता है का उपयोग करना चाहिए। इसका अर्थ यह है कि अभिव्यक्ति के परिणाम के बजाय आपको अभिव्यक्ति को मध्यवर्ती के रूप में स्टोर करना चाहिए, इस शर्त के साथ कि इस मध्यवर्ती को केवल निम्नलिखित कोड में उपयोग किया जाएगा।

अभिव्यक्ति प्रकार Eigen::MatrixBase/... में टेम्पलेट पैरामीटर का निर्धारण करने के रूप में दर्दनाक हो सकता है, तो आप इसके बजाय auto का उपयोग कर सकते हैं। this page में आपको auto/अभिव्यक्ति प्रकारों का उपयोग नहीं करना चाहिए/तो आपको कुछ संकेत मिल सकते हैं। Another page आपको यह भी बताता है कि अभिव्यक्तियों को मूल्यांकन किए बिना फ़ंक्शन पैरामीटर के रूप में कैसे पास किया जाए।

@ggael के उत्तर में .abs2() के निर्देशक प्रयोग के अनुसार, मुझे लगता है कि पहिया को पुनर्निर्मित करने से बचने के लिए एक और दिशानिर्देश है।

+0

प्रतिक्रिया के लिए धन्यवाद! मैं मूल रूप से 'auto' जिस तरह से आप का वर्णन में उपयोग के बारे में सोचा था, लेकिन मैं दूर से डर गया" का उपयोग नहीं करते 'auto' ... जब तक आप 100% तुम क्या कर रहे हैं के बारे में निश्चित" टिप्पणी। :-) – jeremytrimble

+2

'ऑटो' को मध्यवर्ती बफर भी देना चाहिए। – Yakk

+0

वे केवल मध्यवर्ती जो आप का मूल्यांकन किया जा नहीं करना चाहती बफ़र करने के लिए इस्तेमाल किया जाना चाहिए। – kangshiyin

0

मैं सिर्फ आपको यह नोट करना चाहता हूं कि आपने एक गैर-इष्टतम तरीके से प्रोफाइलिंग की है, इसलिए वास्तव में यह समस्या आपकी प्रोफाइलिंग विधि हो सकती है।..

int warmUpCycles = 100; 
int profileCycles = 1000; 

// TEST 1 
for(int i=0; i<warmUpCycles ; i++) 
     doTest1(); 

auto tick = std::chrono::steady_clock::now(); 
for(int i=0; i<profileCycles ; i++) 
     doTest1(); 
auto tock = std::chrono::steady_clock::now(); 
test1_us = (std::chrono::duration_cast<std::chrono::microseconds>(tock-tick)).count(); 

// TEST 2 


// TEST 3 

एक बार जब आप उचित तरीके से परीक्षण किया है, तो आप निष्कर्ष पर आ सकते हैं

:

चूंकि कैश इलाके की तरह बहुत सी बातें ध्यान में रखने के लिए आप की रूपरेखा कि जिस तरह से यह कर सकते हैं

मुझे अत्यधिक संदेह है कि जब आप एक समय में एक ऑपरेशन प्रोफाइल कर रहे हैं, तो आप तीसरे टेस्ट पर कैश किए गए संस्करण का उपयोग कर समाप्त हो जाते हैं क्योंकि ऑपरेशन को संकलक द्वारा पुन: आदेश दिया जा सकता है।

इसके अलावा आपको यह देखने के लिए अलग-अलग कंपाइलर्स आज़माएं कि क्या समस्या टेम्पलेट्स को अनलोल करना है (टेम्पलेट को अनुकूलित करने की गहराई सीमा है: यह संभावना है कि आप इसे एक बड़ी अभिव्यक्ति के साथ दबा सकते हैं)।

अगर ईजिन समर्थन सेमेन्टिक्स को स्थानांतरित करता है, तो इसका कोई कारण नहीं है कि एक संस्करण तेज क्यों होना चाहिए क्योंकि यह हमेशा गारंटी नहीं देता है कि अभिव्यक्ति को अनुकूलित किया जा सकता है।

कृपया कोशिश करें और मुझे बताएं, यह दिलचस्प है। यह सुनिश्चित करें कि -O3 जैसे झंडे के साथ अनुकूलन सक्षम करना सुनिश्चित करें, अनुकूलन के बिना प्रोफाइलिंग व्यर्थ है।

कंपाइलर को सबकुछ दूर करने के लिए रोकने के लिए, फ़ाइल से प्रारंभिक इनपुट का उपयोग करें या cin और फिर फ़ंक्शंस के अंदर इनपुट को फिर से फ़ीड करें।

+1

ले जाएँ अर्थ यहाँ कोई मदद नहीं की है, क्योंकि temporaries कैश छूट जाए परिचय और संचालन फ़्यूज़ होने से अनुकूलन के अवसर है कि संभव बना रहे हैं को रोकने क्योंकि। – ggael

13

क्या होता है कि .real() चरण की वजह से, ईजिन स्पष्ट रूप से test2 को सदिश नहीं करेगा। इस प्रकार यह मानक कॉम्प्लेक्स :: ऑपरेटर * ऑपरेटर को कॉल करेगा, दुर्भाग्य से, जीसीसी द्वारा कभी भी रेखांकित नहीं किया जाता है। दूसरी तरफ, अन्य संस्करण, इजेन के परिसरों के स्वयं के वेक्टरकृत उत्पाद कार्यान्वयन का उपयोग करते हैं।

इसके विपरीत, आईसीसी इनलाइन जटिल :: ऑपरेटर * करता है, इस प्रकार test2 आईसीसी के लिए सबसे तेजी से कर रही है। तुम भी फिर से लिखने कर सकते हैं test2 के रूप में:

return mat.array().abs2().sum(); 

सभी compilers पर भी बेहतर प्रदर्शन प्राप्त करने के:

gcc: 
test1_us: 66016 
test2_us: 26654 
test3_us: 34814 

icpc: 
test1_us: 87225 
test2_us: 8274 
test3_us: 44598 

clang: 
test1_us: 87543 
test2_us: 26891 
test3_us: 44617 

इस मामले में आईसीसी की बहुत अच्छी स्कोर अपने चतुर ऑटो vectorization इंजन की वजह से है।

test2 संशोधित किए बिना जीसीसी की इनलाइनिंग विफलता को हल करने का एक और तरीका complex<float> के लिए अपना खुद का operator* परिभाषित करना है।

namespace std { 
    complex<float> operator*(const complex<float> &a, const complex<float> &b) { 
    return complex<float>(real(a)*real(b) - imag(a)*imag(b), imag(a)*real(b) + real(a)*imag(b)); 
    } 
} 

और फिर मैं: उदाहरण के लिए, निम्नलिखित जोड़ें अपनी फ़ाइल के शीर्ष पर

gcc: 
test1_us: 69352 
test2_us: 28171 
test3_us: 36501 

icpc: 
test1_us: 93810 
test2_us: 11350 
test3_us: 51007 

clang: 
test1_us: 83138 
test2_us: 26206 
test3_us: 45224 
बेशक

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

+0

ठीक है, तो ऐसा लगता है मेरे अंतर्ज्ञान की तरह सही हो गया होता एक जीसीसी मोड़ के अलावा,। जवाब के लिए धन्यवाद। मेरे अन्य दो प्रश्नों के बिंदु पर, क्या कोई ऐसी युक्तियां हैं जो आप इस बात की अंतर्दृष्टि के लिए सुझाव देंगे कि ईजिन ने अभिव्यक्ति को अनुकूलित करने के लिए कैसे चुना है? इसके अलावा, वहाँ (जैसे मैं घोषित किया जा सकता था 'arr',' मूल्यांकन बाद में होने की अनुमति देने के लिए conj', 'magc' आदि कुछ अलग प्रकार के रूप में) आलसी मूल्यांकन निरोधक बिना एकाधिक subexpressions में एक अभिकलन को तोड़ने के लिए किसी भी तरह से है? – jeremytrimble

+0

क्यों नहीं '.real()' vectorized जा, पुस्तकालय में दोष होगा? – Yakk

+3

'-fcx सीमित-range' (' -ffast-math' में शामिल है) या '-fcx-fortran-rules', जीसीसी के साथ जटिल गुणा इनलाइन होगा। असुरक्षित मोड आईसीसी चूक, एक संदिग्ध पसंद ... –

4

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

float test1(const MatrixXcf & mat) 
{ 
    auto arr = mat.array(); 
    auto conj = arr.conjugate(); 
    auto magc = arr * conj; 
    auto mag = magc.real(); 
    return mag.sum(); 
} 

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

संबंधित मुद्दे