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
कार्यान्वयन के लिए पूरी तरह से दिखाई देने वाली परिभाषा के साथ । मैंने DoNotOptimize
Google बेंचमार्क लाइब्रेरी से एक (गैर-पोर्टेबल) संस्करण भी निकाला है, जिसे आप यहां पा सकते हैं: 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
यहां एपीआई के समान मानकीकरण की संभावना देख रही है।