st ++ :: C ++ 11 को सक्षम करते समय वेक्टर प्रदर्शन प्रतिगमन


235

मुझे एक छोटा C ++ स्निपेट में एक दिलचस्प प्रदर्शन प्रतिगमन मिला है, जब मैं C ++ 11 सक्षम करता हूं:

#include <vector>

struct Item
{
  int a;
  int b;
};

int main()
{
  const std::size_t num_items = 10000000;
  std::vector<Item> container;
  container.reserve(num_items);
  for (std::size_t i = 0; i < num_items; ++i) {
    container.push_back(Item());
  }
  return 0;
}

जी ++ (जीसीसी) के साथ 4.8.2 20131219 (प्रीलेरेज) और सी ++ 03 मुझे मिलता है:

milian:/tmp$ g++ -O3 main.cpp && perf stat -r 10 ./a.out

Performance counter stats for './a.out' (10 runs):

        35.206824 task-clock                #    0.988 CPUs utilized            ( +-  1.23% )
                4 context-switches          #    0.116 K/sec                    ( +-  4.38% )
                0 cpu-migrations            #    0.006 K/sec                    ( +- 66.67% )
              849 page-faults               #    0.024 M/sec                    ( +-  6.02% )
       95,693,808 cycles                    #    2.718 GHz                      ( +-  1.14% ) [49.72%]
  <not supported> stalled-cycles-frontend 
  <not supported> stalled-cycles-backend  
       95,282,359 instructions              #    1.00  insns per cycle          ( +-  0.65% ) [75.27%]
       30,104,021 branches                  #  855.062 M/sec                    ( +-  0.87% ) [77.46%]
            6,038 branch-misses             #    0.02% of all branches          ( +- 25.73% ) [75.53%]

      0.035648729 seconds time elapsed                                          ( +-  1.22% )

दूसरी ओर C ++ 11 सक्षम होने के साथ, प्रदर्शन में काफी गिरावट आई है:

milian:/tmp$ g++ -std=c++11 -O3 main.cpp && perf stat -r 10 ./a.out

Performance counter stats for './a.out' (10 runs):

        86.485313 task-clock                #    0.994 CPUs utilized            ( +-  0.50% )
                9 context-switches          #    0.104 K/sec                    ( +-  1.66% )
                2 cpu-migrations            #    0.017 K/sec                    ( +- 26.76% )
              798 page-faults               #    0.009 M/sec                    ( +-  8.54% )
      237,982,690 cycles                    #    2.752 GHz                      ( +-  0.41% ) [51.32%]
  <not supported> stalled-cycles-frontend 
  <not supported> stalled-cycles-backend  
      135,730,319 instructions              #    0.57  insns per cycle          ( +-  0.32% ) [75.77%]
       30,880,156 branches                  #  357.057 M/sec                    ( +-  0.25% ) [75.76%]
            4,188 branch-misses             #    0.01% of all branches          ( +-  7.59% ) [74.08%]

    0.087016724 seconds time elapsed                                          ( +-  0.50% )

क्या कोई इसे समझा सकता है? अब तक मेरा अनुभव यह था कि C ++ 11, esp सक्षम करके STL तेज हो जाता है। शब्दार्थ को आगे बढ़ाने के लिए धन्यवाद।

संपादित करें: जैसा कि सुझाव दिया गया है, container.emplace_back();इसके बजाय प्रदर्शन C ++ 03 संस्करण के बराबर है। C ++ 03 संस्करण कैसे प्राप्त कर सकते हैं push_back?

milian:/tmp$ g++ -std=c++11 -O3 main.cpp && perf stat -r 10 ./a.out

Performance counter stats for './a.out' (10 runs):

        36.229348 task-clock                #    0.988 CPUs utilized            ( +-  0.81% )
                4 context-switches          #    0.116 K/sec                    ( +-  3.17% )
                1 cpu-migrations            #    0.017 K/sec                    ( +- 36.85% )
              798 page-faults               #    0.022 M/sec                    ( +-  8.54% )
       94,488,818 cycles                    #    2.608 GHz                      ( +-  1.11% ) [50.44%]
  <not supported> stalled-cycles-frontend 
  <not supported> stalled-cycles-backend  
       94,851,411 instructions              #    1.00  insns per cycle          ( +-  0.98% ) [75.22%]
       30,468,562 branches                  #  840.991 M/sec                    ( +-  1.07% ) [76.71%]
            2,723 branch-misses             #    0.01% of all branches          ( +-  9.84% ) [74.81%]

   0.036678068 seconds time elapsed                                          ( +-  0.80% )

1
यदि आप असेंबली के लिए संकलन करते हैं, तो आप देख सकते हैं कि हुड के नीचे क्या चल रहा है। यह भी देखें stackoverflow.com/questions/8021874/...
कोगवील

8
यदि आप बदल क्या होता है push_back(Item())करने के लिए emplace_back()सी ++ 11 संस्करण में?
Cogwheel

8
ऊपर देखें, कि प्रतिगमन को "ठीक करता है"। मुझे अभी भी आश्चर्य है कि क्यों push_back C ++ 03 और C ++ 11 के बीच प्रदर्शन में वापस आ गया है।
दूधिया

1
@milianw यह बताता है कि मैं गलत कार्यक्रम संकलित कर रहा था। मेरी टिप्पणियों पर ध्यान न दें।

2
Clang3.4 के साथ C ++ 11 संस्करण तेज है, C ++ 98 संस्करण के लिए 0.047s 0.0 0.0s बनाम
प्रेटोरियन

जवाबों:


247

मैं अपनी मशीन पर आपके परिणामों को उन विकल्पों के साथ पुन: पेश कर सकता हूं जिन्हें आप अपनी पोस्ट में लिखते हैं।

हालाँकि, अगर मैं लिंक टाइम ऑप्टिमाइज़ेशन को सक्षम करता हूं (मैं -flto4.7.2 पर ध्वज को भी पास करता हूं ), तो परिणाम समान हैं:

(मैं आपका मूल कोड संकलित कर रहा हूं, container.push_back(Item());)

$ g++ -std=c++11 -O3 -flto regr.cpp && perf stat -r 10 ./a.out 

 Performance counter stats for './a.out' (10 runs):

         35.426793 task-clock                #    0.986 CPUs utilized            ( +-  1.75% )
                 4 context-switches          #    0.116 K/sec                    ( +-  5.69% )
                 0 CPU-migrations            #    0.006 K/sec                    ( +- 66.67% )
            19,801 page-faults               #    0.559 M/sec                  
        99,028,466 cycles                    #    2.795 GHz                      ( +-  1.89% ) [77.53%]
        50,721,061 stalled-cycles-frontend   #   51.22% frontend cycles idle     ( +-  3.74% ) [79.47%]
        25,585,331 stalled-cycles-backend    #   25.84% backend  cycles idle     ( +-  4.90% ) [73.07%]
       141,947,224 instructions              #    1.43  insns per cycle        
                                             #    0.36  stalled cycles per insn  ( +-  0.52% ) [88.72%]
        37,697,368 branches                  # 1064.092 M/sec                    ( +-  0.52% ) [88.75%]
            26,700 branch-misses             #    0.07% of all branches          ( +-  3.91% ) [83.64%]

       0.035943226 seconds time elapsed                                          ( +-  1.79% )



$ g++ -std=c++98 -O3 -flto regr.cpp && perf stat -r 10 ./a.out 

 Performance counter stats for './a.out' (10 runs):

         35.510495 task-clock                #    0.988 CPUs utilized            ( +-  2.54% )
                 4 context-switches          #    0.101 K/sec                    ( +-  7.41% )
                 0 CPU-migrations            #    0.003 K/sec                    ( +-100.00% )
            19,801 page-faults               #    0.558 M/sec                    ( +-  0.00% )
        98,463,570 cycles                    #    2.773 GHz                      ( +-  1.09% ) [77.71%]
        50,079,978 stalled-cycles-frontend   #   50.86% frontend cycles idle     ( +-  2.20% ) [79.41%]
        26,270,699 stalled-cycles-backend    #   26.68% backend  cycles idle     ( +-  8.91% ) [74.43%]
       141,427,211 instructions              #    1.44  insns per cycle        
                                             #    0.35  stalled cycles per insn  ( +-  0.23% ) [87.66%]
        37,366,375 branches                  # 1052.263 M/sec                    ( +-  0.48% ) [88.61%]
            26,621 branch-misses             #    0.07% of all branches          ( +-  5.28% ) [83.26%]

       0.035953916 seconds time elapsed  

कारणों के लिए, किसी को उत्पन्न विधानसभा कोड ( g++ -std=c++11 -O3 -S regr.cpp) को देखने की जरूरत है । C ++ 11 मोड में उत्पन्न कोड C ++ 98 मोड की तुलना में काफी अधिक अव्यवस्थित है और डिफ़ॉल्ट के साथ फ़ंक्शन C ++ 11 मोड में
void std::vector<Item,std::allocator<Item>>::_M_emplace_back_aux<Item>(Item&&)
विफल रहता है inline-limit

यह विफल इनलाइन का डोमिनोज़ प्रभाव है। इसलिए नहीं कि यह फ़ंक्शन कहा जा रहा है (यह भी नहीं कहा जाता है!), लेकिन क्योंकि हमें तैयार रहना है: यदि इसे कहा जाता है, तो फ़ंक्शन का अर्निंग्स ( Item.aऔर Item.b) पहले से ही सही जगह पर होना चाहिए। यह एक बहुत गड़बड़ कोड की ओर जाता है।

यहां केस के लिए उत्पन्न कोड का प्रासंगिक हिस्सा है जहां इनलाइनिंग सफल होती है :

.L42:
    testq   %rbx, %rbx  # container$D13376$_M_impl$_M_finish
    je  .L3 #,
    movl    $0, (%rbx)  #, container$D13376$_M_impl$_M_finish_136->a
    movl    $0, 4(%rbx) #, container$D13376$_M_impl$_M_finish_136->b
.L3:
    addq    $8, %rbx    #, container$D13376$_M_impl$_M_finish
    subq    $1, %rbp    #, ivtmp.106
    je  .L41    #,
.L14:
    cmpq    %rbx, %rdx  # container$D13376$_M_impl$_M_finish, container$D13376$_M_impl$_M_end_of_storage
    jne .L42    #,

यह लूप के लिए एक अच्छा और कॉम्पैक्ट है। अब, आइए इसकी तुलना असफल इनलाइन मामले से करें:

.L49:
    testq   %rax, %rax  # D.15772
    je  .L26    #,
    movq    16(%rsp), %rdx  # D.13379, D.13379
    movq    %rdx, (%rax)    # D.13379, *D.15772_60
.L26:
    addq    $8, %rax    #, tmp75
    subq    $1, %rbx    #, ivtmp.117
    movq    %rax, 40(%rsp)  # tmp75, container.D.13376._M_impl._M_finish
    je  .L48    #,
.L28:
    movq    40(%rsp), %rax  # container.D.13376._M_impl._M_finish, D.15772
    cmpq    48(%rsp), %rax  # container.D.13376._M_impl._M_end_of_storage, D.15772
    movl    $0, 16(%rsp)    #, D.13379.a
    movl    $0, 20(%rsp)    #, D.13379.b
    jne .L49    #,
    leaq    16(%rsp), %rsi  #,
    leaq    32(%rsp), %rdi  #,
    call    _ZNSt6vectorI4ItemSaIS0_EE19_M_emplace_back_auxIIS0_EEEvDpOT_   #

यह कोड अव्यवस्थित है और पिछले मामले की तुलना में बहुत अधिक चल रहा है। फ़ंक्शन call(अंतिम पंक्ति दिखाई गई) से पहले, तर्कों को उचित रूप से रखा जाना चाहिए:

leaq    16(%rsp), %rsi  #,
leaq    32(%rsp), %rdi  #,
call    _ZNSt6vectorI4ItemSaIS0_EE19_M_emplace_back_auxIIS0_EEEvDpOT_   #

हालांकि यह वास्तव में कभी निष्पादित नहीं होता है, लूप पहले चीजों को व्यवस्थित करता है:

movl    $0, 16(%rsp)    #, D.13379.a
movl    $0, 20(%rsp)    #, D.13379.b

इससे गड़बड़ कोड हो जाता है। यदि कोई कार्य नहीं callहोता है, क्योंकि inlining सफल होता है, तो हमारे पास लूप में केवल 2 चाल निर्देश हैं और कोई गड़बड़ नहीं है %rsp(स्टैक पॉइंटर)। हालाँकि, अगर इनलाइनिंग विफल हो जाती है, तो हमें 6 चालें मिलती हैं और हम बहुत गड़बड़ करते हैं %rsp

बस मेरे सिद्धांत को प्रमाणित करने के लिए (नोट करें -finline-limit), दोनों C ++ 11 मोड में:

 $ g++ -std=c++11 -O3 -finline-limit=105 regr.cpp && perf stat -r 10 ./a.out

 Performance counter stats for './a.out' (10 runs):

         84.739057 task-clock                #    0.993 CPUs utilized            ( +-  1.34% )
                 8 context-switches          #    0.096 K/sec                    ( +-  2.22% )
                 1 CPU-migrations            #    0.009 K/sec                    ( +- 64.01% )
            19,801 page-faults               #    0.234 M/sec                  
       266,809,312 cycles                    #    3.149 GHz                      ( +-  0.58% ) [81.20%]
       206,804,948 stalled-cycles-frontend   #   77.51% frontend cycles idle     ( +-  0.91% ) [81.25%]
       129,078,683 stalled-cycles-backend    #   48.38% backend  cycles idle     ( +-  1.37% ) [69.49%]
       183,130,306 instructions              #    0.69  insns per cycle        
                                             #    1.13  stalled cycles per insn  ( +-  0.85% ) [85.35%]
        38,759,720 branches                  #  457.401 M/sec                    ( +-  0.29% ) [85.43%]
            24,527 branch-misses             #    0.06% of all branches          ( +-  2.66% ) [83.52%]

       0.085359326 seconds time elapsed                                          ( +-  1.31% )

 $ g++ -std=c++11 -O3 -finline-limit=106 regr.cpp && perf stat -r 10 ./a.out

 Performance counter stats for './a.out' (10 runs):

         37.790325 task-clock                #    0.990 CPUs utilized            ( +-  2.06% )
                 4 context-switches          #    0.098 K/sec                    ( +-  5.77% )
                 0 CPU-migrations            #    0.011 K/sec                    ( +- 55.28% )
            19,801 page-faults               #    0.524 M/sec                  
       104,699,973 cycles                    #    2.771 GHz                      ( +-  2.04% ) [78.91%]
        58,023,151 stalled-cycles-frontend   #   55.42% frontend cycles idle     ( +-  4.03% ) [78.88%]
        30,572,036 stalled-cycles-backend    #   29.20% backend  cycles idle     ( +-  5.31% ) [71.40%]
       140,669,773 instructions              #    1.34  insns per cycle        
                                             #    0.41  stalled cycles per insn  ( +-  1.40% ) [88.14%]
        38,117,067 branches                  # 1008.646 M/sec                    ( +-  0.65% ) [89.38%]
            27,519 branch-misses             #    0.07% of all branches          ( +-  4.01% ) [86.16%]

       0.038187580 seconds time elapsed                                          ( +-  2.05% )

दरअसल, अगर हम कंपाइलर से उस फ़ंक्शन को इनलाइन करने के लिए बस थोड़ी सी मेहनत करने की कोशिश करते हैं, तो प्रदर्शन में अंतर दूर हो जाता है।


तो इस कहानी से क्या लेना देना है? यह विफल इनलाइन आपको बहुत महंगा पड़ सकता है और आपको संकलक क्षमताओं का पूरा उपयोग करना चाहिए: मैं केवल लिंक समय अनुकूलन की सिफारिश कर सकता हूं। इसने मेरे कार्यक्रमों (2.5x तक) को एक महत्वपूर्ण प्रदर्शन को बढ़ावा दिया और -fltoध्वज को पारित करने के लिए मुझे बस इतना करना चाहिए । यह एक बहुत अच्छा सौदा है! ;)

हालाँकि, मैं इनलाइन कीवर्ड के साथ आपके कोड को ट्रैश करने की सलाह नहीं देता; संकलक तय करें कि क्या करना है। (आशावादी को इनलाइन कीवर्ड को वैसे भी श्वेत स्थान मानने की अनुमति है।)


महान सवाल, +1!


3
NB: inlineफ़ंक्शन इनलाइनिंग से कोई लेना-देना नहीं है; इसका अर्थ है "परिभाषित इनलाइन" नहीं "कृपया इनलाइन करें"। यदि आप वास्तव में इनलाइनिंग, उपयोग __attribute__((always_inline))या समान मांगना चाहते हैं ।
जॉन पर्पडी

2
@JonPurdy बिलकुल नहीं है, उदाहरण के लिए वर्ग के सदस्य कार्य अंतर्निहित रूप से इनलाइन हैं। inlineकंपाइलर के लिए एक अनुरोध यह भी है कि आप फ़ंक्शन को इनलाइन करना चाहते हैं और उदाहरण के लिए Intel C ++ कंपाइलर ने प्रदर्शन चेतावनी देने के लिए उपयोग किया है यदि यह आपके अनुरोध को पूरा नहीं करता है। (मैं हाल ही में अगर यह अभी भी करता है icc की जाँच नहीं की है।) दुर्भाग्य से, मैंने देखा है कि लोग अपने कोड को ट्रैश inlineकर रहे हैं और चमत्कार होने की प्रतीक्षा कर रहे हैं। मैं उपयोग नहीं करता __attribute__((always_inline)); संभावना है कि कंपाइलर डेवलपर्स बेहतर जानते हैं कि इनलाइन क्या है और क्या नहीं। (यहां प्रतिवाद के बावजूद।)
अली

1
@JonPurdy दूसरी ओर, यदि आप एक फ़ंक्शन इनलाइन को परिभाषित करते हैं जो किसी वर्ग का सदस्य फ़ंक्शन नहीं है , तो आपके पास वास्तव में कोई विकल्प नहीं है, लेकिन इसे इनलाइन चिह्नित करने के लिए अन्यथा आपको लिंकर से कई परिभाषा त्रुटियां मिलेंगी। अगर आपका यही मतलब है तो ठीक है।
अली

1
हां, मेरा यही मतलब है। मानक कहते हैं, " inlineनिर्दिष्ट कार्यान्वयन के लिए संकेत करता है कि कॉल के बिंदु पर फ़ंक्शन बॉडी का इनलाइन प्रतिस्थापन सामान्य फ़ंक्शन कॉल तंत्र के लिए पसंद किया जाना है।" ((7.1.2.2) हालांकि, कार्यान्वयन को उस अनुकूलन को करने की आवश्यकता नहीं होती है, क्योंकि यह काफी हद तक एक संयोग है कि inlineकार्य अक्सर इनलाइनिंग के लिए अच्छे उम्मीदवार होने के लिए होते हैं। इसलिए यह स्पष्ट होना बेहतर है और एक कंपाइलर प्रागमा का उपयोग करना है।
जॉन पूर्डी

3
@JonPurdy पहले हाफ के लिए: हां, इसका मतलब यह है कि " ऑप्टिमाइज़र को इनलाइन कीवर्ड को वैसे भी व्हाइट स्पेस मानने की अनुमति है।" कंपाइलर प्राग्मा के लिए, मैं इसका उपयोग नहीं करूंगा, मैं इसे लिंक टाइम ऑप्टिमाइज़ेशन तक छोड़ दूंगा कि इनलाइन या नहीं। यह बहुत अच्छा काम करता है; इसने अपने आप ही इस मुद्दे को हल कर दिया जिसका जवाब यहाँ चर्चा में दिया गया है।
अली
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.