2013-10-18 2 views
19

सी ++ फ़ाइल में किसी फ़ंक्शन की स्थिति उसके प्रदर्शन को क्यों प्रभावित करती है? विशेष रूप से नीचे दिए गए उदाहरण में हमारे पास दो समान कार्य हैं जिनमें अलग-अलग, लगातार प्रदर्शन प्रोफ़ाइल हैं। कोई इसकी जांच करने और यह निर्धारित करने के लिए कैसे जाता है कि प्रदर्शन इतना अलग क्यों है?सी ++ फ़ाइल में किसी फ़ंक्शन की स्थिति उसके प्रदर्शन को प्रभावित करती है

उदाहरण बहुत सरल है कि हमारे पास दो कार्य हैं: ए और बी। प्रत्येक एक तंग पाश में कई बार चलाया जाता है और अनुकूलित (-O3 -march=corei7-avx) और समय।

#include <cstdint> 
#include <iostream> 
#include <numeric> 

#include <boost/timer/timer.hpp> 

bool array[] = {true, false, true, false, false, true}; 

uint32_t __attribute__((noinline)) a() { 
    asm(""); 
    return std::accumulate(std::begin(array), std::end(array), 0); 
} 

uint32_t __attribute__((noinline)) b() { 
    asm(""); 
    return std::accumulate(std::begin(array), std::end(array), 0); 
} 

const size_t WARM_ITERS = 1ull << 10; 
const size_t MAX_ITERS = 1ull << 30; 

void test(const char* name, uint32_t (*fn)()) 
{ 
    std::cout << name << ": "; 
    for (size_t i = 0; i < WARM_ITERS; i++) { 
     fn(); 
     asm(""); 
    } 
    boost::timer::auto_cpu_timer t; 
    for (size_t i = 0; i < MAX_ITERS; i++) { 
     fn(); 
     asm(""); 
    } 
} 

int main(int argc, char **argv) 
{ 
    test("a", a); 
    test("b", b); 
    return 0; 
} 

कुछ उल्लेखनीय विशेषताएं::

  • समारोह ए और बी समान हैं यहाँ कोड है। वे एक ही संचय ऑपरेशन करते हैं और एक ही असेंबली निर्देशों को संकलित करते हैं।
  • टाइमिंग शुरू करने से पहले प्रत्येक परीक्षण पुनरावृत्ति में गर्मी की अवधि होती है और कैश को गर्म करने के साथ किसी भी समस्या को खत्म करने से पहले।

जब यह संकलित करने और चलाने है हम दिखा एक ख की तुलना में काफी धीमी है निम्न आउटपुट प्राप्त:

[[email protected]:~/code/mystery] make && ./mystery 
g++-4.8 -c -g -O3 -Wall -Wno-unused-local-typedefs -std=c++11 -march=corei7-avx -I/usr/local/include/boost-1_54/ mystery.cpp -o mystery.o 
g++-4.8 mystery.o -lboost_system-gcc48-1_54 -lboost_timer-gcc48-1_54 -o mystery 
a: 7.412747s wall, 7.400000s user + 0.000000s system = 7.400000s CPU (99.8%) 
b: 5.729706s wall, 5.740000s user + 0.000000s system = 5.740000s CPU (100.2%) 

हम दो परीक्षणों को उलटने के हैं (यानी फोन test(b) और फिर test(a)) एक अभी भी धीमी है ख से:

[[email protected]:~/code/mystery] make && ./mystery 
g++-4.8 -c -g -O3 -Wall -Wno-unused-local-typedefs -std=c++11 -march=corei7-avx -I/usr/local/include/boost-1_54/ mystery.cpp -o mystery.o 
g++-4.8 mystery.o -lboost_system-gcc48-1_54 -lboost_timer-gcc48-1_54 -o mystery 
b: 5.733968s wall, 5.730000s user + 0.000000s system = 5.730000s CPU (99.9%) 
a: 7.414538s wall, 7.410000s user + 0.000000s system = 7.410000s CPU (99.9%) 

अब हम ++ फ़ाइल सी में कार्यों का स्थान उलटने हैं (एक के ऊपर ख की परिभाषा के लिए कदम) परिणाम उल्टे कर रहे हैं और एक बी की तुलना में तेजी बन जाता है!

[[email protected]:~/code/mystery] make && ./mystery 
g++-4.8 -c -g -O3 -Wall -Wno-unused-local-typedefs -std=c++11 -march=corei7-avx -I/usr/local/include/boost-1_54/ mystery.cpp -o mystery.o 
g++-4.8 mystery.o -lboost_system-gcc48-1_54 -lboost_timer-gcc48-1_54 -o mystery 
a: 5.729604s wall, 5.720000s user + 0.000000s system = 5.720000s CPU (99.8%) 
b: 7.411549s wall, 7.420000s user + 0.000000s system = 7.420000s CPU (100.1%) 

तो अनिवार्य रूप से सी ++ फ़ाइल के शीर्ष पर जो भी फ़ंक्शन धीमा है, धीमा है।

प्रश्नों के कुछ उत्तर आप हो सकता है:

  • कोड संकलित दोनों ए और बी के लिए समान है। Disassembly की जांच की गई है। (रुचि रखने वालों के लिए: http://pastebin.com/2QziqRXR)
  • कोड जीसीसी 4.8, जीसीसी 4.8.1 यूबंटू 13.04, उबंटू 13.10, और उबंटू 12.04.03 पर संकलित किया गया था।
  • प्रभाव इंटेल सैंडी ब्रिज i7-2600 और इंटेल ज़ीऑन एक्स 5482 सीपीयू पर देखे गए प्रभाव।

ऐसा क्यों होगा? इस तरह की कुछ जांच करने के लिए कौन से टूल्स उपलब्ध हैं?

+0

क्या यह संभव है कि वे अलग-अलग पृष्ठों पर समाप्त हो रहे हैं, और इससे अतिरिक्त काम हो रहा है? मुझे यह अजीब लगता है कि CPU समय सिस्टम माप में है, उपयोगकर्ता नहीं। इसका तात्पर्य है कि यह उपयोगकर्ता कोड लेने का समय नहीं चल रहा है, लेकिन प्रक्रियाओं की ओर से कुछ ओएस स्तर की बात है। –

+0

अंधेरे में एक पूर्ण शॉट के रूप में, मैं सुझाव दूंगा कि पहले सत्र चलने वाले सत्र के परिणामस्वरूप बी सत्र को और भी गर्म किया जाएगा ... (संपादित करें: ओह, आप उलटा हुआ ...) –

+0

@ डेव्स मेरा मानना ​​है कि समय सभी उपयोगकर्ता अंतरिक्ष भूमि में है। लूप गर्मी लूप (माप से पहले) कैश और शाखा भविष्यवाणी को गर्म करने के लिए पर्याप्त होना चाहिए। – Shane

उत्तर

6

ऐसा लगता है कि यह कैश एलियासिंग समस्या है।

परीक्षण केस काफी चालाक है, और इसे समय से पहले कैश में सही ढंग से लोड करता है। ऐसा लगता है कि सब कुछ कैश में फिट बैठता है - हालांकि नकली, मैं valgrind के cachegrind उपकरण के उत्पादन को देखकर यह पुष्टि कर लें, और के रूप में एक ऐसा ही एक छोटे से परीक्षण के मामले में उम्मीद करेंगे, कोई महत्वपूर्ण कैश छूट जाए हैं:

valgrind --tool=cachegrind --I1=32768,8,64 --D1=32768,8,64 /tmp/so 
==11130== Cachegrind, a cache and branch-prediction profiler 
==11130== Copyright (C) 2002-2012, and GNU GPL'd, by Nicholas Nethercote et al. 
==11130== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info 
==11130== Command: /tmp/so 
==11130== 
--11130-- warning: L3 cache found, using its data for the LL simulation. 
a: 6.692648s wall, 6.670000s user + 0.000000s system = 6.670000s CPU (99.7%) 
b: 7.306552s wall, 7.280000s user + 0.000000s system = 7.280000s CPU (99.6%) 
==11130== 
==11130== I refs:  2,484,996,374 
==11130== I1 misses:   1,843 
==11130== LLi misses:   1,694 
==11130== I1 miss rate:   0.00% 
==11130== LLi miss rate:   0.00% 
==11130== 
==11130== D refs:  537,530,151 (470,253,428 rd + 67,276,723 wr) 
==11130== D1 misses:   14,477 ( 12,433 rd +  2,044 wr) 
==11130== LLd misses:   8,336 (  6,817 rd +  1,519 wr) 
==11130== D1 miss rate:   0.0% (  0.0%  +  0.0% ) 
==11130== LLd miss rate:   0.0% (  0.0%  +  0.0% ) 
==11130== 
==11130== LL refs:    16,320 ( 14,276 rd +  2,044 wr) 
==11130== LL misses:   10,030 (  8,511 rd +  1,519 wr) 
==11130== LL miss rate:   0.0% (  0.0%  +  0.0% ) 

मैंने सामान्य इंटेल CPUs से मेल खाने के लिए 64 बाइट कैश लाइन आकार के साथ 32k, 8 तरीके से संबद्ध कैश चुना, और बार-बार ए और बी फ़ंक्शन के बीच समान विसंगति देखी।

सभी लेकिन एक ही कैश लाइन आकार के साथ एक 32k, 128 रास्ता साहचर्य कैश हालांकि, कि अंतर के साथ एक काल्पनिक मशीन पर चल रहा है चला जाता है:

valgrind --tool=cachegrind --I1=32768,128,64 --D1=32768,128,64 /tmp/so 
==11135== Cachegrind, a cache and branch-prediction profiler 
==11135== Copyright (C) 2002-2012, and GNU GPL'd, by Nicholas Nethercote et al. 
==11135== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info 
==11135== Command: /tmp/so 
==11135== 
--11135-- warning: L3 cache found, using its data for the LL simulation. 
a: 6.754838s wall, 6.730000s user + 0.010000s system = 6.740000s CPU (99.8%) 
b: 6.827246s wall, 6.800000s user + 0.000000s system = 6.800000s CPU (99.6%) 
==11135== 
==11135== I refs:  2,484,996,642 
==11135== I1 misses:   1,816 
==11135== LLi misses:   1,718 
==11135== I1 miss rate:   0.00% 
==11135== LLi miss rate:   0.00% 
==11135== 
==11135== D refs:  537,530,207 (470,253,470 rd + 67,276,737 wr) 
==11135== D1 misses:   14,297 ( 12,276 rd +  2,021 wr) 
==11135== LLd misses:   8,336 (  6,817 rd +  1,519 wr) 
==11135== D1 miss rate:   0.0% (  0.0%  +  0.0% ) 
==11135== LLd miss rate:   0.0% (  0.0%  +  0.0% ) 
==11135== 
==11135== LL refs:    16,113 ( 14,092 rd +  2,021 wr) 
==11135== LL misses:   10,054 (  8,535 rd +  1,519 wr) 
==11135== LL miss rate:   0.0% (  0.0%  +  0.0% ) 
एक 8 रास्ता कैश में के बाद से

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

संपादित करें: अधिक कैश संबद्धता पर: http://en.wikipedia.org/wiki/CPU_cache#Associativity


एक और संपादित करें: मैं perf उपकरण के माध्यम से निगरानी हार्डवेयर घटना के साथ इस की पुष्टि की है।

मैंने कमांड लाइन तर्क मौजूद था या नहीं, इस पर निर्भर करता है कि मैंने केवल एक() या बी() को कॉल करने के लिए स्रोत को संशोधित किया है। समय मूल परीक्षण मामले में समान हैं।

sudo perf record -e dTLB-loads,dTLB-load-misses,dTLB-stores,dTLB-store-misses,iTLB-loads,iTLB-load-misses /tmp/so 
a: 6.317755s wall, 6.300000s user + 0.000000s system = 6.300000s CPU (99.7%) 
sudo perf report 

4K dTLB-loads 
97 dTLB-load-misses 
4K dTLB-stores 
7 dTLB-store-misses 
479 iTLB-loads 
142 iTLB-load-misses    

जबकि

sudo perf record -e dTLB-loads,dTLB-load-misses,dTLB-stores,dTLB-store-misses,iTLB-loads,iTLB-load-misses /tmp/so foobar 
b: 4.854249s wall, 4.840000s user + 0.000000s system = 4.840000s CPU (99.7%) 
sudo perf report 

3K dTLB-loads 
87 dTLB-load-misses 
3K dTLB-stores 
19 dTLB-store-misses 
259 iTLB-loads 
93 iTLB-load-misses 

कि ख दिखा कम TLB कार्रवाई है, और इसलिए कैश बेदखल किए जाने की जरूरत नहीं है। यह देखते हुए कि दोनों के बीच की कार्यक्षमता अन्यथा समान है, इसे केवल अलियासिंग द्वारा समझाया जा सकता है।

0

आप a और btest से कॉल कर रहे हैं। चूंकि कंपाइलर के पास आपके दो कार्यों को पुन: व्यवस्थित करने का कोई कारण नहीं है ab (मूल में) test से आगे है। आप टेम्पलेट का भी उपयोग कर रहे हैं ताकि वास्तविक कोड जनरेशन सी ++ स्रोत में जो दिखता है उससे काफी बड़ा हो।

इसलिए यह बहुत संभव है कि b के लिए अनुदेश स्मृति test, a दूर आगे जा रहा है कैश में नहीं है और इसलिए नीचे कम कैश या CPU मुख्य स्मृति से लाने के लिए अधिक समय लग साथ मिलकर अनुदेश कैश में हो जाता है है कि b

इसलिए यह संभव है उसकी वजह से लंबे समय तक शिक्षा b से a के लिए चक्र लाने, a रन b की तुलना में धीमी भले ही वास्तविक कोड एक ही है, यह सिर्फ आगे की दूरी पर है।

कुछ सीपीयू आर्किटेक्चर (जैसे बांह कॉर्टेक्स-ए श्रृंखला) समर्थन प्रदर्शन काउंटर जो कैश मिस की संख्या को गिनते हैं। perf जैसे टूल, उपयुक्त प्रदर्शन काउंटर के साथ काम करने के लिए सेट करते समय इस डेटा को कैप्चर कर सकते हैं।

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

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