C ++ में स्टेटमेंट ऑर्डर लागू करना


111

मान लीजिए कि मेरे पास कई बयान हैं जिन्हें मैं एक निश्चित क्रम में निष्पादित करना चाहता हूं। मैं अनुकूलन स्तर 2 के साथ g ++ का उपयोग करना चाहता हूं, इसलिए कुछ बयानों को फिर से व्यवस्थित किया जा सकता है। बयानों के एक निश्चित क्रम को लागू करने के लिए कौन से उपकरण हैं?

निम्नलिखित उदाहरण पर विचार करें।

using Clock = std::chrono::high_resolution_clock;

auto t1 = Clock::now(); // Statement 1
foo();                  // Statement 2
auto t2 = Clock::now(); // Statement 3

auto elapsedTime = t2 - t1;

इस उदाहरण में यह महत्वपूर्ण है कि दिए गए क्रम में 1-3 कथनों को निष्पादित किया जाता है। हालाँकि, संकलनकर्ता यह नहीं सोच सकता कि कथन 2 1 और 3 से स्वतंत्र है और निम्नानुसार कोड निष्पादित करें?

using Clock=std::chrono::high_resolution_clock;

foo();                  // Statement 2
auto t1 = Clock::now(); // Statement 1
auto t2 = Clock::now(); // Statement 3

auto elapsedTime = t2 - t1;

34
यदि कंपाइलर को लगता है कि वे स्वतंत्र हैं जब वे नहीं हैं, तो कंपाइलर टूट गया है और आपको एक बेहतर कंपाइलर का उपयोग करना चाहिए।
डेविड श्वार्ट्ज


1
__sync_synchronize()किसी भी मदद का हो सकता है?
vsz

3
@HowardHinnant: यदि इस तरह के निर्देश को परिभाषित किया गया था, तो मानक C की शब्दार्थ शक्ति में जबरदस्त सुधार किया जाएगा, और यदि डेटा के अवरोध के बाद किए गए रीड्स को छूट देने के लिए एलियासिंग नियमों को समायोजित किया गया था, जो इससे पहले लिखा गया था।
सुपर

4
@DavidSchwartz इस मामले में यह मापने के लिए समय fooलगता है कि चलाने के लिए कौन सा संकलक को अनदेखा करने की अनुमति है, ठीक उसी तरह जैसे कि एक अलग धागे से अवलोकन को अनदेखा करने की अनुमति है।
कोडइंचोज

जवाबों:


100

C ++ मानक समिति के साथ इस पर चर्चा के बाद मैं कुछ अधिक व्यापक उत्तर देने का प्रयास करना चाहता हूं। C ++ कमेटी का सदस्य होने के अलावा, मैं LLVM और Clang कंपाइलर का डेवलपर भी हूं।

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

हम इसे रोकने की कोशिश कर सकते हैं, लेकिन इसके अत्यंत नकारात्मक परिणाम होंगे और अंततः विफल होंगे।

सबसे पहले, संकलक में इसे रोकने का एकमात्र तरीका यह बताना है कि ये सभी मूल संचालन अवलोकनीय हैं। समस्या यह है कि यह तब कंपाइल ऑप्टिमाइजेशन के भारी बहुमत को रोक देगा। संकलक के अंदर, हमारे पास अनिवार्य रूप से मॉडल के लिए कोई अच्छा तंत्र नहीं है कि समय देखने योग्य है, लेकिन कुछ और नहीं। हमारे पास इस बात का भी अच्छा मॉडल नहीं है कि ऑपरेशन में कितना समय लगता है । एक उदाहरण के रूप में, 32-बिट अहस्ताक्षरित पूर्णांक को 64-बिट अहस्ताक्षरित पूर्णांक में परिवर्तित करने में समय लगता है? X86-64 पर शून्य समय लगता है, लेकिन अन्य आर्किटेक्चर पर इसे गैर-शून्य समय लगता है। यहां कोई उदारतापूर्वक सही उत्तर नहीं है।

लेकिन फिर भी अगर हम इन कार्यों को फिर से करने से संकलक को रोकने में कुछ नायकों के माध्यम से सफल होते हैं, तो कोई गारंटी नहीं है कि यह पर्याप्त होगा। एक x86 मशीन पर अपने C ++ प्रोग्राम को निष्पादित करने के लिए एक मान्य और अनुरूप तरीके पर विचार करें: DynamoRIO। यह एक प्रणाली है जो प्रोग्राम के मशीन कोड का गतिशील मूल्यांकन करता है। एक चीज जो यह कर सकता है वह ऑनलाइन अनुकूलन है, और यह समय के बाहर बुनियादी अंकगणितीय निर्देशों की पूरी श्रृंखला को अंजाम देने में भी सक्षम है। और यह व्यवहार गतिशील मूल्यांकनकर्ताओं के लिए अद्वितीय नहीं है, वास्तविक x86 सीपीयू भी गतिशील रूप से (बहुत कम संख्या में) निर्देश और पुन: व्यवस्थित करेगा।

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

लेकिन इस सब के कारण आपको उम्मीद नहीं खोनी चाहिए। जब आप बुनियादी गणितीय कार्यों के निष्पादन के लिए समय चाहते हैं, तो हमने अच्छी तरह से काम करने वाली तकनीकों का अध्ययन किया है। आमतौर पर इनका उपयोग माइक्रो बेंचमार्किंग करते समय किया जाता है । मैंने CppCon2015 में इस बारे में बात की: https://youtu.be/nXaxk27zwlk

वहां दिखाई गई तकनीकें विभिन्न माइक्रो-बेंचमार्क पुस्तकालयों द्वारा प्रदान की जाती हैं जैसे कि Google: https://github.com/google/benchmark#preventing-optimization

इन तकनीकों की कुंजी डेटा पर ध्यान केंद्रित करना है। आप ऑप्टिमाइज़र के लिए अभिकलन अपारदर्शी को इनपुट बनाते हैं और आशावादी को गणना अपारदर्शी के परिणाम। एक बार जब आप ऐसा कर लेते हैं, तो आप इसे मज़बूती से समय दे सकते हैं। आइए मूल प्रश्न में उदाहरण का एक यथार्थवादी संस्करण देखें, लेकिन fooकार्यान्वयन के लिए पूरी तरह से दिखाई देने वाली परिभाषा के साथ । मैंने DoNotOptimizeGoogle बेंचमार्क लाइब्रेरी से एक (गैर-पोर्टेबल) संस्करण भी निकाला है, जिसे आप यहां पा सकते हैं: https://github.com/google/benchmark/blob/master/include/benchmark/benchmark_api.h#L208

#include <chrono>

template <class T>
__attribute__((always_inline)) inline void DoNotOptimize(const T &value) {
  asm volatile("" : "+m"(const_cast<T &>(value)));
}

// The compiler has full knowledge of the implementation.
static int foo(int x) { return x * 2; }

auto time_foo() {
  using Clock = std::chrono::high_resolution_clock;

  auto input = 42;

  auto t1 = Clock::now();         // Statement 1
  DoNotOptimize(input);
  auto output = foo(input);       // Statement 2
  DoNotOptimize(output);
  auto t2 = Clock::now();         // Statement 3

  return t2 - t1;
}

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

% ./bin/clang++ -std=c++14 -c -S -o - so.cpp -O3
        .text
        .file   "so.cpp"
        .globl  _Z8time_foov
        .p2align        4, 0x90
        .type   _Z8time_foov,@function
_Z8time_foov:                           # @_Z8time_foov
        .cfi_startproc
# BB#0:                                 # %entry
        pushq   %rbx
.Ltmp0:
        .cfi_def_cfa_offset 16
        subq    $16, %rsp
.Ltmp1:
        .cfi_def_cfa_offset 32
.Ltmp2:
        .cfi_offset %rbx, -16
        movl    $42, 8(%rsp)
        callq   _ZNSt6chrono3_V212system_clock3nowEv
        movq    %rax, %rbx
        #APP
        #NO_APP
        movl    8(%rsp), %eax
        addl    %eax, %eax              # This is "foo"!
        movl    %eax, 12(%rsp)
        #APP
        #NO_APP
        callq   _ZNSt6chrono3_V212system_clock3nowEv
        subq    %rbx, %rax
        addq    $16, %rsp
        popq    %rbx
        retq
.Lfunc_end0:
        .size   _Z8time_foov, .Lfunc_end0-_Z8time_foov
        .cfi_endproc


        .ident  "clang version 3.9.0 (trunk 273389) (llvm/trunk 273380)"
        .section        ".note.GNU-stack","",@progbits

यहां आप संकलक को कॉल को foo(input)एकल निर्देश पर डाउन करने के लिए अनुकूलित करते हुए देख सकते हैं addl %eax, %eax, लेकिन समय के बाहर ले जाने या निरंतर इनपुट के बावजूद इसे पूरी तरह से समाप्त करने के बिना।

आशा है कि यह मदद करता है, और C ++ मानक समिति DoNotOptimizeयहां एपीआई के समान मानकीकरण की संभावना देख रही है।


1
आपके उत्तर के लिए धन्यवाद। मैंने इसे नए सर्वोत्तम उत्तर के रूप में चिह्नित किया है। मैं ऐसा पहले भी कर सकता था, लेकिन मैंने कई महीनों तक इस स्टैकओवरफ्लो पेज को नहीं पढ़ा। मुझे C ++ प्रोग्राम बनाने के लिए क्लैंग कंपाइलर का उपयोग करने में बहुत दिलचस्पी है। अन्य बातों के अलावा, मुझे यह पसंद है कि कोई एक क्लिंक में चर नामों में यूनिकोड वर्णों का उपयोग कर सकता है। मुझे लगता है कि मैं स्टैकओवरफ़्लो पर क्लैंग के बारे में अधिक प्रश्न पूछूंगा।
S2108887

5
जबकि मैं समझता हूं कि यह फू को पूरी तरह से अनुकूलित करने से कैसे रोकता है, तो क्या आप थोड़ा विस्तार कर सकते हैं कि यह कॉल Clock::now()को फू के सापेक्ष पुन: व्यवस्थित होने से क्यों रोकता है ? क्या ऑप्टिमाइज़र को यह मान लेना है DoNotOptimizeऔर Clock::now()इसकी पहुँच किसी आम वैश्विक स्थिति को संशोधित कर सकती है, जो बदले में उन्हें इन और आउटपुट से बाँध देगी? या क्या आप अनुकूलक के कार्यान्वयन की कुछ वर्तमान सीमाओं पर निर्भर हैं?
मिकेम्बो

2
DoNotOptimizeइस उदाहरण में एक सिंथेटिक "अवलोकनीय" घटना है। यह ऐसा है जैसे यह इनपुट के प्रतिनिधित्व के साथ कुछ टर्मिनल के लिए दृश्यमान आउटपुट को मुद्रित करता है। चूँकि घड़ी पढ़ना भी अवलोकनीय है (आप समय पास कर रहे हैं) उन्हें कार्यक्रम के अवलोकनीय व्यवहार को बदले बिना फिर से आदेशित नहीं किया जा सकता है।
चैंडलर कारुथ

1
मैं अभी भी अवधारणा "अवलोकनीय" के साथ बिल्कुल स्पष्ट नहीं हूं, अगर fooफ़ंक्शन कुछ ऑपरेशन कर रहा है जैसे सॉकेट से पढ़ना जो थोड़ी देर के लिए अवरुद्ध हो सकता है, क्या यह एक नमूदार ऑपरेशन की गिनती करता है? और चूंकि read"पूरी तरह से ज्ञात" ऑपरेशन (दाएं?) नहीं है, क्या कोड क्रम में रहेगा?
राविनिसैडस्क

"मूलभूत समस्या यह है कि पूर्णांक जोड़ जैसी किसी चीज़ का परिचालन शब्दार्थ पूरी तरह से कार्यान्वयन के लिए जाना जाता है।" लेकिन मुझे लगता है कि यह मुद्दा पूर्णांक जोड़ के शब्दार्थ नहीं है, यह फ़ंक्शन फू () को कॉल करने का शब्दार्थ है। जब तक फू () एक ही संकलन इकाई में नहीं है, यह कैसे पता चलता है कि फू () और घड़ी () आपस में मेल नहीं खाते हैं?
डेव

59

सारांश:

लगता है कि रीरोडिंग को रोकने के लिए कोई गारंटीकृत तरीका नहीं है, लेकिन जब तक लिंक-टाइम / फुल-प्रोग्राम ऑप्टिमाइज़ेशन सक्षम नहीं होता है, तब तक एक अलग संकलन इकाई में कॉल किए गए फ़ंक्शन का पता लगाना काफी अच्छा दांव लगता है । (कम से कम जीसीसी के साथ, हालांकि तर्क यह सुझाव देगा कि यह अन्य संकलक के साथ भी होने की संभावना है।) यह फ़ंक्शन कॉल की लागत पर आता है - इन-कोड एक ही संकलन इकाई में परिभाषा द्वारा है और पुन: व्यवस्थित करने के लिए खुला है।

मूल उत्तर:

GCC ने कॉल -O2 ऑप्टिमाइज़ेशन को फिर से शुरू किया:

#include <chrono>
static int foo(int x)    // 'static' or not here doesn't affect ordering.
{
    return x*2;
}
int fred(int x)
{
    auto t1 = std::chrono::high_resolution_clock::now();
    int y = foo(x);
    auto t2 = std::chrono::high_resolution_clock::now();
    return y;
}

जीसीसी 5.3.0:

g++ -S --std=c++11 -O0 fred.cpp :

_ZL3fooi:
        pushq   %rbp
        movq    %rsp, %rbp
        movl    %ecx, 16(%rbp)
        movl    16(%rbp), %eax
        addl    %eax, %eax
        popq    %rbp
        ret
_Z4fredi:
        pushq   %rbp
        movq    %rsp, %rbp
        subq    $64, %rsp
        movl    %ecx, 16(%rbp)
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movq    %rax, -16(%rbp)
        movl    16(%rbp), %ecx
        call    _ZL3fooi
        movl    %eax, -4(%rbp)
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movq    %rax, -32(%rbp)
        movl    -4(%rbp), %eax
        addq    $64, %rsp
        popq    %rbp
        ret

परंतु:

g++ -S --std=c++11 -O2 fred.cpp :

_Z4fredi:
        pushq   %rbx
        subq    $32, %rsp
        movl    %ecx, %ebx
        call    _ZNSt6chrono3_V212system_clock3nowEv
        call    _ZNSt6chrono3_V212system_clock3nowEv
        leal    (%rbx,%rbx), %eax
        addq    $32, %rsp
        popq    %rbx
        ret

अब, बाहरी कार्य के रूप में फू () के साथ:

#include <chrono>
int foo(int x);
int fred(int x)
{
    auto t1 = std::chrono::high_resolution_clock::now();
    int y = foo(x);
    auto t2 = std::chrono::high_resolution_clock::now();
    return y;
}

g++ -S --std=c++11 -O2 fred.cpp :

_Z4fredi:
        pushq   %rbx
        subq    $32, %rsp
        movl    %ecx, %ebx
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movl    %ebx, %ecx
        call    _Z3fooi
        movl    %eax, %ebx
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movl    %ebx, %eax
        addq    $32, %rsp
        popq    %rbx
        ret

लेकिन, अगर इसे -flto (लिंक-टाइम ऑप्टिमाइज़ेशन) के साथ जोड़ा गया है:

0000000100401710 <main>:
   100401710:   53                      push   %rbx
   100401711:   48 83 ec 20             sub    $0x20,%rsp
   100401715:   89 cb                   mov    %ecx,%ebx
   100401717:   e8 e4 ff ff ff          callq  100401700 <__main>
   10040171c:   e8 bf f9 ff ff          callq  1004010e0 <_ZNSt6chrono3_V212system_clock3nowEv>
   100401721:   e8 ba f9 ff ff          callq  1004010e0 <_ZNSt6chrono3_V212system_clock3nowEv>
   100401726:   8d 04 1b                lea    (%rbx,%rbx,1),%eax
   100401729:   48 83 c4 20             add    $0x20,%rsp
   10040172d:   5b                      pop    %rbx
   10040172e:   c3                      retq

3
तो MSVC और ICC करता है। क्लैंग एकमात्र ऐसा है जो मूल अनुक्रम को संरक्षित करता है।
कोड़ी ग्रे

3
आप कहीं भी t1 और t2 का उपयोग नहीं करते हैं, इसलिए यह सोच सकता है कि परिणाम त्याग दिया जा सकता है और कोड को फिर से व्यवस्थित करें
phuclv

3
@ निल - मैं कुछ भी अधिक ठोस पेशकश नहीं कर सकता, लेकिन मुझे लगता है कि मेरी टिप्पणी अंतर्निहित कारण से संबंधित है: संकलक जानता है कि फू () अब प्रभावित नहीं कर सकता (), न ही इसके विपरीत, और इसलिए पुन: व्यवस्थित करता है। एक्सटर्नल स्कोप फ़ंक्शंस और डेटा से जुड़े विभिन्न प्रयोग इसकी पुष्टि करते हैं। इसमें स्थैतिक फू होना () फ़ाइल-स्कोप वैरिएबल N पर निर्भर करता है - यदि N को स्टैटिक के रूप में घोषित किया जाता है, तो रीऑर्डरिंग होता है, जबकि यदि इसे नॉन-स्टैटिक घोषित किया जाता है (अर्थात यह अन्य संकलन इकाइयों के लिए दृश्यमान है, और इसलिए संभावित रूप से साइड इफेक्ट के अधीन है। एक्सटर्नल फंक्शन जैसे कि अब ()) रीऑर्डर नहीं होता है।
जेरेमी

3
@ Lưu V Lnh Phúc: सिवाय इसके कि कॉल खुद को इलाज्ड नहीं हैं। एक बार फिर, मुझे लगता है इस वजह से संकलक क्या उनके साइड इफेक्ट हो सकता है पता नहीं है - लेकिन यह करता है पता है कि उन लोगों के साइड इफेक्ट foo के व्यवहार को प्रभावित नहीं कर सकते हैं ()।
जेरेमी

3
और एक अंतिम नोट: निर्दिष्ट करना -flto (लिंक-टाइम ऑप्टिमाइज़ेशन) अन्यथा गैर-पुन: व्यवस्थित मामलों में भी पुन: व्यवस्थित करने का कारण बनता है।
जेरेमी

20

कंपाइलर, या प्रोसेसर के द्वारा पुन: व्यवस्थित किया जा सकता है।

अधिकांश कंपाइलर रीड-राइट निर्देशों के पुन: व्यवस्थित होने से रोकने के लिए एक मंच-विशिष्ट विधि प्रदान करते हैं। जीसीसी पर, यह है

asm volatile("" ::: "memory");

( अधिक जानकारी यहाँ )

ध्यान दें कि यह केवल अप्रत्यक्ष रूप से संचालन को रोकता है, जब तक कि वे रीड / राइट पर निर्भर करते हैं।

व्यवहार में, मैंने अभी तक ऐसी प्रणाली नहीं देखी है, जहां इस Clock::now()तरह के अवरोध के रूप में सिस्टम कॉल का समान प्रभाव पड़ता है। आप सुनिश्चित होने के लिए परिणामी विधानसभा का निरीक्षण कर सकते हैं।

हालांकि, यह असामान्य नहीं है कि संकलन के समय परीक्षण के तहत कार्य का मूल्यांकन किया जाता है। "यथार्थवादी" निष्पादन को लागू करने के लिए, आपको foo()I / O या volatileरीड से इनपुट प्राप्त करने की आवश्यकता हो सकती है ।


एक और विकल्प होगा foo()- इनलाइन को फिर से निष्क्रिय करना, यह संकलक विशिष्ट है और आमतौर पर पोर्टेबल नहीं है, लेकिन इसका एक ही प्रभाव होगा।

जीसीसी पर, यह होगा __attribute__ ((noinline))


@Ruslan एक बुनियादी मुद्दा लाता है: यह माप कितना यथार्थवादी है?

निष्पादन समय कई कारकों से प्रभावित होता है: एक वह वास्तविक हार्डवेयर है जिस पर हम चल रहे हैं, दूसरा कैश, मेमोरी, डिस्क और सीपीयू कोर जैसे साझा संसाधनों तक समवर्ती पहुंच है।

तो हम आमतौर पर तुलनीय समय प्राप्त करने के लिए क्या करते हैं : सुनिश्चित करें कि वे कम त्रुटि मार्जिन के साथ प्रतिलिपि प्रस्तुत करने योग्य हैं । यह उन्हें कुछ हद तक कृत्रिम बनाता है।

"हॉट कैश" बनाम "कोल्ड कैश" निष्पादन प्रदर्शन आसानी से परिमाण के एक क्रम से भिन्न हो सकते हैं - लेकिन वास्तव में, यह कुछ इनबेटन ("गुनगुना") होगा?


2
asmटाइमर कॉल के बीच कथनों के निष्पादन समय को प्रभावित करने वाली आपकी हैक : मेमोरी क्लॉबर के बाद के कोड को मेमोरी से सभी वेरिएबल को फिर से लोड करना होता है।
रुस्लान

@ रोलन: उनकी हैक, मेरी नहीं। शुद्ध करने के विभिन्न स्तर हैं, और ऐसा कुछ करना प्रजनन योग्य परिणामों के लिए अपरिहार्य है।
पीटरचेन

2
ध्यान दें कि 'एएसएम' के साथ हैक केवल उन कार्यों के लिए एक बाधा के रूप में मदद करता है जो स्मृति को छूते हैं, और ओपी उससे अधिक में रुचि रखते हैं। अधिक जानकारी के लिए मेरा जवाब देखें।
चैंडलर कारुथ

11

C ++ भाषा परिभाषित करती है कि कई तरीकों से क्या देखने योग्य है।

अगर foo()कुछ भी देखने योग्य नहीं है, तो इसे पूरी तरह से समाप्त किया जा सकता है। यदि foo()केवल एक गणना करता है जो "स्थानीय" स्थिति में मूल्यों को संग्रहीत करता है (यह स्टैक पर या किसी ऑब्जेक्ट में कहीं भी हो), और संकलक साबित कर सकता है कि कोई भी सुरक्षित रूप से व्युत्पन्न सूचक Clock::now()कोड में नहीं मिल सकता है , तो कोई अवलोकन परिणाम नहीं हैं Clock::now()कॉल को स्थानांतरित करना।

यदि foo()किसी फ़ाइल या डिस्प्ले के साथ इंटरैक्ट किया जाता है, और कंपाइलर यह साबित नहीं कर सकता है कि फ़ाइल या डिस्प्ले के साथ इंटरैक्ट नहींClock::now() करता है , तो फिर से काम नहीं किया जा सकता है, क्योंकि फ़ाइल या डिस्प्ले के साथ इंटरैक्शन अवलोकनीय व्यवहार है।

जब आप संकलक-विशिष्ट हैक्स का उपयोग कोड को (इनलाइन असेंबली की तरह) इधर-उधर न करने के लिए मजबूर करने के लिए कर सकते हैं, तो एक और तरीका यह है कि आप अपने संकलक को बाहर करने का प्रयास करें।

गतिशील रूप से भरी हुई लाइब्रेरी बनाएँ। प्रश्न में कोड से पहले इसे लोड करें।

वह पुस्तकालय एक बात को उजागर करता है:

namespace details {
  void execute( void(*)(void*), void *);
}

और इसे इस तरह लपेटता है:

template<class F>
void execute( F f ) {
  struct bundle_t {
    F f;
  } bundle = {std::forward<F>(f)};

  auto tmp_f = [](void* ptr)->void {
    auto* pb = static_cast<bundle_t*>(ptr);
    (pb->f)();
  };
  details::execute( tmp_f, &bundle );
}

जो एक अशक्त लैम्ब्डा को पैक करता है और डायनेमिक लाइब्रेरी का उपयोग करके इसे एक संदर्भ में चलाता है जिसे कंपाइलर समझ नहीं सकता है।

डायनेमिक लाइब्रेरी के अंदर, हम करते हैं:

void details::execute( void(*f)(void*), void *p) {
  f(p);
}

जो बहुत सरल है।

अब कॉल को फिर से चालू करने के लिए execute, उसे डायनामिक लाइब्रेरी को समझना चाहिए, जिसे वह आपके परीक्षण कोड को संकलित करते समय नहीं कर सकता है।

यह अभी भी foo()शून्य साइड इफेक्ट्स के साथ समाप्त कर सकता है , लेकिन आप कुछ जीतते हैं, आप कुछ खो देते हैं।


19
"एक अन्य दृष्टिकोण आपके कंपाइलर को बाहर करने का प्रयास करना है" यदि यह वाक्यांश खरगोश के छेद से नीचे जाने का संकेत नहीं है, तो मुझे नहीं पता कि यह क्या है। :-)
कोडी ग्रे

1
मुझे लगता है कि यह नोट करने में मददगार हो सकता है कि कोड को निष्पादित करने के लिए ब्लॉक करने के लिए आवश्यक समय को "अवलोकन योग्य" व्यवहार नहीं माना जाता है, जिसे बनाए रखने के लिए संकलक की आवश्यकता होती है । यदि कोड के ब्लॉक को निष्पादित करने का समय "अवलोकन योग्य" था, तो प्रदर्शन अनुकूलन का कोई भी रूप स्वीकार्य नहीं होगा। हालांकि यह C और C ++ के लिए एक "कारण अवरोधक" को परिभाषित करने में मददगार होगा, जिसके लिए अवरोधक के बाद किसी भी कोड को निष्पादित करने के लिए एक संकलक की आवश्यकता होगी जब तक कि बाधा उत्पन्न होने से पहले सभी साइड-इफेक्ट्स उत्पन्न कोड [कोड] द्वारा नियंत्रित नहीं किए गए थे यह सुनिश्चित करना चाहता है कि डेटा पूरी तरह से ...
सुपरकैट

1
... हार्डवेयर कैश के माध्यम से प्रचारित करने के लिए हार्डवेयर-विशिष्ट साधनों का उपयोग करने की आवश्यकता होगी, लेकिन एक हार्डवेयर-विशिष्ट साधन जब तक सभी पोस्ट लिखे गए पूर्ण नहीं होते तब तक बिना किसी बाधा निर्देश के बेकार हो जाएगा यह सुनिश्चित करने के लिए कि सभी लंबित लेखन कंपाइलर द्वारा ट्रैक किए गए हैं। हार्डवेयर में पोस्ट किया जाना चाहिए इससे पहले कि यह सुनिश्चित करने के लिए कहा जाए कि सभी पोस्ट किए गए लेखन पूर्ण हैं।] मुझे ऐसा करने का कोई तरीका नहीं पता है कि किसी भी भाषा में डमी volatileएक्सेस का उपयोग किए बिना या बाहरी कोड पर कॉल करें।
सुपरकाट

4

नहीं, यह नहीं हो सकता। C ++ मानक के अनुसार [intro.execution]:

14 एक पूर्ण-अभिव्यक्ति के साथ जुड़े प्रत्येक मूल्य संगणना और साइड इफेक्ट का मूल्यांकन किया जाने वाले प्रत्येक पूर्ण संगणना और साइड इफेक्ट के साथ जुड़ा हुआ है।

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

ध्यान दें कि यदि लागू करने के नियम की शर्तों को यहां पूरा नहीं किया गया है। यह सोचना अनुचित है कि कोई भी संकलक यह साबित करने में सक्षम होगा कि सिस्टम समय को प्राप्त करने के लिए कॉल को पुन: व्यवस्थित करने से अवलोकन कार्यक्रम के व्यवहार पर असर नहीं पड़ेगा। यदि कोई ऐसी परिस्थिति थी जिसमें दो बार कॉल करने के लिए मनाया व्यवहार को बदलने के बिना समय को पुन: व्यवस्थित किया जा सकता है, तो यह वास्तव में एक संकलक का उत्पादन करने के लिए बहुत ही अयोग्य होगा जो एक कार्यक्रम का विश्लेषण करता है जिसमें पर्याप्त समझ के साथ यह निश्चितता के साथ अनुमान लगाने में सक्षम हो।


12
वहाँ अभी भी के रूप में अगर नियम है
एमएम

18
जब तक -नियम संकलक कोड को कुछ भी कर सकता है जब तक कि यह देखने योग्य व्यवहार को नहीं बदलता है। निष्पादन का समय अवलोकनीय नहीं है। इसलिए यह कोड की संख्या के विपरीत रेखाओं को फिर से चालू कर सकता है जब तक कि परिणाम समान होगा (अधिकांश संकलक समझदार काम करते हैं और समय कॉल को फिर से नहीं करते हैं, लेकिन इसकी आवश्यकता नहीं है)
Revolver_Ocelot

6
निष्पादन का समय अवलोकनीय नहीं है। यह काफी अजीब है। व्यावहारिक, गैर-तकनीकी दृष्टिकोण से, निष्पादन का समय (उर्फ "प्रदर्शन") बहुत ही अवलोकन योग्य है।
फ्रैडरिक हमीदी

3
इस बात पर निर्भर करता है कि आप समय को कैसे मापते हैं। मानक C ++ में कोड के कुछ निकाय को निष्पादित करने के लिए ली गई घड़ी चक्रों की संख्या को मापना संभव नहीं है।
पीटर

3
@dba तुम कुछ चीजें एक साथ मिला रहे हो। लिंकर अब Win16 अनुप्रयोगों को उत्पन्न नहीं कर सकता है, यह काफी हद तक सही है, लेकिन ऐसा इसलिए है क्योंकि उन्होंने उस प्रकार के बाइनरी को उत्पन्न करने के लिए समर्थन हटा दिया है। WIn16 ऐप पीई प्रारूप का उपयोग नहीं करते हैं। इसका मतलब यह नहीं है कि या तो कंपाइलर या लिंकर को एपीआई फ़ंक्शन के बारे में विशेष ज्ञान है। दूसरा मुद्दा रनटाइम लाइब्रेरी से संबंधित है। NT 4. पर चलने वाले बाइनरी को उत्पन्न करने के लिए MSVC के नवीनतम संस्करण को प्राप्त करने में कोई समस्या नहीं है, मैंने यह किया है। जैसे ही आप CRT में लिंक करने का प्रयास करते हैं, समस्या आती है, जो उपलब्ध फ़ंक्शन को कॉल करता है।
कोड़ी ग्रे

2

नहीं।

कभी-कभी, "जैसा-अगर" नियम से, बयानों को फिर से आदेश दिया जा सकता है। ऐसा इसलिए नहीं है क्योंकि वे तार्किक रूप से एक-दूसरे से स्वतंत्र हैं, बल्कि इसलिए कि स्वतंत्रता कार्यक्रम के शब्दार्थ में बदलाव किए बिना इस तरह के पुन: आदेश की अनुमति देती है।

एक सिस्टम कॉल को स्थानांतरित करना जो वर्तमान समय को प्राप्त करता है स्पष्ट रूप से उस स्थिति को संतुष्ट नहीं करता है। एक संकलक जो जानबूझकर या अनजाने में ऐसा करता है, गैर-आज्ञाकारी है और वास्तव में मूर्खतापूर्ण है।

सामान्य तौर पर, मैं किसी भी अभिव्यक्ति की उम्मीद नहीं करूंगा, जिसके परिणामस्वरूप सिस्टम कॉल "आक्रामक रूप से अनुकूलन करने वाले कंपाइलर" द्वारा "दूसरा-अनुमान लगाया गया" हो। यह सिर्फ इतना नहीं जानता कि सिस्टम कॉल क्या करता है।


5
मैं मानता हूं कि यह मूर्खतापूर्ण होगा, लेकिन मैंने इसे गैर-अनुरूप नहीं कहा । कंपाइलर को ज्ञान हो सकता है कि कंक्रीट सिस्टम पर सिस्टम क्या कहता है और यदि इसके दुष्प्रभाव हैं। मैं उम्मीद करूंगा कि कंपाइलर्स इस तरह के कॉल को केवल सामान्य उपयोग के मामले को कवर करने के लिए, बेहतर उपयोगकर्ता अनुभव के लिए अनुमति नहीं देंगे, इसलिए नहीं कि मानक इसे प्रतिबंधित करता है।
Revolver_Ocelot

4
@Revolver_Ocelot: प्रोग्राम के शब्दार्थ को बदलने वाले ऑप्टिमाइज़ेशन (ठीक है, कॉपी एलीशन के लिए सहेजें) मानक के अनुरूप हैं, चाहे आप सहमत हों या न हों।
ऑर्बिट

6
के तुच्छ मामले में राज्य के साथ बातचीत करने के लिए कोड के लिए कोई परिभाषित तरीके नहींint x = 0; clock(); x = y*2; clock(); हैं । C ++ मानक के तहत, यह जानना जरूरी नहीं है कि यह क्या करता है - यह स्टैक की जांच कर सकता है (और जब गणना होती है तो नोटिस करता है), लेकिन यह C ++ की समस्या नहीं हैclock()xclock()
यक्क - एडम नेवरामॉन्ट

5
याक की बात को और आगे ले जाने के लिए: यह सच है कि सिस्टम को फिर से आदेश देना है, ताकि पहले का परिणाम सौंपा जाए t2 और दूसरे को जाए t1, गैर-अनुरूपता और मूर्खतापूर्ण होगा यदि उन मूल्यों का उपयोग किया जाता है, तो यह उत्तर क्या याद करता है एक सिस्टम कॉल पर अन्य कंपाइलर कभी-कभी अन्य कोड को री-ऑर्डर कर सकते हैं। इस मामले में, बशर्ते यह जानता है कि क्या foo()करता है (उदाहरण के लिए क्योंकि इसमें इनबिल्ट है) और इसलिए वह (शिथिल रूप से बोलना) यह एक शुद्ध कार्य है तो इसे इधर-उधर कर सकते हैं।
स्टीव जेसोप

1
.. फिर से धीरे से बोल, यह इसलिए है क्योंकि वहाँ कोई गारंटी नहीं है कि वास्तविक कार्यान्वयन (अमूर्त मशीन नहीं है) y*yसिस्टम कॉल से पहले, केवल मनोरंजन के लिए गणना नहीं करेगा । इस बात की भी कोई गारंटी नहीं है कि वास्तविक क्रियान्वयन इस सट्टा गणना के परिणाम का उपयोग बाद में किसी भी बिंदु पर नहीं करेगा x, इसलिए कॉल के बीच कुछ भी नहीं कर रहा है clock()। जो कुछ भी इनलाइन फ़ंक्शन fooकरता है, वही करता है, बशर्ते इसका कोई साइड-इफेक्ट न हो और यह उस स्थिति पर निर्भर न हो जो इसके द्वारा परिवर्तित हो सकती है clock()
स्टीव जेसोप

0

noinline फ़ंक्शन + इनलाइन असेंबली ब्लैक बॉक्स + पूर्ण डेटा निर्भरता

यह https://stackoverflow.com/a/38025837/895245 पर आधारित है, लेकिन क्योंकि मैंने कोई स्पष्ट औचित्य नहीं देखा है कि क्यों::now() वहाँ पुन: व्यवस्थित नहीं किया जा सकता है, तो मैं पागल हो जाऊंगा और इसे एक साथ एक noinline के अंदर रख दूंगा एएसएम।

इस तरह से मुझे पूरा यकीन है कि noinline"संबंध" के बाद से पुन: व्यवस्थित नहीं हो सकता है::now डेटा निर्भरता करता है।

main.cpp

#include <chrono>
#include <iostream>
#include <string>

// noinline ensures that the ::now() cannot be split from the __asm__
template <class T>
__attribute__((noinline)) auto get_clock(T& value) {
    // Make the compiler think we actually use / modify the value.
    // It can't "see" what is going on inside the assembly string.
    __asm__ __volatile__ ("" : "+g" (value));
    return std::chrono::high_resolution_clock::now();
}

template <class T>
static T foo(T niters) {
    T result = 42;
    for (T i = 0; i < niters; ++i) {
        result = (result * result) - (3 * result) + 1;
    }
    return result;
}

int main(int argc, char **argv) {
    unsigned long long input;
    if (argc > 1) {
        input = std::stoull(argv[1], NULL, 0);
    } else {
        input = 1;
    }

    // Must come before because it could modify input
    // which is passed as a reference.
    auto t1 = get_clock(input);
    auto output = foo(input);
    // Must come after as it could use the output.
    auto t2 = get_clock(output);
    std::cout << "output " << output << std::endl;
    std::cout << "time (ns) "
              << std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1).count()
              << std::endl;
}

गिटहब ऊपर

संकलित करें और चलाएं:

g++ -ggdb3 -O3 -std=c++14 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out 1000
./main.out 10000
./main.out 100000

इस पद्धति का एकमात्र मामूली पहलू यह है कि हम एक अतिरिक्त जोड़ते हैं callq एक inlineविधि पर निर्देश । objdump -CDशो जिसमें mainशामिल हैं:

    11b5:       e8 26 03 00 00          callq  14e0 <auto get_clock<unsigned long long>(unsigned long long&)>
    11ba:       48 8b 34 24             mov    (%rsp),%rsi
    11be:       48 89 c5                mov    %rax,%rbp
    11c1:       b8 2a 00 00 00          mov    $0x2a,%eax
    11c6:       48 85 f6                test   %rsi,%rsi
    11c9:       74 1a                   je     11e5 <main+0x65>
    11cb:       31 d2                   xor    %edx,%edx
    11cd:       0f 1f 00                nopl   (%rax)
    11d0:       48 8d 48 fd             lea    -0x3(%rax),%rcx
    11d4:       48 83 c2 01             add    $0x1,%rdx
    11d8:       48 0f af c1             imul   %rcx,%rax
    11dc:       48 83 c0 01             add    $0x1,%rax
    11e0:       48 39 d6                cmp    %rdx,%rsi
    11e3:       75 eb                   jne    11d0 <main+0x50>
    11e5:       48 89 df                mov    %rbx,%rdi
    11e8:       48 89 44 24 08          mov    %rax,0x8(%rsp)
    11ed:       e8 ee 02 00 00          callq  14e0 <auto get_clock<unsigned long long>(unsigned long long&)>

इसलिए हम देखते हैं कि fooइनबिल्ट था, लेकिनget_clock इसके आसपास नहीं थे।

get_clock हालांकि यह बेहद कुशल है, इसमें सिंगल लीफ कॉल अनुकूलित निर्देश शामिल है जो स्टैक को छूता भी नहीं है:

00000000000014e0 <auto get_clock<unsigned long long>(unsigned long long&)>:
    14e0:       e9 5b fb ff ff          jmpq   1040 <std::chrono::_V2::system_clock::now()@plt>

चूंकि घड़ी की सटीकता स्वयं सीमित है, मुझे लगता है कि यह संभावना नहीं है कि आप एक अतिरिक्त समय के प्रभावों को नोटिस कर पाएंगे jmpq । ध्यान दें कि एक साझा पुस्तकालय में callहोने के बावजूद किसी की आवश्यकता ::now()है।

::now()इनलाइन असेंबली से डेटा निर्भरता के साथ कॉल करें

यह सबसे कुशल समाधान संभव होगा, जो jmpqकि ऊपर उल्लिखित अतिरिक्त पर भी निर्भर करता है।

यह दुर्भाग्य से सही ढंग से करने के लिए बेहद कठिन है जैसा कि यहां दिखाया गया है: विस्तारित इनलाइन एएसएम में प्रिंटिंग कॉलिंग

यदि आपका समय माप सीधे इनलाइन असेंबली में बिना किसी कॉल के किया जा सकता है, तो इस तकनीक का उपयोग किया जा सकता है। यह उदाहरण के लिए gem5 मैजिक इंस्ट्रूमेंटेशन निर्देशों के लिए मामला है , 86 RDTSC (यकीन नहीं करता है, तो यह अब और प्रतिनिधि है) और संभवतः अन्य प्रदर्शन काउंटरों।

संबंधित सूत्र:

जीसीसी 8.3.0, उबंटू 19.04 के साथ परीक्षण किया गया।


1
आपको आमतौर पर एक स्पिल / रीलोड के साथ मजबूर करने की आवश्यकता नहीं होती है "+m", "+r"कंपाइलर को एक मूल्य के रूप में उपयोग करने के लिए एक बहुत अधिक कुशल तरीका है और फिर मान लें कि परिवर्तनशील बदल गया है।
पीटर कॉर्ड्स
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.