X86_64 असेंबली में एक बेकार लूप को बेकार एमओवी निर्देशों को पेश करने में तेजी क्यों आएगी?


222

पृष्ठभूमि:

एम्बेडेड विधानसभा भाषा के साथ कुछ पास्कल कोड का अनुकूलन करते हुए , मैंने एक अनावश्यक MOVनिर्देश पर ध्यान दिया, और इसे हटा दिया।

मेरे आश्चर्य के लिए, गैर-आवश्यक निर्देश को हटाने से मेरा कार्यक्रम धीमा हो गया

मैंने पाया कि मनमाने ढंग से अनुपयोगी MOVनिर्देशों को जोड़ने से प्रदर्शन और भी बढ़ गया।

प्रभाव अनिश्चित है, और निष्पादन आदेश के आधार पर परिवर्तन: एक ही कबाड़ निर्देश स्थानांतरित एक भी लाइन द्वारा ऊपर और नीचे या एक मंदी का उत्पादन

मैं समझता हूं कि सीपीयू सभी प्रकार के अनुकूलन और सुव्यवस्थित करता है, लेकिन, यह काला जादू अधिक लगता है।

आँकड़े:

मेरे कोड का एक संस्करण सशर्त रूप से एक लूप के बीच में तीन जंक संचालन को संकलित करता है जो 2**20==1048576कई बार चलता है। (आसपास का कार्यक्रम सिर्फ SHA-256 हैश की गणना करता है)।

मेरी बल्कि पुरानी मशीन (Intel (R) Core (TM) 2 CPU 6400 @ 2.13 GHz) पर परिणाम:

avg time (ms) with -dJUNKOPS: 1822.84 ms
avg time (ms) without:        1836.44 ms

प्रोग्राम को लूप में 25 बार चलाया गया था, रन ऑर्डर हर बार बेतरतीब ढंग से बदल रहा है।

अंश:

{$asmmode intel}
procedure example_junkop_in_sha256;
  var s1, t2 : uint32;
  begin
    // Here are parts of the SHA-256 algorithm, in Pascal:
    // s0 {r10d} := ror(a, 2) xor ror(a, 13) xor ror(a, 22)
    // s1 {r11d} := ror(e, 6) xor ror(e, 11) xor ror(e, 25)
    // Here is how I translated them (side by side to show symmetry):
  asm
    MOV r8d, a                 ; MOV r9d, e
    ROR r8d, 2                 ; ROR r9d, 6
    MOV r10d, r8d              ; MOV r11d, r9d
    ROR r8d, 11    {13 total}  ; ROR r9d, 5     {11 total}
    XOR r10d, r8d              ; XOR r11d, r9d
    ROR r8d, 9     {22 total}  ; ROR r9d, 14    {25 total}
    XOR r10d, r8d              ; XOR r11d, r9d

    // Here is the extraneous operation that I removed, causing a speedup
    // s1 is the uint32 variable declared at the start of the Pascal code.
    //
    // I had cleaned up the code, so I no longer needed this variable, and 
    // could just leave the value sitting in the r11d register until I needed
    // it again later.
    //
    // Since copying to RAM seemed like a waste, I removed the instruction, 
    // only to discover that the code ran slower without it.
    {$IFDEF JUNKOPS}
    MOV s1,  r11d
    {$ENDIF}

    // The next part of the code just moves on to another part of SHA-256,
    // maj { r12d } := (a and b) xor (a and c) xor (b and c)
    mov r8d,  a
    mov r9d,  b
    mov r13d, r9d // Set aside a copy of b
    and r9d,  r8d

    mov r12d, c
    and r8d, r12d  { a and c }
    xor r9d, r8d

    and r12d, r13d { c and b }
    xor r12d, r9d

    // Copying the calculated value to the same s1 variable is another speedup.
    // As far as I can tell, it doesn't actually matter what register is copied,
    // but moving this line up or down makes a huge difference.
    {$IFDEF JUNKOPS}
    MOV s1,  r9d // after mov r12d, c
    {$ENDIF}

    // And here is where the two calculated values above are actually used:
    // T2 {r12d} := S0 {r10d} + Maj {r12d};
    ADD r12d, r10d
    MOV T2, r12d

  end
end;

इसे स्वयं आज़माएं:

यदि आप इसे स्वयं आज़माना चाहते हैं, तो कोड GitHub पर ऑनलाइन है ।

मेरे सवाल:

  • रैम के लिए किसी रजिस्टर की सामग्री की बेकार नकल करने से प्रदर्शन में वृद्धि क्यों होगी ?
  • क्यों एक ही बेकार निर्देश कुछ लाइनों पर एक गति प्रदान करता है, और दूसरों पर मंदी?
  • क्या यह व्यवहार कुछ ऐसा है जिसका कंपाइलर द्वारा अनुमान लगाया जा सकता है?

7
सभी प्रकार के 'बेकार' निर्देश हैं जो वास्तव में निर्भरता श्रृंखला को तोड़ने, सेवानिवृत्त के रूप में भौतिक रजिस्टरों को चिह्नित करने आदि के लिए काम कर सकते हैं। इन ऑपरेशनों का शोषण करने के लिए माइक्रोआर्किटेक्चर के कुछ ज्ञान की आवश्यकता होती है । आपके प्रश्न को एक न्यूनतम उदाहरण के रूप में निर्देशों का एक छोटा अनुक्रम प्रदान करना चाहिए, न कि लोगों को गीथब को निर्देशित करने के बजाय।
ब्रेट हेल

1
@BrettHale अच्छी बात है, धन्यवाद। मैंने कुछ टिप्पणी के साथ एक अंश जोड़ा। क्या रजिस्टर के मान को कॉपी करने के लिए रैम को रजिस्टर को सेवानिवृत्त के रूप में चिह्नित करना होगा, भले ही उसमें मूल्य का उपयोग बाद में किया जाए?
tangentstorm

9
क्या आप उन औसत पर मानक विचलन डाल सकते हैं? इस पोस्ट में कोई वास्तविक संकेत नहीं है कि वास्तविक अंतर है।
17

2
क्या आप कृपया rdtscp इंस्ट्रक्शन का उपयोग करके निर्देशों को टाइम करने की कोशिश कर सकते हैं, और दोनों संस्करणों के लिए घड़ी के चक्र की जांच कर सकते हैं?
जकोबॉट्स

2
क्या यह मेमोरी संरेखण के कारण भी हो सकता है? मैंने स्वयं गणित नहीं किया (आलसी: पी) लेकिन कुछ डमी निर्देशों को जोड़ने से आपके कोड को स्मृति संरेखित किया जा सकता है ...
लोरेंजो डेमेटे

जवाबों:


144

गति में सुधार का सबसे संभावित कारण यह है कि:

  • एक MOV डालने से बाद के निर्देश अलग-अलग मेमोरी एड्रेस में शिफ्ट हो जाते हैं
  • उन स्थानांतरित निर्देशों में से एक एक महत्वपूर्ण सशर्त शाखा थी
  • शाखा की भविष्यवाणी तालिका में अलियासिंग के कारण उस शाखा का गलत अनुमान लगाया जा रहा था
  • शाखा को स्थानांतरित करने से उपनाम समाप्त हो गया और शाखा को सही ढंग से भविष्यवाणी करने की अनुमति दी गई

आपका Core2 प्रत्येक सशर्त कूद के लिए एक अलग इतिहास रिकॉर्ड नहीं रखता है। इसके बजाय यह सभी सशर्त छलांगों का साझा इतिहास रखता है। वैश्विक शाखा की भविष्यवाणी का एक नुकसान यह है कि अगर विभिन्न सशर्त छलांग असंबंधित हैं, तो इतिहास अप्रासंगिक जानकारी से पतला है।

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

यदि आप यह समझना चाहते हैं कि शाखा के प्रदर्शन कैसे प्रभावित करते हैं, तो इस उत्कृष्ट उत्तर पर एक नज़र डालें: https://stackoverflow.com/a/11227902/1001643

कंपाइलर्स के पास आमतौर पर यह जानने के लिए पर्याप्त जानकारी नहीं होती है कि कौन सी शाखाएँ अलग-अलग होंगी और क्या वे उपनाम महत्वपूर्ण होंगे। हालाँकि, उस जानकारी को Cachegrind और VTune जैसे टूल के साथ रनटाइम पर निर्धारित किया जा सकता है ।


2
हम्म। यह आशाजनक लगता है। इस sha256 कार्यान्वयन में केवल सशर्त शाखाएं फॉर लूप की समाप्ति के लिए चेक हैं। उस समय, मैंने इस संशोधन को गिट में एक विषमता के रूप में टैग किया था और अनुकूलन जारी रखा। मेरा अगला कदम विधानसभा में पास्कल फॉर लूप को फिर से लिखना था, जिस बिंदु पर इन अतिरिक्त निर्देशों का अब सकारात्मक प्रभाव नहीं था। शायद मुक्त पास्कल का उत्पन्न कोड प्रोसेसर के लिए उस सरल काउंटर की तुलना में भविष्यवाणी करना कठिन था जो मैंने इसे बदल दिया था।
स्पर्शरेखा

1
@tangentstorm यह एक अच्छे सारांश की तरह लगता है। शाखा भविष्यवाणी तालिका बहुत बड़ी नहीं है, इसलिए एक तालिका प्रविष्टि एक से अधिक शाखा को संदर्भित कर सकती है। यह कुछ भविष्यवाणियों को बेकार कर सकता है। समस्या आसानी से तय हो जाती है अगर एक परस्पर विरोधी शाखाएं तालिका के दूसरे भाग में चली जाती हैं। लगभग कोई भी छोटा सा बदलाव इसे कर सकता है :-)
रेमंड हेटिंगर

1
मुझे लगता है कि यह मेरे द्वारा देखे गए विशिष्ट व्यवहार का सबसे उचित स्पष्टीकरण है, इसलिए मैं इसे उत्तर के रूप में चिह्नित करने जा रहा हूं। धन्यवाद। :)
tangentstorm

3
इसी तरह की समस्या की एक बहुत ही उत्कृष्ट चर्चा है कि बोच में योगदान करने वालों में से एक ने भाग लिया, आप इसे अपने उत्तर में जोड़ना चाह सकते हैं: emulators.com/docs/nx25_nostradamus.htm
leander

3
केवल शाखा लक्ष्य की तुलना में बहुत अधिक के लिए इन्सान संरेखण मायने रखता है। Decode अड़चनें Core2 और Nehalem के लिए एक बहुत बड़ा मुद्दा है: अक्सर इसकी निष्पादन इकाइयों को व्यस्त रखने में एक कठिन समय होता है। सैंडब्रिज की यूओपी कैश की शुरूआत ने थ्रूपुट की एक बड़ी राशि को बढ़ाया। इस समस्या के कारण शाखा लक्ष्य लक्षित किया जाता है , लेकिन यह सभी कोड को प्रभावित करता है।
पीटर कॉर्ड्स

80

आप http://research.google.com/pubs/pub37077.html पढ़ना चाह सकते हैं

टीएल; डीआर: कार्यक्रमों में बेतरतीब ढंग से एनओपी निर्देश डालने से प्रदर्शन में आसानी से 5% या उससे अधिक की वृद्धि हो सकती है, और नहीं, कंपाइलर आसानी से इसका फायदा नहीं उठा सकते हैं। यह आमतौर पर शाखा भविष्यवक्ता और कैश व्यवहार का एक संयोजन है, लेकिन यह सिर्फ एक आरक्षण स्टेशन स्टाल हो सकता है (यहां तक ​​कि कोई निर्भरता श्रृंखला नहीं है जो टूटी हुई हैं या स्पष्ट संसाधन ओवर-सब्सक्रिप्शन जो भी हो)।


1
दिलचस्प। लेकिन क्या प्रोसेसर (या एफपीसी) यह देखने के लिए पर्याप्त स्मार्ट है कि राम को लिखना इस मामले में एनओपी है?
tangentstorm

8
कोडांतरक अनुकूलित नहीं है।
मार्को वैन डे वोइट

5
संकलक बार-बार निर्माण और प्रोफाइलिंग जैसी अविश्वसनीय रूप से महंगी अनुकूलन करके और फिर संकलित annealing या आनुवंशिक एल्गोरिथ्म के साथ संकलक आउटपुट को अलग करके कंपाइलर इसका फायदा उठा सकते हैं। मैंने उस क्षेत्र में कुछ काम के बारे में पढ़ा है। लेकिन हम कम से कम 5-10 मिनट के 100% सीपीयू को संकलित करने के लिए बात कर रहे हैं, और परिणामी अनुकूलन संभवतः सीपीयू कोर मॉडल और यहां तक ​​कि कोर या माइक्रोकोड संशोधन विशिष्ट होंगे।
एडमरिमेनको

मैं इसे यादृच्छिक एनओपी फोन नहीं होता था, ये स्पष्टीकरण दें कि NOPs प्रदर्शन (tl; डॉ: पर सकारात्मक प्रभाव हो सकता है stackoverflow.com/a/5901856/357198 ) और एनओपी के यादृच्छिक प्रविष्टि के प्रदर्शन गिरावट में परिणाम था। कागज की दिलचस्प बात यह है कि जीसीसी द्वारा 'रणनीतिक' एनओपी को हटाने से प्रदर्शन पर कोई प्रभाव नहीं पड़ा है!
PuercoPop

15

मुझे लगता है कि आधुनिक सीपीयू विधानसभा निर्देशों को मानते हैं, जबकि सीपीयू को निष्पादन निर्देश प्रदान करने के लिए एक प्रोग्रामर को अंतिम दृश्यमान परत, वास्तव में सीपीयू द्वारा वास्तविक निष्पादन से कई परतें हैं।

आधुनिक सीपीयू RISC / CISC संकर हैं जो CISC x86 निर्देशों को आंतरिक निर्देशों में अनुवादित करते हैं जो व्यवहार में अधिक RISC हैं। इसके अतिरिक्त, आउट-ऑफ-ऑर्डर निष्पादन विश्लेषक, शाखा भविष्यवक्ता, इंटेल के "माइक्रो-ऑप्स फ्यूजन" हैं जो एक साथ काम के बड़े बैचों में निर्देश देने की कोशिश करते हैं (जैसे वीएलआईडब्ल्यू / इटेनियम टाइटैनिक)। यहां तक ​​कि कैश सीमाएं भी हैं जो कोड को ईश्वर के लिए तेजी से चला सकती हैं-क्यों-यदि यह बड़ा है (शायद कैश नियंत्रक इसे अधिक बुद्धिमानी से स्लॉट करता है, या इसे लंबे समय तक बनाए रखता है)।

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

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


6
आपका उत्तर भ्रमित करने वाला है। कई जगहों पर ऐसा लगता है जैसे आप अनुमान लगा रहे हैं, हालाँकि आप जो कहते हैं उसमें से अधिकांश सही है।
अल्काद्रदो

2
शायद मुझे स्पष्ट करना चाहिए। मुझे जो भ्रामक लगता है वह निश्चितता की कमी है
अल्काद्रदो

3
यह अनुमान लगाना कि समझदारी और अच्छे तर्क के साथ यह पूरी तरह से मान्य है।
jturolla

7
कोई भी व्यक्ति वास्तव में यह सुनिश्चित करने के लिए नहीं जान सकता है कि ओपी इस अजीब व्यवहार को क्यों देख रहा है, जब तक कि यह इंटेल का एक इंजीनियर नहीं था जिसके पास विशेष नैदानिक ​​उपकरण तक पहुंच थी। तो बाकी सभी अनुमान लगा सकते हैं। यह @ चरवाहे का दोष नहीं है।
एलेक्स डी।

2
downvote; आपके कहे अनुसार कोई भी व्यवहार ओपी देख रहा है। आपका जवाब बेकार है।
फ़ूज

0

कैश तैयार करना

मेमोरी में मूवमेंट को कैशे तैयार कर सकते हैं और बाद के मूव ऑपरेशन को तेज कर सकते हैं। एक सीपीयू में आमतौर पर दो लोड यूनिट और एक स्टोर यूनिट होती है। एक लोड यूनिट मेमोरी से एक रजिस्टर (प्रति चक्र एक बार पढ़ा जाता है), एक स्टोर यूनिट रजिस्टर से मेमोरी में पढ़ सकता है। अन्य इकाइयाँ भी हैं जो रजिस्टरों के बीच संचालन करती हैं। सभी इकाइयाँ समानांतर में काम करती हैं। इसलिए, प्रत्येक चक्र पर, हम एक ही बार में कई ऑपरेशन कर सकते हैं, लेकिन दो से अधिक भार, एक स्टोर और कई रजिस्टर ऑपरेशन नहीं। आमतौर पर यह सादे रजिस्टरों के साथ 4 सरल ऑपरेशन तक, एक्सएमएम / वाईएमएम रजिस्टरों के साथ 3 सरल ऑपरेशन और किसी भी तरह के रजिस्टरों के साथ 1-2 जटिल ऑपरेशन तक होता है। आपके कोड में रजिस्टरों के साथ बहुत सारे ऑपरेशन हैं, इसलिए एक डमी मेमोरी स्टोर ऑपरेशन नि: शुल्क है (क्योंकि वैसे भी 4 से अधिक रजिस्टर ऑपरेशन हैं), लेकिन यह मेमोरी स्टोर को बाद के स्टोर ऑपरेशन के लिए तैयार करता है। यह जानने के लिए कि मेमोरी स्टोर कैसे काम करते हैं, कृपया देखेंइंटेल 64 और IA-32 आर्किटेक्चर ऑप्टिमाइज़ेशन संदर्भ मैनुअल

झूठी निर्भरता को तोड़ना

हालांकि यह आपके मामले को बिल्कुल संदर्भित नहीं करता है, लेकिन कभी-कभी 64-बिट प्रोसेसर (जैसा कि आपके मामले में है) के तहत 32-बिट mov संचालन का उपयोग उच्च बिट्स (32-63) को खाली करने और निर्भरता श्रृंखलाओं को तोड़ने के लिए किया जाता है।

यह अच्छी तरह से ज्ञात है कि x86-64 के तहत, 32-बिट ऑपरेंड का उपयोग करके 64-बिट रजिस्टर के उच्च बिट्स को साफ करता है। दलीलों ने संबंधित खंड - 3.4.1.1 - Intel® 64 और IA-32 आर्किटेक्चर सॉफ्टवेयर डेवलपर के मैनुअल वॉल्यूम 1 को पढ़ा : 1

32-बिट ऑपरेंड्स 32-बिट परिणाम उत्पन्न करते हैं, गंतव्य सामान्य-उद्देश्य रजिस्टर में 64-बिट परिणाम के लिए शून्य-विस्तारित

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

Intel® 64 और IA-32 आर्किटेक्चर ऑप्टिमाइज़ेशन संदर्भ मैनुअल से एक उद्धरण , खंड 3.5.1.8:

आंशिक अनुक्रम को संशोधित करने वाले कोड अनुक्रम इसकी निर्भरता श्रृंखला में कुछ देरी का अनुभव कर सकते हैं, लेकिन निर्भरता तोड़ने वाले मुहावरों का उपयोग करके इसे टाला जा सकता है। इंटेल कोर माइक्रो-आर्किटेक्चर पर आधारित प्रोसेसर में, कई निर्देश स्पष्ट निष्पादन निर्भरता में मदद कर सकते हैं, जब सॉफ्टवेयर इन निर्देशों का उपयोग करके रजिस्टर सामग्री को शून्य करने के लिए उपयोग करता है। आंशिक रजिस्टरों के बजाय 32-बिट रजिस्टरों पर काम करके निर्देशों के बीच रजिस्टरों के कुछ हिस्सों पर निर्भरता को तोड़ें। चाल के लिए, यह 32-बिट चाल के साथ या MOVZX का उपयोग करके पूरा किया जा सकता है।

असेंबली / कंपाइलर कोडिंग नियम 37. (एम प्रभाव, एमएच सामान्यता) : आंशिक रजिस्टरों के बजाय 32-बिट रजिस्टरों पर काम करके निर्देशों के बीच रजिस्टरों के कुछ हिस्सों पर निर्भरता को तोड़ें। चाल के लिए, यह 32-बिट चाल के साथ या MOVZX का उपयोग करके पूरा किया जा सकता है।

X64 के लिए 32-बिट ऑपरेंड के साथ MOVZX और MOV समतुल्य हैं - ये सभी निर्भरता श्रृंखलाओं को तोड़ते हैं।

इसलिए आपका कोड तेजी से निष्पादित होता है। यदि कोई निर्भरता नहीं हैं, तो सीपीयू आंतरिक रूप से रजिस्टरों का नाम बदल सकता है, भले ही पहली नजर में ऐसा लग सकता है कि दूसरा निर्देश पहले निर्देश द्वारा उपयोग किए गए रजिस्टर को संशोधित करता है, और दो समानांतर में निष्पादित नहीं कर सकते हैं। लेकिन पंजीकरण नाम बदलने के कारण वे कर सकते हैं।

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

मुझे लगता है कि अब आप देखते हैं कि यह बहुत स्पष्ट है।


यह सब सच है, लेकिन प्रश्न में प्रस्तुत कोड से इसका कोई लेना-देना नहीं है।
कोड़ी ग्रे

@CodyGray - आपकी प्रतिक्रिया के लिए धन्यवाद। मैंने उत्तर को संपादित किया है और मामले के बारे में एक अध्याय जोड़ा है - रजिस्टर ऑपरेशन से घिरी हुई मेमोरी कैश को तैयार करती है और यह तब से मुफ़्त है क्योंकि स्टोर यूनिट वैसे भी बेकार है। तो बाद में स्टोर ऑपरेशन तेज हो जाएगा।
मैक्सिम मासियूटिन

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.