2016-10-19 8 views
6

बेंचमार्किंग मैं हाल ही में इस शानदार cpp2015 बात CppCon 2015: Chandler Carruth "Tuning C++: Benchmarks, and CPUs, and Compilers! Oh My!"रोकथाम संकलक अनुकूलन जबकि

तकनीक कोड के अनुकूलन नीचे कार्यों उपयोग कर रहा है से संकलक को रोकने के लिए उल्लेख किया है में से एक में जाना।

static void escape(void *p) { 
    asm volatile("" : : "g"(p) : "memory"); 
} 

static void clobber() { 
    asm volatile("" : : : "memory"); 
} 

void benchmark() 
{ 
    vector<int> v; 
    v.reserve(1); 
    escape(v.data()); 
    v.push_back(10); 
    clobber() 
} 

मैं इसे समझने की कोशिश कर रहा हूं। निम्नानुसार प्रश्न।

1) क्लॉबर पर बचने का क्या फायदा है?

2) ऊपर दिए गए उदाहरण से यह क्लोबबर() पिछले कथन (push_back) को अनुकूलित तरीके से रोकता है। अगर ऐसा है तो नीचे स्निपेट सही क्यों नहीं है?

void benchmark() 
{ 
    vector<int> v; 
    v.reserve(1); 
    v.push_back(10); 
    clobber() 
} 

यदि यह पर्याप्त भ्रमित नहीं था, मूर्खता (अमेरिकन प्लान के सूत्रण lib) एक और भी stranger implementation

प्रासंगिक स्निपेट है:

कि
template <class T> 
void doNotOptimizeAway(T&& datum) { 
    asm volatile("" : "+r" (datum)); 
} 

मेरी समझ से ऊपर टुकड़ा संकलक सूचित करता है कि असेंबली ब्लॉक डाटाम को लिखेंगे। लेकिन अगर संकलक को पता चलता है कि इस डेटाम का कोई उपभोक्ता नहीं है तो यह अभी भी दाटम सही उत्पादित इकाई को अनुकूलित कर सकता है?

मुझे लगता है कि यह सामान्य ज्ञान नहीं है और किसी भी मदद की सराहना की जाती है!

+2

वास्तव में, "+ r" इसका मतलब है कि स्निपेट, दोनों पढ़ सकते हैं और गृहीत लिखेंगे। चूंकि (जीसीसी) कंपाइलर यह नहीं जानता कि एएसएम द्वारा डाटाम का उपयोग/संशोधित किया जा सकता है, यह किसी भी परिस्थिति में इसे अनुकूलित नहीं कर सकता है। –

+1

वह कार्यक्षमता अब Google बेंचमार्क में बनाई गई है। बेंचमार्क :: DoNotOptimize जैसा कि यहां देखा गया है: github.com/google/benchmark – xaxxon

उत्तर

3

1) क्लॉबर पर बचने का क्या फायदा है?

escape()clobber() से अधिक लाभ नहीं है। escape()पूरक निम्नलिखित महत्वपूर्ण रास्ते मेंclobber():

clobber() के प्रभाव याद है कि एक काल्पनिक वैश्विक जड़ सूचक के माध्यम से संभावित रूप से पहुँचा जा सकता है तक सीमित है। दूसरे शब्दों में, आवंटित स्मृति का कंपाइलर का मॉडल पॉइंटर्स के माध्यम से एक दूसरे के संदर्भ में ब्लॉक का एक कनेक्टेड ग्राफ है, और कहा गया काल्पनिक वैश्विक रूट सूचक उस ग्राफ के प्रवेश बिंदु के रूप में कार्य करता है। (इस मॉडल में मेमोरी लीक का हिसाब नहीं लिया जाता है, यानी कंपाइलर इस संभावना को अनदेखा करता है कि खोए गए सूचक मूल्य के कारण एक बार सुलभ ब्लॉक पहुंच योग्य हो सकते हैं)। एक नया आवंटित ब्लॉक ऐसे ग्राफ का हिस्सा नहीं है, और clobber() के किसी भी दुष्प्रभाव से प्रतिरक्षा है। escape() यह सुनिश्चित करता है कि पारित पता स्मृति ब्लॉक के वैश्विक स्तर पर सुलभ सेट से संबंधित है। जब एक नए आवंटित मेमोरी ब्लॉक पर लागू होता है, तो escape() को इसे ग्राफ में जोड़ने का प्रभाव होता है।

2) ऊपर दिए गए उदाहरण से यह क्लॉबर() पिछले कथन (push_back) को अनुकूलित तरीके से रोकता है। यदि यह केस है तो नीचे स्निपेट सही क्यों नहीं है?

void benchmark() 
{ 
    vector<int> v; 
    v.reserve(1); 
    v.push_back(10); 
    clobber(); 
} 

आवंटन v.reserve(1) अंदर छुपा clobber() को दिखाई, जब तक यह escape() के माध्यम से पंजीकृत है नहीं है।

4

टीएल; डी doNotOptimizeAway एक कृत्रिम "उपयोग" बनाता है।

यहां कुछ शब्दकोष: एक "def" ("परिभाषा") एक कथन है, जो एक चर के लिए मान निर्दिष्ट करता है; एक "उपयोग" एक कथन है, जो कुछ ऑपरेशन करने के लिए एक चर के मान का उपयोग करता है।

यदि किसी डीफ़ के तुरंत बाद बिंदु से, प्रोग्राम से बाहर निकलने के सभी पथों को एक चर के उपयोग का सामना नहीं करना पड़ता है, तो def को dead कहा जाता है और मृत कोड उन्मूलन (डीसीई) पास इसे हटा देगा। जो बदले में अन्य डीफ मरने का कारण बन सकता है (यदि वह डीफ़ वैरिएबल ऑपरेटिंग के आधार पर उपयोग किया गया था), आदि

स्केलर प्रतिस्थापन (एसआरए) पास के स्केलर रिप्लेसमेंट के बाद कार्यक्रम की कल्पना करें, जो स्थानीय std::vector को बदल देता है दो चर len और ptr। किसी बिंदु पर कार्यक्रम ptr पर मान निर्दिष्ट करता है; वह कथन एक डीफ़ है।

अब, मूल कार्यक्रम ने वेक्टर के साथ कुछ भी नहीं किया; दूसरे शब्दों में len या ptr का उपयोग नहीं किया गया था। इसलिए, उनके सभी डीफ़ मर गए हैं और डीसीई उन्हें हटा सकता है, प्रभावी ढंग से सभी कोड हटा रहा है और बेंचमार्क बेकार बना सकता है।

doNotOptimizeAway(ptr) जोड़ना एक कृत्रिम उपयोग बनाता है, जो डीसीई को डीफ़ को हटाने से रोकता है। (एक साइड नोट के रूप में, मुझे "+" में कोई बिंदु नहीं दिखता है, "जी" पर्याप्त होना चाहिए था)।

मेमोरी लोड और स्टोर्स के साथ तर्क की एक समान पंक्ति का पालन किया जा सकता है: एक स्टोर (डीएफ) मर जाता है अगर प्रोग्राम के अंत तक कोई रास्ता नहीं है, जिसमें उस स्टोर स्थान से लोड (उपयोग) होता है। जैसा कि मनमाना स्मृति स्थानों को ट्रैक करना व्यक्तिगत छद्म-रजिस्टर चरों को ट्रैक करने से बहुत कठिन है, कंपाइलर कारणों से रूढ़िवादी कारण है - यदि कार्यक्रम के अंत तक कोई रास्ता नहीं है, तो संभवतः उस स्टोर का उपयोग कर सकता है।

ऐसा एक मामला, स्मृति के क्षेत्र में एक स्टोर है, जिसे एलियास नहीं किया जाता है - उस स्मृति को समाप्त करने के बाद, संभवतः उस स्टोर का उपयोग नहीं हो सकता है, जो अपरिभाषित व्यवहार को ट्रिगर नहीं करता है। IOW, ऐसे कोई उपयोग नहीं हैं।

इस प्रकार एक कंपाइलर v.push_back(42) को समाप्त कर सकता है। लेकिन escape आता है - यह v.data() को मनमाने ढंग से उपनाम के रूप में माना जाता है, जैसा उपरोक्त वर्णित @Leon है।

उदाहरण में clobber() का उद्देश्य सभी एलियाज्ड मेमोरी का कृत्रिम उपयोग बनाना है। हमारे पास एक स्टोर है (push_back(42) से), स्टोर एक ऐसे स्थान पर है जो विश्व स्तर पर अलियाकृत है (escape(v.data()) के कारण), इसलिए clobber() संभावित रूप से उस स्टोर (आईओओ, स्टोर साइड इफेक्ट को देखने योग्य) का उपयोग कर सकता है, इसलिए कंपाइलर को स्टोर को हटाने की अनुमति नहीं है।

कुछ सरल उदाहरण:

उदाहरण मैं:

void f() { 
    int v[1]; 
    v[0] = 42; 
} 

यह किसी भी कोड उत्पन्न नहीं करता है।

उदाहरण द्वितीय:

extern void g(); 

void f() { 
    int v[1]; 
    v[0] = 42; 
    g(); 
} 

यह g() करने के लिए सिर्फ एक फोन, कोई स्मृति दुकान उत्पन्न करता है। फ़ंक्शन g संभवतः v तक नहीं पहुंच सकता है क्योंकि v उपनाम नहीं है।

उदाहरण तृतीय:

void clobber() { 
    __asm__ __volatile__ ("" : : : "memory"); 
} 

void f() { 
    int v[1]; 
    v[0] = 42; 
    clobber(); 
} 

पिछले उदाहरण में की तरह, कोई दुकान उत्पन्न क्योंकि v एलियास नहीं है और clobber करने के लिए कॉल करने के लिए कुछ भी नहीं inlined है।

उदाहरण चतुर्थ:

template<typename T> 
void use(T &&t) { 
    __asm__ __volatile__ ("" :: "g" (t)); 
} 

void f() { 
    int v[1]; 
    use(v); 
    v[0] = 42; 
} 

इस बार v पलायन (अर्थात संभवत: अन्य सक्रियण फ्रेम से पहुँचा जा सकता)। हालांकि, स्टोर अभी भी हटा दिया गया है, क्योंकि इसके बाद उस स्मृति का कोई संभावित उपयोग नहीं था (यूबी के बिना)।

उदाहरण वी:

template<typename T> 
void use(T &&t) { 
    __asm__ __volatile__ ("" :: "g" (t)); 
} 

extern void g(); 

void f() { 
    int v[1]; 
    use(v); 
    v[0] = 42; 
    g(); // same with clobber() 
} 

और अंत में हम दुकान मिलता है, क्योंकि v पलायन और संकलक परंपरागत ढंग से की ही होगी g करने के लिए कॉल संग्रहीत मूल्य का उपयोग कर सकते हैं।

(प्रयोगों https://godbolt.org/g/rFviMI के लिए)

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