क्यों एक सॉर्ट किए गए सरणी को अनसोल्ड सरणी को संसाधित करने की तुलना में तेज़ी से संसाधित कर रहा है?


24440

यहाँ C ++ कोड का एक टुकड़ा है जो कुछ बहुत अजीब व्यवहार दिखाता है। किसी अजीब कारण के लिए, डेटा को चमत्कारिक ढंग से छांटने से कोड लगभग छह गुना तेज हो जाता है:

#include <algorithm>
#include <ctime>
#include <iostream>

int main()
{
    // Generate data
    const unsigned arraySize = 32768;
    int data[arraySize];

    for (unsigned c = 0; c < arraySize; ++c)
        data[c] = std::rand() % 256;

    // !!! With this, the next loop runs faster.
    std::sort(data, data + arraySize);

    // Test
    clock_t start = clock();
    long long sum = 0;

    for (unsigned i = 0; i < 100000; ++i)
    {
        // Primary loop
        for (unsigned c = 0; c < arraySize; ++c)
        {
            if (data[c] >= 128)
                sum += data[c];
        }
    }

    double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;

    std::cout << elapsedTime << std::endl;
    std::cout << "sum = " << sum << std::endl;
}
  • बिना std::sort(data, data + arraySize);, कोड 11.54 सेकंड में चलता है।
  • सॉर्ट किए गए डेटा के साथ, कोड 1.93 सेकंड में चलता है।

प्रारंभ में, मुझे लगा कि यह सिर्फ एक भाषा या संकलक विसंगति हो सकती है, इसलिए मैंने जावा की कोशिश की:

import java.util.Arrays;
import java.util.Random;

public class Main
{
    public static void main(String[] args)
    {
        // Generate data
        int arraySize = 32768;
        int data[] = new int[arraySize];

        Random rnd = new Random(0);
        for (int c = 0; c < arraySize; ++c)
            data[c] = rnd.nextInt() % 256;

        // !!! With this, the next loop runs faster
        Arrays.sort(data);

        // Test
        long start = System.nanoTime();
        long sum = 0;

        for (int i = 0; i < 100000; ++i)
        {
            // Primary loop
            for (int c = 0; c < arraySize; ++c)
            {
                if (data[c] >= 128)
                    sum += data[c];
            }
        }

        System.out.println((System.nanoTime() - start) / 1000000000.0);
        System.out.println("sum = " + sum);
    }
}

एक समान लेकिन कम चरम परिणाम के साथ।


मेरा पहला विचार यह था कि सॉर्टिंग डेटा को कैश में लाता है, लेकिन फिर मैंने सोचा कि यह मूर्खतापूर्ण था क्योंकि सरणी अभी उत्पन्न हुई थी।

  • क्या हो रहा है?
  • क्यों एक सॉर्ट किए गए सरणी को अनसोल्ड सरणी को संसाधित करने की तुलना में तेज़ी से संसाधित कर रहा है?

कोड कुछ स्वतंत्र शब्दों को समेटता है, इसलिए आदेश को कोई फर्क नहीं पड़ता।



15
@SachinVerma मेरे सिर के ऊपर से: 1) जेवीएम आखिरकार सशर्त चालों का उपयोग करने के लिए पर्याप्त स्मार्ट हो सकता है। 2) कोड मेमोरी-बाउंड है। CPU कैश में फिट होने के लिए 200M बहुत बड़ा है। इसलिए प्रदर्शन ब्रांचिंग के बजाय मेमोरी बैंडविड्थ द्वारा टोंटी जाएगा।
रहस्यमयी

11
@ रहस्यवादी, लगभग 2)। मुझे लगा कि भविष्यवाणी तालिका पैटर्न (उस पैटर्न के लिए जाँच की गई वास्तविक चर के बावजूद) पर नज़र रखती है और इतिहास के आधार पर भविष्यवाणी उत्पादन में बदलाव करती है। क्या आप कृपया मुझे एक कारण दे सकते हैं, क्यों एक बड़े बड़े सरणी शाखा भविष्यवाणी से लाभ नहीं होगा?
सचिन वर्मा

14
@SachinVerma यह करता है, लेकिन जब सरणी यह ​​है कि बड़े, एक भी बड़ा कारक संभावना खेल में आता है - स्मृति बैंडविड्थ। स्मृति समतल नहीं है । मेमोरी तक पहुंच बहुत धीमी है, और सीमित मात्रा में बैंडविड्थ है। चीजों को सरल बनाने के लिए, केवल कुछ बाइट्स हैं जो एक निश्चित समय में सीपीयू और मेमोरी के बीच स्थानांतरित हो सकते हैं। इस प्रश्न में एक जैसा सरल कोड शायद उस सीमा को प्रभावित करेगा भले ही यह गलतफहमी से धीमा हो। यह 32768 (128KB) के सरणी के साथ नहीं होता है क्योंकि यह CPU के L2 कैश में फिट होता है।
रहस्यपूर्ण

11
एक नया सुरक्षा दोष है जिसे BranchScope कहा जाता है: cs.ucr.edu/~nael/pubs/asplos18.pdf
Veve

जवाबों:


31783

आप शाखा की असफलता के शिकार हैं।


शाखा भविष्यवाणी क्या है?

एक रेल जंक्शन पर विचार करें:

छवि एक रेल जंक्शन दिखा रही है विकिमीडिया कॉमन्स के माध्यम से मेकानिज्म द्वारा छविCC-by-SA 3.0 लाइसेंस के तहत उपयोग किया जाता है ।

अब तर्क के लिए मान लीजिए कि यह 1800 के दशक में वापस आ गया है - लंबी दूरी या रेडियो संचार से पहले।

आप एक जंक्शन के संचालक हैं और आप एक ट्रेन को आते हुए सुनते हैं। आपको पता नहीं है कि किस रास्ते से जाना है। आप ड्राइवर को यह पूछने के लिए ट्रेन रोकते हैं कि उन्हें कौन सी दिशा चाहिए। और फिर आप स्विच को उचित रूप से सेट करें।

ट्रेनें भारी हैं और उनमें बहुत जड़ता है। तो वे हमेशा के लिए शुरू करते हैं और धीमा करते हैं।

क्या कोई बेहतर तरीका है? आप अनुमान लगाएं कि ट्रेन किस दिशा में जाएगी!

  • यदि आपने सही अनुमान लगाया है, तो यह जारी है।
  • यदि आपने गलत अनुमान लगाया है, तो स्विच को फ्लिप करने के लिए कप्तान आपको रोक देगा, बैक अप और येल करेगा। फिर यह दूसरे पथ को पुनरारंभ कर सकता है।

अगर आप हर बार सही अनुमान लगाते हैं , तो ट्रेन को कभी भी रोकना नहीं पड़ेगा।
यदि आप अक्सर गलत अनुमान लगाते हैं , तो ट्रेन रुकने, बैकअप लेने और पुनः आरंभ करने में बहुत समय व्यतीत करेगी।


एक if-statement पर विचार करें: प्रोसेसर स्तर पर, यह एक शाखा निर्देश है:

संकलित कोड का स्क्रीनशॉट यदि कोई कथन है

आप एक प्रोसेसर हैं और आप एक शाखा देखते हैं। आपको पता नहीं है कि यह किस रास्ते पर जाएगा। आप क्या करते हैं? आप निष्पादन को रोकते हैं और पिछले निर्देशों के पूरा होने तक प्रतीक्षा करते हैं। फिर आप सही रास्ता जारी रखते हैं।

आधुनिक प्रोसेसर जटिल हैं और लंबी पाइपलाइनें हैं। इसलिए वे हमेशा के लिए "वार्म अप" और "धीमा" होते हैं।

क्या कोई बेहतर तरीका है? आप अनुमान लगाएं कि शाखा किस दिशा में जाएगी!

  • यदि आपने सही अनुमान लगाया है, तो आप निष्पादित करना जारी रखते हैं।
  • यदि आपने गलत अनुमान लगाया है, तो आपको पाइपलाइन को फ्लश करने और शाखा में वापस रोल करने की आवश्यकता है। तब आप दूसरे पथ को पुनरारंभ कर सकते हैं।

यदि आप हर बार सही अनुमान लगाते हैं , तो निष्पादन को कभी भी रोकना नहीं होगा।
यदि आप अक्सर गलत अनुमान लगाते हैं , तो आप बहुत समय रुकने, वापस आने और फिर से शुरू करने में बिताते हैं।


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

तो आप रणनीतिक रूप से अनुमान कैसे लगा सकते हैं कि ट्रेन कितनी बार पीछे हटेगी और दूसरे रास्ते से नीचे जाएगी? आप पिछले इतिहास को देखें! यदि ट्रेन समय के 99% बाएं जाती है, तो आप अनुमान करते हैं कि बाएं। यदि यह वैकल्पिक है, तो आप अपने अनुमानों को वैकल्पिक करते हैं। यदि यह हर तीन बार एक तरह से जाता है, तो आप एक ही अनुमान लगाते हैं ...

दूसरे शब्दों में, आप एक पैटर्न की पहचान करने और उसका पालन करने की कोशिश करते हैं। यह कमोबेश शाखा भविष्यवक्ता कैसे काम करते हैं।

अधिकांश अनुप्रयोगों में अच्छी तरह से व्यवहार वाली शाखाएं होती हैं। इसलिए आधुनिक शाखा के भविष्यवक्ता आमतौर पर> 90% हिट दरों को प्राप्त करेंगे। लेकिन जब कोई पहचानने योग्य पैटर्न के साथ अप्रत्याशित शाखाओं का सामना करना पड़ता है, तो शाखा भविष्यवक्ता लगभग बेकार हैं।

आगे पढ़ें: "शाखा भविष्यवक्ता" विकिपीडिया पर लेख


जैसा कि ऊपर से संकेत दिया गया है, अपराधी यह है कि अगर बयान:

if (data[c] >= 128)
    sum += data[c];

ध्यान दें कि डेटा 0 और 255 के बीच समान रूप से वितरित किया जाता है। जब डेटा को सॉर्ट किया जाता है, तो लगभग पुनरावृत्तियों का पहला भाग if-स्टेटमेंट में प्रवेश नहीं करेगा। उसके बाद, वे सभी इफ-स्टेटमेंट दर्ज करेंगे।

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

त्वरित दृश्य:

T = branch taken
N = branch not taken

data[] = 0, 1, 2, 3, 4, ... 126, 127, 128, 129, 130, ... 250, 251, 252, ...
branch = N  N  N  N  N  ...   N    N    T    T    T  ...   T    T    T  ...

       = NNNNNNNNNNNN ... NNNNNNNTTTTTTTTT ... TTTTTTTTTT  (easy to predict)

हालाँकि, जब डेटा पूरी तरह से यादृच्छिक होता है, तो शाखा भविष्यवक्ता बेकार हो जाता है, क्योंकि यह यादृच्छिक डेटा की भविष्यवाणी नहीं कर सकता है। इस प्रकार संभवतः लगभग 50% गलतफहमी होगी (यादृच्छिक अनुमान से बेहतर नहीं)।

data[] = 226, 185, 125, 158, 198, 144, 217, 79, 202, 118,  14, 150, 177, 182, 133, ...
branch =   T,   T,   N,   T,   T,   T,   T,  N,   T,   N,   N,   T,   T,   T,   N  ...

       = TTNTTTTNTNNTTTN ...   (completely random - hard to predict)

तो क्या कर सकते हैं?

यदि कंपाइलर एक सशर्त चाल में शाखा का अनुकूलन करने में सक्षम नहीं है, तो आप कुछ हैक की कोशिश कर सकते हैं यदि आप प्रदर्शन के लिए पठनीयता का त्याग करने के लिए तैयार हैं।

बदलने के:

if (data[c] >= 128)
    sum += data[c];

साथ में:

int t = (data[c] - 128) >> 31;
sum += ~t & data[c];

यह शाखा को समाप्त करता है और इसे कुछ बिटवाइज़ ऑपरेशंस से बदल देता है।

(ध्यान दें कि यह हैक मूल इफ-स्टेटमेंट के समतुल्य नहीं है। लेकिन इस मामले में, यह सभी इनपुट मानों के लिए मान्य है data[])

बेंचमार्क: कोर i7 920 @ 3.5 GHz

C ++ - विज़ुअल स्टूडियो 2010 - x64 रिलीज़

//  Branch - Random
seconds = 11.777

//  Branch - Sorted
seconds = 2.352

//  Branchless - Random
seconds = 2.564

//  Branchless - Sorted
seconds = 2.587

जावा - नेटबीन्स 7.1.1 JDK 7 - x64

//  Branch - Random
seconds = 10.93293813

//  Branch - Sorted
seconds = 5.643797077

//  Branchless - Random
seconds = 3.113581453

//  Branchless - Sorted
seconds = 3.186068823

टिप्पणियों:

  • शाखा के साथ: सॉर्ट किए गए और अनसोल्ड डेटा के बीच बहुत बड़ा अंतर है।
  • हैक के साथ: सॉर्ट और अनसोल्ड डेटा के बीच कोई अंतर नहीं है।
  • C ++ के मामले में, हैक वास्तव में डेटा के छाँटे जाने पर शाखा के साथ तुलना में धीमा है।

अंगूठे का एक सामान्य नियम महत्वपूर्ण छोरों (जैसे इस उदाहरण में) में डेटा-निर्भर ब्रांचिंग से बचने के लिए है।


अपडेट करें:

  • X64 के साथ -O3या -ftree-vectorizeपर GCC 4.6.1 एक सशर्त चाल उत्पन्न करने में सक्षम है। इसलिए सॉर्ट किए गए और अनसोल्ड डेटा के बीच कोई अंतर नहीं है - दोनों तेज हैं।

    (या कुछ तेजी से: पहले से ही हल किए गए मामले के लिए, cmovविशेष रूप से धीमा हो सकता है , खासकर अगर जीसीसी इसे महत्वपूर्ण पथ पर रखता है add, विशेष रूप से इंटेल पर ब्रॉडवेल से पहले जहां cmov2 चक्र विलंबता है: जीसीसी अनुकूलन ध्वज -O3 कोड -ओ 2 की तुलना में धीमा बनाता है )

  • वीसी ++ 2010 इस शाखा के लिए सशर्त चाल भी उत्पन्न करने में असमर्थ है /Ox

  • इंटेल C ++ कंपाइलर (ICC) 11 कुछ चमत्कारी करता है। यह दो छोरों को आपस में मिलाता है , जिससे अप्रत्याशित शाखा को बाहरी लूप में फहराया जाता है। इतना ही नहीं यह गलतफहमी के लिए प्रतिरक्षा है, यह कुलपति ++ और जीसीसी जो भी उत्पन्न कर सकता है उससे दोगुना है! दूसरे शब्दों में, ICC ने बेंचमार्क को हराने के लिए टेस्ट-लूप का फायदा उठाया ...

  • यदि आप इंटेल कंपाइलर को ब्रांचलेस कोड देते हैं, तो यह सही-सही सदिश करता है ... और शाखा (लूप इंटरचेंज के साथ) के समान ही तेज़ है।

यह दिखाने के लिए कि परिपक्व आधुनिक संकलक भी कोड को अनुकूलित करने की क्षमता में बेतहाशा भिन्न हो सकते हैं ...


255
इस अनुवर्ती प्रश्न पर एक नज़र डालें: stackoverflow.com/questions/11276291/… इंटेल कम्पाइलर बाहरी लूप से पूरी तरह से छुटकारा पाने के लिए काफी करीब आ गया।
रहस्यमय जूल

23
@ मैस्टिक ट्रेन / कंपाइलर को कैसे पता चलता है कि वह गलत रास्ते में प्रवेश कर गया है?
onmyway133

25
@ पूर्व: पदानुक्रमित स्मृति संरचनाओं को देखते हुए, यह कहना असंभव है कि कैश मिस का खर्च क्या होगा। यह L1 में छूट सकता है और धीमे L2 में हल हो सकता है, या L3 में छूट सकता है और सिस्टम मेमोरी में हल हो सकता है। हालांकि, जब तक कि कुछ विचित्र कारण से यह कैश मिस डिस्क से लोड होने के लिए एक अनिवासी पृष्ठ में मेमोरी का कारण बनता है, तो आपके पास एक अच्छा बिंदु है ... मेमोरी में लगभग 25-30 वर्षों में मिलीसेकंड की सीमा तक पहुंच का समय नहीं है ;)
एंडॉन एम। कोलमैन

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

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

4086

शाखा की भविष्यवाणी।

एक क्रमबद्ध सरणी के साथ, स्थिति data[c] >= 128पहले falseमूल्यों की एक लकीर के लिए है, फिर trueसभी बाद के मूल्यों के लिए बन जाती है । यह भविष्यवाणी करना आसान है। एक अनसुलझी सरणी के साथ, आप ब्रांचिंग लागत के लिए भुगतान करते हैं।


105
क्या शाखा भविष्यवाणी विभिन्न पैटर्न के साथ क्रमबद्ध सरणियों बनाम सरणियों पर बेहतर काम करती है? उदाहरण के लिए, सरणी के लिए -> {१०, ५, २०, १०, ४०, २०, ...} पैटर्न से सरणी में अगला तत्व pred० है। क्या इस तरह की सरणी को शाखा भविष्यवाणी में उतारा जाएगा? यदि पैटर्न का पालन किया जाता है तो अगला तत्व कौन सा है? या क्या यह आमतौर पर केवल हल किए गए सरणियों के साथ मदद करता है?
एडम फ्रीमैन

132
तो मूल रूप से बिग-ओ के बारे में पारंपरिक रूप से सीखी गई हर चीज खिड़की से बाहर है? ब्रांचिंग लागत की तुलना में एक छँटाई लागत लगाने के लिए बेहतर है?
अग्रिम पाठक

133
@AgrimPathak निर्भर करता है बहुत बड़े इनपुट के लिए, उच्च जटिलता वाला एल्गोरिथ्म कम जटिलता के साथ एल्गोरिथ्म की तुलना में तेज़ होता है जब उच्च जटिलता वाले एल्गोरिदम के लिए स्थिरांक छोटे होते हैं। जहां ब्रेक-ईवन बिंदु का अनुमान लगाना कठिन हो सकता है। इसके अलावा, इसकी तुलना करें , स्थानीयता महत्वपूर्ण है। बिग-ओ महत्वपूर्ण है, लेकिन यह प्रदर्शन के लिए एकमात्र मानदंड नहीं है।
डैनियल फिशर

65
शाखा भविष्यवाणी कब होती है? भाषा को कब पता चलेगा कि सरणी सॉर्ट की गई है? मैं उस सरणी की स्थिति के बारे में सोच रहा हूं, जो इस तरह दिखती है: [1,2,3,4,5, ... 998,999,1000, 3, 10001, 1000,000]? क्या यह अस्पष्ट 3 रनिंग टाइम बढ़ाएगा? क्या यह लंबे समय तक अनसुलझी सरणी होगी?
फिलिप बार्टुज़ि

63
@FilipBartuzi शाखा की भविष्यवाणी प्रोसेसर में होती है, भाषा के स्तर के नीचे (लेकिन भाषा संकलक को यह बताने के लिए तरीके पेश कर सकती है कि क्या संभावना है, इसलिए संकलक कोड को उस के अनुकूल बना सकता है)। आपके उदाहरण में, आउट-ऑफ-ऑर्डर 3 एक शाखा-मिसप्रिंट (उपयुक्त परिस्थितियों के लिए, जहां 3 1000 से अलग परिणाम देता है) को ले जाएगा, और इस प्रकार उस सरणी को संसाधित करने में संभवतः एक दर्जन या सौ नैनोसेकंड से अधिक समय लगेगा। क्रमबद्ध सरणी, शायद ही कभी ध्यान देने योग्य होगा। क्या समय लगता है मैं गलतफहमी की उच्च दर है, प्रति 1000 में से एक गलतफहमी ज्यादा नहीं है।
डैनियल फिशर

3310

जब डेटा सॉर्ट किया जाता है, तो प्रदर्शन में बहुत सुधार होता है, यही कारण है कि शाखा भविष्यवाणी जुर्माना हटा दिया जाता है, जैसा कि रहस्यवादी के जवाब में खूबसूरती से समझाया गया है ।

अब, यदि हम कोड को देखते हैं

if (data[c] >= 128)
    sum += data[c];

हम पा सकते हैं कि इस विशेष if... else...शाखा का अर्थ किसी शर्त के संतुष्ट होने पर कुछ जोड़ना है। इस प्रकार की शाखा को एक सशर्त चाल कथन में आसानी से परिवर्तित किया जा सकता है , जिसे cmovlएक x86प्रणाली में एक सशर्त चाल अनुदेश में संकलित किया जाएगा :। शाखा और इस प्रकार संभावित शाखा भविष्यवाणी जुर्माना हटा दिया जाता है।

में C, इस प्रकार C++, बयान है, जो में सशर्त कदम अनुदेश में (किसी भी अनुकूलन के बिना) सीधे संकलन होगा x86, त्रिगुट ऑपरेटर है ... ? ... : ...। इसलिए हम उपरोक्त कथन को एक समकक्ष में फिर से लिखते हैं:

sum += data[c] >=128 ? data[c] : 0;

पठनीयता बनाए रखते हुए, हम स्पीडअप कारक की जांच कर सकते हैं।

Intel Core i7 -2600K @ 3.4 GHz और विज़ुअल स्टूडियो 2010 रिलीज़ मोड पर, बेंचमार्क है (प्रारूपिक से कॉपी किया गया प्रारूप):

86

//  Branch - Random
seconds = 8.885

//  Branch - Sorted
seconds = 1.528

//  Branchless - Random
seconds = 3.716

//  Branchless - Sorted
seconds = 3.71

64

//  Branch - Random
seconds = 11.302

//  Branch - Sorted
 seconds = 1.830

//  Branchless - Random
seconds = 2.736

//  Branchless - Sorted
seconds = 2.737

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

अब x86उनके द्वारा उत्पन्न विधानसभा की जांच करके अधिक बारीकी से देखें । सादगी के लिए, हम दो कार्यों का उपयोग करते हैं max1औरmax2

max1सशर्त शाखा का उपयोग करता है if... else ...:

int max1(int a, int b) {
    if (a > b)
        return a;
    else
        return b;
}

max2टर्नेरी ऑपरेटर का उपयोग करता है ... ? ... : ...:

int max2(int a, int b) {
    return a > b ? a : b;
}

एक x86-64 मशीन पर, GCC -Sनीचे विधानसभा उत्पन्न करता है।

:max1
    movl    %edi, -4(%rbp)
    movl    %esi, -8(%rbp)
    movl    -4(%rbp), %eax
    cmpl    -8(%rbp), %eax
    jle     .L2
    movl    -4(%rbp), %eax
    movl    %eax, -12(%rbp)
    jmp     .L4
.L2:
    movl    -8(%rbp), %eax
    movl    %eax, -12(%rbp)
.L4:
    movl    -12(%rbp), %eax
    leave
    ret

:max2
    movl    %edi, -4(%rbp)
    movl    %esi, -8(%rbp)
    movl    -4(%rbp), %eax
    cmpl    %eax, -8(%rbp)
    cmovge  -8(%rbp), %eax
    leave
    ret

max2निर्देश के उपयोग के कारण बहुत कम कोड का उपयोग करता है cmovge। लेकिन असली लाभ यह है कि max2इसमें शाखा कूद शामिल नहीं है,jmp , जो कि अनुमानित परिणाम ठीक नहीं होने पर एक महत्वपूर्ण प्रदर्शन जुर्माना होगा।

तो एक सशर्त कदम बेहतर प्रदर्शन क्यों करता है?

एक ठेठ x86प्रोसेसर में, एक निर्देश का निष्पादन कई चरणों में विभाजित होता है। मोटे तौर पर, हमारे पास विभिन्न चरणों से निपटने के लिए अलग-अलग हार्डवेयर हैं। इसलिए हमें एक नई शुरुआत करने के लिए एक निर्देश का इंतजार करने की जरूरत नहीं है। इसे पाइपलाइनिंग कहा जाता है

शाखा मामले में, निम्नलिखित निर्देश पूर्ववर्ती द्वारा निर्धारित किया जाता है, इसलिए हम पाइपलाइनिंग नहीं कर सकते। हमें या तो इंतजार करना होगा या भविष्यवाणी करनी होगी।

एक सशर्त चाल मामले में, निष्पादन सशर्त चाल अनुदेश को कई चरणों में विभाजित किया जाता है, लेकिन पहले के चरणों की तरह Fetchऔर Decodeपिछले अनुदेश के परिणाम पर निर्भर नहीं करता है; केवल बाद के चरणों को परिणाम की आवश्यकता होती है। इस प्रकार, हम एक अनुदेश के निष्पादन समय के एक अंश का इंतजार करते हैं। यही कारण है कि जब भविष्यवाणी आसान होती है तो सशर्त चाल संस्करण शाखा की तुलना में धीमा होता है।

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

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


7
@ BlueRaja-DannyPflughoeft यह अन-ऑप्टिमाइज़्ड वर्जन है। संकलक ने टर्नेरी-ऑपरेटर का अनुकूलन नहीं किया, यह केवल इसे स्थानांतरित करता है। यदि पर्याप्त अनुकूलन स्तर दिया जाता है, तो जीसीसी अनुकूलन कर सकता है, फिर भी, यह सशर्त चाल की शक्ति दिखाता है, और मैन्युअल अनुकूलन से फर्क पड़ता है।
WiSaGaN

100
@WiSaGaN कोड कुछ भी प्रदर्शित नहीं करता है, क्योंकि आपके दो कोड समान मशीन कोड के लिए संकलित हैं। यह महत्वपूर्ण रूप से महत्वपूर्ण है कि लोगों को यह विचार नहीं आता है कि किसी भी तरह यदि आपके उदाहरण में बयान आपके उदाहरण में टेनेरी से अलग है। यह सच है कि आप अपने अंतिम पैराग्राफ में समानता के मालिक हैं, लेकिन यह इस तथ्य को नहीं मिटाता है कि बाकी का उदाहरण हानिकारक है।
जस्टिन एल।

55
यदि आप भ्रामक -O0उदाहरण को हटाने और अपने दो टेस्टस्कैप्स पर अनुकूलित असम में अंतर दिखाने के लिए अपना उत्तर संशोधित करते हैं, तो @iSaGaN मेरा डाउनवोट निश्चित रूप से एक अपवोट में बदल जाएगा ।
जस्टिन एल।

56
@UpAndAdam परीक्षण के समय, VS2010 मूल शाखा को उच्च अनुकूलन स्तर निर्दिष्ट करते हुए भी सशर्त चाल में अनुकूलित नहीं कर सकता है, जबकि gcc कर सकते हैं।
WiSaGaN

9
यह टर्नरी ऑपरेटर चाल जावा के लिए खूबसूरती से काम करता है। मिस्टिकल के जवाब को पढ़ने के बाद, मैं सोच रहा था कि जावा के लिए झूठी शाखा भविष्यवाणी से बचने के लिए क्या किया जा सकता है क्योंकि जावा में -O3 के बराबर कुछ भी नहीं है। टर्नरी ऑपरेटर: 2.1943s और मूल: 6.0303s।
परिजनों ने

2271

यदि आप इस कोड में और भी अधिक अनुकूलन के बारे में उत्सुक हैं, तो इस पर विचार करें:

मूल लूप के साथ शुरू:

for (unsigned i = 0; i < 100000; ++i)
{
    for (unsigned j = 0; j < arraySize; ++j)
    {
        if (data[j] >= 128)
            sum += data[j];
    }
}

लूप इंटरचेंज के साथ, हम सुरक्षित रूप से इस लूप को बदल सकते हैं:

for (unsigned j = 0; j < arraySize; ++j)
{
    for (unsigned i = 0; i < 100000; ++i)
    {
        if (data[j] >= 128)
            sum += data[j];
    }
}

फिर, आप देख सकते हैं कि लूप ifके निष्पादन के दौरान सशर्त स्थिर है i, इसलिए आप इसे फहरा सकते हैंif बाहर हैं:

for (unsigned j = 0; j < arraySize; ++j)
{
    if (data[j] >= 128)
    {
        for (unsigned i = 0; i < 100000; ++i)
        {
            sum += data[j];
        }
    }
}

फिर, आप देखते हैं कि इनर लूप को एक सिंगल एक्सप्रेशन में ढहाया जा सकता है, मान लें कि फ्लोटिंग पॉइंट मॉडल इसे अनुमति देता है (/fp:fast उदाहरण के लिए, फेंक दिया जाता है)

for (unsigned j = 0; j < arraySize; ++j)
{
    if (data[j] >= 128)
    {
        sum += data[j] * 100000;
    }
}

वह पहले की तुलना में 100,000 गुना तेज है।


276
यदि आप धोखा देना चाहते हैं, तो आप लूप के बाहर गुणा भी कर सकते हैं और लूप के बाद योग = * 100000 कर सकते हैं।
जयाफ

78
@ मिचेल - मेरा मानना ​​है कि यह उदाहरण वास्तव में लूप-इन्वारिएंट उत्थापन (LIH) अनुकूलन का उदाहरण है , न कि लूप स्वैप । इस मामले में, संपूर्ण आंतरिक लूप बाहरी लूप से स्वतंत्र होता है और इसलिए इसे बाहरी लूप से बाहर फहराया जा सकता है, जिसके परिणामस्वरूप परिणाम केवल iएक इकाई = 1e5 से अधिक गुणा होता है । इससे अंतिम परिणाम पर कोई फर्क नहीं पड़ता है, लेकिन मैं सिर्फ रिकॉर्ड को सीधे सेट करना चाहता था क्योंकि यह एक ऐसा अक्सर पृष्ठ है।
येयर अल्टमैन

54
हालाँकि, छोरों की अदला-बदली की सरल भावना में नहीं, ifइस बिंदु पर आंतरिक को इस में परिवर्तित किया जा सकता है: sum += (data[j] >= 128) ? data[j] * 100000 : 0;जो संकलक को कम cmovgeया बराबर करने में सक्षम हो सकता है ।
एलेक्स नॉर्थ-कीज

43
बाहरी लूप को आंतरिक लूप द्वारा लिए गए समय को प्रोफ़ाइल के लिए पर्याप्त बनाना है। तो आप क्यों स्वैप करेंगे? अंत में, उस लूप को वैसे भी हटा दिया जाएगा।
साहेबहित्स

34
@ सौरभित्स: गलत सवाल: कंपाइलर लूप स्वैप क्यों नहीं करेगा। माइक्रोबेन्चर्स कठिन है;)
मैथ्यू एम।

1884

इसमें कोई संदेह नहीं है कि हममें से कुछ लोग कोड की पहचान करने के तरीकों में रुचि रखते हैं जो सीपीयू के शाखा-भविष्यवक्ता के लिए समस्याग्रस्त है। Valgrind टूल cachegrindमें एक शाखा-भविष्यवक्ता सिम्युलेटर है, जो --branch-sim=yesध्वज का उपयोग करके सक्षम है । इस सवाल में उदाहरणों पर इसे चलाने के लिए, बाहरी छोरों की संख्या 10000 तक कम हो जाती है और इसके साथ संकलित किया जाता है g++, ये परिणाम देते हैं:

छाँटे गए:

==32551== Branches:        656,645,130  (  656,609,208 cond +    35,922 ind)
==32551== Mispredicts:         169,556  (      169,095 cond +       461 ind)
==32551== Mispred rate:            0.0% (          0.0%     +       1.2%   )

अवर्गीकृत:

==32555== Branches:        655,996,082  (  655,960,160 cond +  35,922 ind)
==32555== Mispredicts:     164,073,152  (  164,072,692 cond +     460 ind)
==32555== Mispred rate:           25.0% (         25.0%     +     1.2%   )

cg_annotateप्रश्न में लूप के लिए हमारे द्वारा निर्मित लाइन-बाय-लाइन आउटपुट में ड्रिलिंग :

छाँटे गए:

          Bc    Bcm Bi Bim
      10,001      4  0   0      for (unsigned i = 0; i < 10000; ++i)
           .      .  .   .      {
           .      .  .   .          // primary loop
 327,690,000 10,016  0   0          for (unsigned c = 0; c < arraySize; ++c)
           .      .  .   .          {
 327,680,000 10,006  0   0              if (data[c] >= 128)
           0      0  0   0                  sum += data[c];
           .      .  .   .          }
           .      .  .   .      }

अवर्गीकृत:

          Bc         Bcm Bi Bim
      10,001           4  0   0      for (unsigned i = 0; i < 10000; ++i)
           .           .  .   .      {
           .           .  .   .          // primary loop
 327,690,000      10,038  0   0          for (unsigned c = 0; c < arraySize; ++c)
           .           .  .   .          {
 327,680,000 164,050,007  0   0              if (data[c] >= 128)
           0           0  0   0                  sum += data[c];
           .           .  .   .          }
           .           .  .   .      }

यह आपको समस्याग्रस्त रेखा को आसानी से पहचानने देता है - बिना if (data[c] >= 128)बिके संस्करण में यह रेखा Bcmकैशग्रिंड के शाखा-पूर्वानुमान मॉडल के तहत 164,050,007 गलत तरीके से सशर्त शाखाएँ ( ) पैदा कर रही है, जबकि यह केवल सॉर्ट किए गए संस्करण में 10,006 का कारण बन रही है।


वैकल्पिक रूप से, लिनक्स पर आप समान कार्य को पूरा करने के लिए प्रदर्शन काउंटर सबसिस्टम का उपयोग कर सकते हैं, लेकिन सीपीयू काउंटरों का उपयोग करके मूल प्रदर्शन के साथ।

perf stat ./sumtest_sorted

छाँटे गए:

 Performance counter stats for './sumtest_sorted':

  11808.095776 task-clock                #    0.998 CPUs utilized          
         1,062 context-switches          #    0.090 K/sec                  
            14 CPU-migrations            #    0.001 K/sec                  
           337 page-faults               #    0.029 K/sec                  
26,487,882,764 cycles                    #    2.243 GHz                    
41,025,654,322 instructions              #    1.55  insns per cycle        
 6,558,871,379 branches                  #  555.455 M/sec                  
       567,204 branch-misses             #    0.01% of all branches        

  11.827228330 seconds time elapsed

अवर्गीकृत:

 Performance counter stats for './sumtest_unsorted':

  28877.954344 task-clock                #    0.998 CPUs utilized          
         2,584 context-switches          #    0.089 K/sec                  
            18 CPU-migrations            #    0.001 K/sec                  
           335 page-faults               #    0.012 K/sec                  
65,076,127,595 cycles                    #    2.253 GHz                    
41,032,528,741 instructions              #    0.63  insns per cycle        
 6,560,579,013 branches                  #  227.183 M/sec                  
 1,646,394,749 branch-misses             #   25.10% of all branches        

  28.935500947 seconds time elapsed

यह असंतुष्टता के साथ स्रोत कोड एनोटेशन भी कर सकता है।

perf record -e branch-misses ./sumtest_unsorted
perf annotate -d sumtest_unsorted
 Percent |      Source code & Disassembly of sumtest_unsorted
------------------------------------------------
...
         :                      sum += data[c];
    0.00 :        400a1a:       mov    -0x14(%rbp),%eax
   39.97 :        400a1d:       mov    %eax,%eax
    5.31 :        400a1f:       mov    -0x20040(%rbp,%rax,4),%eax
    4.60 :        400a26:       cltq   
    0.00 :        400a28:       add    %rax,-0x30(%rbp)
...

देखें प्रदर्शन ट्यूटोरियल अधिक जानकारी के लिए।


74
यह डरावना है, अनसोल्ड लिस्ट में ऐड को हिट करने का 50% मौका होना चाहिए। किसी भी तरह से शाखा की भविष्यवाणी में केवल 25% की दर है, यह 50% से बेहतर कैसे कर सकता है?
टॉलब्रियन

128
@ long.b.lo: 25% सभी शाखाओं का है - लूप में दो शाखाएँ हैं , एक data[c] >= 128(जिसके लिए 50% छूट की दर है जैसा कि आप सुझाव देते हैं) और एक लूप की स्थिति के लिए c < arraySize~ ~ 0% छूट दर ।
कैफ़े

1340

मैं बस इस सवाल और इसके जवाब पर पढ़ा, और मुझे लगता है कि एक जवाब गायब है।

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

यह दृष्टिकोण सामान्य रूप से काम करता है यदि:

  1. यह एक छोटी सी तालिका है और प्रोसेसर में कैश होने की संभावना है, और
  2. आप चीजों को काफी टाइट लूप में चला रहे हैं और / या प्रोसेसर डेटा को प्रीलोड कर सकता है।

पृष्ठभूमि और क्यों

प्रोसेसर के दृष्टिकोण से, आपकी मेमोरी धीमी है। गति के अंतर की भरपाई करने के लिए, कैश का एक जोड़ा आपके प्रोसेसर (L1 / L2 कैश) में बनाया गया है। तो कल्पना कीजिए कि आप अपनी अच्छी गणना कर रहे हैं और यह पता लगा सकते हैं कि आपको स्मृति का एक टुकड़ा चाहिए। प्रोसेसर अपने 'लोड' ऑपरेशन को प्राप्त करेगा और मेमोरी के टुकड़े को कैश में लोड कर देगा - और फिर शेष गणना करने के लिए कैश का उपयोग करता है। क्योंकि मेमोरी अपेक्षाकृत धीमी है, यह 'लोड' आपके प्रोग्राम को धीमा कर देगा।

शाखा भविष्यवाणी की तरह, यह पेंटियम प्रोसेसर में अनुकूलित किया गया था: प्रोसेसर भविष्यवाणी करता है कि उसे डेटा का एक टुकड़ा लोड करने की आवश्यकता है और ऑपरेशन से पहले कैश में लोड करने का प्रयास करता है वास्तव में कैश को हिट करता है। जैसा कि हमने पहले ही देखा है, शाखा भविष्यवाणी कभी-कभी बुरी तरह से गलत हो जाती है - सबसे बुरी स्थिति में आपको वापस जाने की आवश्यकता होती है और वास्तव में एक मेमोरी लोड की प्रतीक्षा करते हैं, जो हमेशा के लिए ले जाएगा ( दूसरे शब्दों में: असफल शाखा की भविष्यवाणी खराब है, एक मेमोरी एक शाखा की भविष्यवाणी विफल होने के बाद लोड केवल भयानक है! )।

सौभाग्य से हमारे लिए, यदि मेमोरी एक्सेस पैटर्न अनुमानित है, तो प्रोसेसर इसे अपने तेज़ कैश में लोड करेगा और सब ठीक है।

सबसे पहले हमें यह जानना चाहिए कि छोटी क्या है ? जबकि छोटा आम तौर पर बेहतर होता है, अंगूठे का एक नियम लुकअप टेबल से चिपका होता है जो आकार में <= 4096 बाइट्स होते हैं। ऊपरी सीमा के रूप में: यदि आपका लुकअप टेबल 64K से बड़ा है तो शायद यह पुनर्विचार करने लायक है।

एक मेज का निर्माण

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

इस स्थिति में: = = 128 का अर्थ है कि हम मान रख सकते हैं, <128 का अर्थ है कि हम इससे छुटकारा पा लें। ऐसा करने का सबसे आसान तरीका है 'और': यदि हम इसे रखते हैं, तो हम और यह 7FFFFFFF; अगर हम इससे छुटकारा पाना चाहते हैं, तो हम और यह 0. के साथ है। सूचना यह भी है कि 128 2 की शक्ति है - इसलिए हम आगे बढ़ सकते हैं और 32768/128 पूर्णांकों की तालिका बना सकते हैं और इसे एक शून्य और बहुत से भर सकते हैं 7FFFFFFFF की।

प्रबंधित भाषाएँ

आपको आश्चर्य हो सकता है कि यह प्रबंधित भाषाओं में क्यों अच्छा काम करता है। आखिरकार, प्रबंधित भाषाएं एक शाखा के साथ सरणियों की सीमाओं की जांच करती हैं ताकि यह सुनिश्चित हो सके कि आप गड़बड़ नहीं करते हैं ...

खैर, बिल्कुल नहीं ... :-)

प्रबंधित भाषाओं के लिए इस शाखा को समाप्त करने पर काफी काम किया गया है। उदाहरण के लिए:

for (int i = 0; i < array.Length; ++i)
{
   // Use array[i]
}

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

यदि आप प्रबंधित भाषाओं में लुकअप से परेशानी में हैं - & 0x[something]FFFतो सीमा जाँच को अनुमानित बनाने के लिए आपके लुकअप फंक्शन को जोड़ने के लिए कुंजी है - और इसे तेजी से चलते हुए देखें।

इस मामले का परिणाम

// Generate data
int arraySize = 32768;
int[] data = new int[arraySize];

Random random = new Random(0);
for (int c = 0; c < arraySize; ++c)
{
    data[c] = random.Next(256);
}

/*To keep the spirit of the code intact, I'll make a separate lookup table
(I assume we cannot modify 'data' or the number of loops)*/

int[] lookup = new int[256];

for (int c = 0; c < 256; ++c)
{
    lookup[c] = (c >= 128) ? c : 0;
}

// Test
DateTime startTime = System.DateTime.Now;
long sum = 0;

for (int i = 0; i < 100000; ++i)
{
    // Primary loop
    for (int j = 0; j < arraySize; ++j)
    {
        /* Here you basically want to use simple operations - so no
        random branches, but things like &, |, *, -, +, etc. are fine. */
        sum += lookup[data[j]];
    }
}

DateTime endTime = System.DateTime.Now;
Console.WriteLine(endTime - startTime);
Console.WriteLine("sum = " + sum);
Console.ReadLine();

57
आप शाखा-भविष्यवक्ता को बायपास करना चाहते हैं, क्यों? यह एक अनुकूलन है।
डस्टिन ओपरा

108
क्योंकि कोई शाखा एक शाखा से बेहतर नहीं है :-) बहुत सारी स्थितियों में यह बस बहुत तेज़ है ... यदि आप अनुकूलन कर रहे हैं, तो यह निश्चित रूप से एक कोशिश के लायक है। वे इसका उपयोग f.ex में भी बहुत कम करते हैं। graphics.stanford.edu/~seander/bithacks.html
atlaste

36
सामान्य लुकअप टेबल में तेज हो सकता है, लेकिन क्या आपने इस विशेष स्थिति के लिए परीक्षण चलाए हैं? आपके पास अपने कोड में अभी भी एक शाखा की स्थिति होगी, केवल अब यह तालिका पीढ़ी भाग को देखने के लिए ले जाया गया है। आपको अभी भी अपना पूर्ण बढ़ावा नहीं मिलेगा
ज़ैन रिज़वी

38
@ ज़ैन अगर आप वास्तव में जानना चाहते हैं ... हाँ: शाखा के साथ 15 सेकंड और मेरे संस्करण के साथ 10। भले ही, यह किसी भी तरह से जानने के लिए एक उपयोगी तकनीक है।
18

42
क्यों नहीं sum += lookup[data[j]]जहां lookup256 प्रविष्टियों के साथ एक सरणी है, पहले वाले शून्य हैं और पिछले वाले सूचकांक के बराबर हैं?
क्राइस वंडरमोटन

1200

जब सरणी को क्रमबद्ध किया जाता है, तो डेटा 0 और 255 के बीच वितरित किया जाता है, पुनरावृत्तियों की पहली छमाही के आसपास- स्टेटमेंट if( ifविवरण नीचे साझा किया गया है) में प्रवेश नहीं करेगा ।

if (data[c] >= 128)
    sum += data[c];

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

आइए इसे बेहतर समझने के लिए कुछ बेंच मार्किंग करें

एक ifस्थापन का प्रदर्शन इस बात पर निर्भर करता है कि इसकी स्थिति का अनुमान लगाने योग्य पैटर्न है या नहीं। यदि स्थिति हमेशा सही या हमेशा गलत होती है, तो प्रोसेसर में शाखा पूर्वानुमान तर्क पैटर्न को उठाएगा। दूसरी ओर, यदि पैटर्न अप्रत्याशित है, तो if-स्टेटेशन बहुत अधिक महंगा होगा।

आइए विभिन्न स्थितियों के साथ इस लूप के प्रदर्शन को मापें:

for (int i = 0; i < max; i++)
    if (condition)
        sum++;

यहाँ अलग-अलग सही-गलत पैटर्न के साथ लूप का समय दिया गया है:

Condition                Pattern             Time (ms)
-------------------------------------------------------
(i & 0×80000000) == 0    T repeated          322

(i & 0xffffffff) == 0    F repeated          276

(i & 1) == 0             TF alternating      760

(i & 3) == 0             TFFFTFFF           513

(i & 2) == 0             TTFFTTFF           1675

(i & 4) == 0             TTTTFFFFTTTTFFFF   1275

(i & 8) == 0             8T 8F 8T 8F        752

(i & 16) == 0            16T 16F 16T 16F    490

एक " खराब " सच्चा-गलत पैटर्न ifएक " अच्छा " पैटर्न की तुलना में छह गुना तक धीमी गति से बना सकता है ! बेशक, कौन सा पैटर्न अच्छा है और कौन सा बुरा है यह कंपाइलर और विशिष्ट प्रोसेसर द्वारा उत्पन्न सटीक निर्देशों पर निर्भर करता है।

इसलिए प्रदर्शन पर शाखा की भविष्यवाणी के प्रभाव के बारे में कोई संदेह नहीं है!


23
@MooDDuck 'क्योंकि इससे कोई फर्क नहीं पड़ेगा - वह मूल्य कुछ भी हो सकता है, लेकिन यह अभी भी इन थ्रेसहोल्ड की सीमा में होगा। तो जब आप पहले से ही सीमा जानते हैं तो एक यादृच्छिक मूल्य क्यों दिखाते हैं? हालांकि मैं मानता हूं कि आप पूर्णता के लिए एक दिखा सकते हैं, और 'सिर्फ इसके लिए।'
cst1992

24
@ cst1992: अभी उनकी सबसे धीमी टाइमिंग है TTFFTTFFTTFF, जो कि मेरी इंसानी आंख को काफी प्रेडिक्टेबल लगती है। बेतरतीब ढंग से अप्रत्याशित है, इसलिए यह पूरी तरह से संभव है कि यह अभी भी धीमा होगा, और इस प्रकार यहां दिखाई गई सीमाओं के बाहर। OTOH, यह हो सकता है कि TTFFTTFF पूरी तरह से पैथोलॉजिकल केस को हिट करता है। बता नहीं सकते, क्योंकि उन्होंने यादृच्छिक के लिए समय नहीं दिखाया था।
मूइंग डक

21
@MooingDuck एक इंसानी नज़र के लिए, "TTFFTTFFTTFF" एक प्रेडिक्टेबल सीक्वेंस है, लेकिन हम यहां जिस बारे में बात कर रहे हैं, वह सीपीयू में बनी ब्रांच प्रेडिक्टर का व्यवहार है। शाखा भविष्यवक्ता एआई-स्तरीय पैटर्न मान्यता नहीं है; यह बहुत सरल है। जब आप केवल वैकल्पिक शाखाएँ बनाते हैं तो यह अच्छी तरह से भविष्यवाणी नहीं करता है। अधिकांश कोड में, शाखाएं लगभग हर समय उसी तरह से जाती हैं; एक लूप पर विचार करें जो एक हजार बार निष्पादित होता है। लूप के अंत में शाखा 999 बार लूप की शुरुआत में वापस चली जाती है, और फिर हज़ारवां समय कुछ अलग करता है। एक बहुत ही सरल शाखा पूर्वसूचक आमतौर पर अच्छी तरह से काम करता है।
स्टीवेहा

18
@steveha: मुझे लगता है कि आप इस बारे में धारणा बना रहे हैं कि सीपीयू शाखा भविष्यवक्ता कैसे काम करता है, और मैं उस पद्धति से असहमत हूं। मुझे नहीं पता कि शाखा भविष्यवक्ता कितना उन्नत है, लेकिन मुझे लगता है कि यह आपकी तुलना में कहीं अधिक उन्नत है। आप शायद सही हैं, लेकिन माप निश्चित रूप से अच्छा होगा।
मिंग डक

5
@steveha: दो-स्तरीय अनुकूली भविष्यवक्ता TTFFTTFF पैटर्न पर बिना किसी समस्या के लॉक कर सकता है। "इस भविष्यवाणी पद्धति के वेरिएंट का उपयोग अधिकांश आधुनिक माइक्रोप्रोसेसरों में किया जाता है"। स्थानीय शाखा की भविष्यवाणी और वैश्विक शाखा की भविष्यवाणी एक दो स्तर के अनुकूली भविष्यवक्ता पर आधारित हैं, वे भी कर सकते हैं। "वैश्विक शाखा की भविष्यवाणी एएमडी प्रोसेसर में और इंटेल पेंटियम एम, कोर, कोर 2, और सिल्वरमोंट-आधारित एटम प्रोसेसर में उपयोग की जाती है" इस सूची में सहमत भविष्यवक्ता, हाइब्रिड भविष्यवक्ता, अप्रत्यक्ष कूद की भविष्यवाणी भी जोड़ते हैं। लूप भविष्यवक्ता पर ताला नहीं है, लेकिन 75% हिट। वह केवल 2 छोड़ता है जो लॉक नहीं कर सकता है
Mooing Duck

1126

शाखा भविष्यवाणी त्रुटियों से बचने का एक तरीका एक लुकअप तालिका बनाना और डेटा का उपयोग करके इसे अनुक्रमित करना है। स्टीफन डी ब्रुजन ने अपने जवाब में चर्चा की।

लेकिन इस मामले में, हम जानते हैं कि मूल्य सीमा [0, 255] में हैं और हम केवल मूल्यों के बारे में परवाह करते हैं = = 128। इसका मतलब है कि हम आसानी से एक बिट को निकाल सकते हैं जो हमें बताएगा कि क्या हम मूल्य चाहते हैं या नहीं: स्थानांतरण द्वारा दाएं 7 बिट्स के लिए डेटा, हम 0 बिट या 1 बिट के साथ छोड़ दिए जाते हैं, और हम केवल 1 बिट होने पर वैल्यू जोड़ना चाहते हैं। चलो इस बिट को "निर्णय बिट" कहते हैं।

एक सरणी में एक सूचकांक के रूप में निर्णय बिट के 0/1 मूल्य का उपयोग करके, हम कोड बना सकते हैं जो समान रूप से तेज़ होगा चाहे डेटा सॉर्ट किया गया हो या नहीं। हमारा कोड हमेशा एक मूल्य जोड़ देगा, लेकिन जब निर्णय बिट 0 होता है, तो हम उस मूल्य को जोड़ देंगे जहां हम परवाह नहीं करते हैं। यहाँ कोड है:

// Test
clock_t start = clock();
long long a[] = {0, 0};
long long sum;

for (unsigned i = 0; i < 100000; ++i)
{
    // Primary loop
    for (unsigned c = 0; c < arraySize; ++c)
    {
        int j = (data[c] >> 7);
        a[j] += data[c];
    }
}

double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;
sum = a[1];

यह कोड जोड़ के आधे हिस्से को बर्बाद करता है, लेकिन कभी भी शाखा भविष्यवाणी विफलता नहीं होती है। यह एक वास्तविक विवरण के साथ संस्करण की तुलना में यादृच्छिक डेटा पर बहुत तेजी से होता है।

लेकिन मेरे परीक्षण में, एक स्पष्ट लुकअप तालिका इससे थोड़ी तेज थी, शायद इसलिए कि लुकअप तालिका में अनुक्रमण बिट शिफ्टिंग की तुलना में थोड़ा तेज था। यह दिखाता है कि मेरा कोड कैसे सेट अप करता है और लुकअप टेबल का उपयोग करता है ( lutकोड में " लुकअप टेबल " के लिए अकल्पनीय रूप से कहा गया है)। यहाँ C ++ कोड है:

// Declare and then fill in the lookup table
int lut[256];
for (unsigned c = 0; c < 256; ++c)
    lut[c] = (c >= 128) ? c : 0;

// Use the lookup table after it is built
for (unsigned i = 0; i < 100000; ++i)
{
    // Primary loop
    for (unsigned c = 0; c < arraySize; ++c)
    {
        sum += lut[data[c]];
    }
}

इस मामले में, लुकअप तालिका केवल 256 बाइट्स थी, इसलिए यह कैश में अच्छी तरह से फिट बैठता है और सभी तेज था। यदि डेटा 24-बिट मान था, तो यह तकनीक अच्छी तरह से काम नहीं करेगी और हम केवल उनमें से आधा चाहते थे ... लुकअप तालिका व्यावहारिक होने के लिए बहुत बड़ी होगी। दूसरी ओर, हम ऊपर दिखाए गए दो तकनीकों को जोड़ सकते हैं: पहले बिट्स को शिफ्ट करें, फिर एक लुकअप टेबल को इंडेक्स करें। 24-बिट मान के लिए जिसे हम केवल शीर्ष आधा मान चाहते हैं, हम संभावित रूप से डेटा को 12 बिट्स द्वारा सही शिफ्ट कर सकते हैं, और टेबल इंडेक्स के लिए 12-बिट मान के साथ छोड़ दिया जा सकता है। एक 12-बिट टेबल इंडेक्स में 4096 मानों की तालिका होती है, जो व्यावहारिक हो सकती है।

एक सरणी में अनुक्रमित करने की तकनीक, एक ifबयान का उपयोग करने के बजाय , यह तय करने के लिए इस्तेमाल किया जा सकता है कि किस सूचक का उपयोग करना है। मैंने एक पुस्तकालय देखा जिसमें बाइनरी ट्री को लागू किया गया था, और इसके बजाय दो नामित पॉइंटर्स ( pLeftऔर pRightजो भी हो) की लंबाई-दो सरणी थी और निर्णय लेने के लिए "निर्णय बिट" तकनीक का उपयोग किया था कि किसका पालन करना है। उदाहरण के लिए, इसके बजाय:

if (x < node->value)
    node = node->pLeft;
else
    node = node->pRight;

यह पुस्तकालय कुछ ऐसा करेगा:

i = (x < node->value);
node = node->link[i];

यहां इस कोड का लिंक दिया गया है: रेड ब्लैक ट्रीज़ , इटरनली कन्फ्यूज्ड


29
ठीक है, आप बस बिट को सीधे और गुणा का उपयोग कर सकते हैं ( data[c]>>7- जिसकी चर्चा यहां कहीं भी की गई है); मैंने जानबूझकर इस समाधान को छोड़ दिया, लेकिन निश्चित रूप से आप सही हैं। बस एक छोटा सा नोट: लुकअप टेबल के लिए अंगूठे का नियम यह है कि अगर यह 4KB (कैशिंग के कारण) में फिट बैठता है, तो यह काम करेगा - अधिमानतः तालिका को जितना संभव हो उतना छोटा बना सकता है। प्रबंधित भाषाओं के लिए, मैं धक्का दे सकता हूँ कि 64KB के लिए, C ++ और C जैसी निम्न-स्तरीय भाषाओं के लिए, मैं शायद पुनर्विचार करूँगा (यह सिर्फ एक अनुभव है)। चूंकि typeof(int) = 4, मैं अधिकतम 10 बिट्स से चिपकना चाहूंगा।
atlaste

17
मुझे लगता है कि 0/1 मान के साथ अनुक्रमण संभवत: एक पूर्णांक से कई गुना अधिक तेज होगा, लेकिन मुझे लगता है कि यदि प्रदर्शन वास्तव में महत्वपूर्ण है तो आप इसे प्रोफाइल कर सकते हैं। मैं मानता हूं कि कैश प्रेशर से बचने के लिए छोटे लुकअप टेबल जरूरी हैं, लेकिन स्पष्ट रूप से अगर आपके पास बड़ा कैश है तो आप बड़ी लुकअप टेबल के साथ दूर जा सकते हैं, इसलिए 4KB हार्ड रूल की तुलना में अधिक अंगूठे का नियम है। मुझे लगता है कि आपका मतलब है sizeof(int) == 4? यह 32-बिट के लिए सही होगा। मेरे दो साल पुराने सेल फोन में 32KB L1 कैश है, इसलिए यहां तक ​​कि 4K लुकअप टेबल भी काम कर सकती है, खासकर अगर लुक वैल्यू इंट के बजाय बाइट हो।
स्टीवेहा

12
संभवतः मैं कुछ याद कर रहा हूं, लेकिन आपके jबराबर 0 या 1 विधि में, क्यों आप jइसे अनुक्रमणिका अनुक्रमणिका का उपयोग करने के बजाय जोड़ने से पहले अपने मूल्य को गुणा नहीं करते हैं (संभवतः इसके 1-jबजाय गुणा किया जाना चाहिए j)
रिचर्ड टिंगल

6
@steveha गुणन तेज होना चाहिए, मैंने इसे इंटेल की पुस्तकों में देखने की कोशिश की, लेकिन यह नहीं मिल सका ... किसी भी तरह से, बेंचमार्किंग भी मुझे यहां परिणाम देता है।
atlaste

10
@ ऑस्टेवहा पीएस: एक और संभावित उत्तर होगा int c = data[j]; sum += c & -(c >> 7);जिसमें किसी भी गुणन की आवश्यकता नहीं है।
atlaste

1021

सॉर्ट किए गए मामले में, आप सफल शाखा भविष्यवाणी या किसी भी शाखा रहित तुलना चाल पर भरोसा करने से बेहतर कर सकते हैं: पूरी तरह से शाखा को हटा दें।

वास्तव में, सरणी का विभाजन एक आकस्मिक क्षेत्र में data < 128और दूसरे के साथ होता है data >= 128। तो आपको विभाजन बिंदु को एक द्विध्रुवीय खोज ( Lg(arraySize) = 15तुलनाओं का उपयोग करके) के साथ ढूंढना चाहिए , फिर उस बिंदु से एक सीधा संचय करना चाहिए।

कुछ ऐसा (अनियंत्रित)

int i= 0, j, k= arraySize;
while (i < k)
{
  j= (i + k) >> 1;
  if (data[j] >= 128)
    k= j;
  else
    i= j;
}
sum= 0;
for (; i < arraySize; i++)
  sum+= data[i];

या, थोड़ा और अधिक मोटे

int i, k, j= (i + k) >> 1;
for (i= 0, k= arraySize; i < k; (data[j] >= 128 ? k : i)= j)
  j= (i + k) >> 1;
for (sum= 0; i < arraySize; i++)
  sum+= data[i];

अभी तक अधिक तेज़ दृष्टिकोण, जो छांटे गए या अनसोल्ड दोनों के लिए एक अनुमानित समाधान देता है: sum= 3137536;(वास्तव में एक समान वितरण, अनुमानित मूल्य 191.5 के साथ 16384 नमूने) :-)


23
sum= 3137536- चतुर। यह थोड़े स्पष्ट रूप से सवाल का बिंदु नहीं है। सवाल स्पष्ट रूप से आश्चर्यजनक प्रदर्शन विशेषताओं को समझाने के बारे में है। मैं यह कहने के लिए इच्छुक हूं कि इसके std::partitionबजाय करने का जोड़ std::sortमूल्यवान है। यद्यपि वास्तविक प्रश्न दिए गए सिंथेटिक बेंचमार्क से अधिक है।
sehe

12
@ डीडएमजीएम: यह वास्तव में किसी दिए गए कुंजी के लिए मानक डाइकोटोमिक खोज नहीं है, बल्कि विभाजन सूचकांक के लिए एक खोज है; इसे प्रति पुनरावृत्ति के लिए एकल तुलना की आवश्यकता होती है। लेकिन इस कोड पर भरोसा न करें, मैंने इसकी जाँच नहीं की है। यदि आप गारंटीशुदा सही कार्यान्वयन में रुचि रखते हैं, तो मुझे बताएं।
यवेस डाएव

831

उपरोक्त व्यवहार शाखा भविष्यवाणी के कारण हो रहा है।

शाखा भविष्यवाणी को समझने के लिए पहले निर्देश पाइपलाइन को समझना होगा :

किसी भी निर्देश को चरणों के एक क्रम में तोड़ा जाता है ताकि विभिन्न चरणों को समांतर रूप से निष्पादित किया जा सके। इस तकनीक को निर्देश पाइपलाइन के रूप में जाना जाता है और आधुनिक प्रोसेसर में थ्रूपुट को बढ़ाने के लिए इसका उपयोग किया जाता है। इसे बेहतर समझने के लिए कृपया इस उदाहरण को विकिपीडिया पर देखें ।

आमतौर पर, आधुनिक प्रोसेसर में काफी लंबी पाइपलाइनें होती हैं, लेकिन आसानी के लिए आइए इन 4 चरणों पर विचार करें।

  1. यदि - स्मृति से निर्देश प्राप्त करें
  2. ID - निर्देश को डिकोड करें
  3. पूर्व - अनुदेश निष्पादित करें
  4. WB - सीपीयू रजिस्टर पर वापस लिखें

4-चरण पाइपलाइन सामान्य रूप से 2 निर्देशों के लिए। 4-चरण पाइपलाइन सामान्य रूप से

उपरोक्त प्रश्न पर वापस चलते हुए निम्नलिखित निर्देशों पर विचार करें:

                        A) if (data[c] >= 128)
                                /\
                               /  \
                              /    \
                        true /      \ false
                            /        \
                           /          \
                          /            \
                         /              \
              B) sum += data[c];          C) for loop or print().

शाखा भविष्यवाणी के बिना, निम्नलिखित होगा:

निर्देश बी या अनुदेश सी निष्पादित करने के लिए प्रोसेसर को निर्देश ए तक इंतजार करना होगा जब तक पाइप लाइन में EX चरण तक नहीं पहुंचता है, क्योंकि निर्देश बी या निर्देश सी में जाने का निर्णय निर्देश ए के परिणाम पर निर्भर करता है। इस तरह दिखेगा।

जब स्थिति सही हो तो: यहां छवि विवरण दर्ज करें

जब हालत झूठी हो जाती है: यहां छवि विवरण दर्ज करें

निर्देश ए के परिणाम की प्रतीक्षा करने के परिणामस्वरूप, उपरोक्त मामले में (शाखा पूर्वानुमान के बिना, सच और झूठ दोनों के लिए) कुल सीपीयू चक्र 7 है।

तो शाखा भविष्यवाणी क्या है?

ब्रांच प्रेडिक्टर यह अनुमान लगाने की कोशिश करेगा कि ब्रांच किस तरह (अगर-तब-और-स्ट्रक्चर है) निश्चित रूप से ज्ञात होने से पहले जाएगी। यह पाइपलाइन के EX चरण तक पहुंचने के लिए निर्देश ए का इंतजार नहीं करेगा, लेकिन यह निर्णय का अनुमान लगाएगा और उस निर्देश पर जाएगा (बी या सी हमारे उदाहरण के मामले में)।

एक सही अनुमान के मामले में, पाइपलाइन कुछ इस तरह दिखती है: यहां छवि विवरण दर्ज करें

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

ओपी के कोड में, पहली बार जब सशर्त, शाखा भविष्यवक्ता को भविष्यवाणी को आधार बनाने के लिए कोई जानकारी नहीं होती है, इसलिए पहली बार यह बेतरतीब ढंग से अगले निर्देश का चयन करेगा। बाद में लूप के लिए, यह इतिहास पर भविष्यवाणी को आधार बना सकता है। आरोही क्रम में क्रमबद्ध एक सरणी के लिए, तीन संभावनाएँ हैं:

  1. सभी तत्व 128 से कम हैं
  2. सभी तत्व 128 से अधिक हैं
  3. कुछ शुरुआती नए तत्व 128 से कम हैं और बाद में यह 128 से अधिक हो गए

आइए हम यह मान लें कि भविष्यवक्ता हमेशा पहले रन पर सच्ची शाखा को ग्रहण करेगा।

इसलिए पहले मामले में, यह हमेशा सच्ची शाखा लेगा क्योंकि ऐतिहासिक रूप से इसकी सभी भविष्यवाणियां सही हैं। दूसरे मामले में, शुरू में यह गलत भविष्यवाणी करेगा, लेकिन कुछ पुनरावृत्तियों के बाद, यह सही भविष्यवाणी करेगा। तीसरे मामले में, यह शुरू में सही ढंग से भविष्यवाणी करेगा जब तक कि तत्व 128 से कम नहीं हो जाते। इसके बाद यह कुछ समय के लिए विफल हो जाएगा और सही हो जाएगा जब यह इतिहास में शाखा भविष्यवाणी विफलता को देखता है।

इन सभी मामलों में संख्या में विफलता बहुत कम होगी और इसके परिणामस्वरूप, केवल कुछ बार आंशिक रूप से निष्पादित निर्देशों को त्यागने और सही शाखा के साथ शुरू करने की आवश्यकता होगी, जिसके परिणामस्वरूप कम सीपीयू चक्र होंगे।

लेकिन एक यादृच्छिक अनसुलझी सरणी के मामले में, पूर्वानुमान को आंशिक रूप से निष्पादित निर्देशों को त्यागने की आवश्यकता होती है और अधिकांश समय सही शाखा के साथ शुरू होता है और इसके परिणामस्वरूप सॉर्ट किए गए सरणी की तुलना में अधिक सीपीयू चक्र होता है।


1
एक साथ दो निर्देशों को कैसे निष्पादित किया जाता है? यह अलग सीपीयू कोर के साथ किया जाता है या पाइपलाइन निर्देश एकल सीपीयू कोर में एकीकृत है?
मकाज़म अख़गरी

1
@ M.kazemAkhgary यह सभी एक तार्किक कोर के अंदर है। यदि आप रुचि रखते हैं, तो यह अच्छी तरह से इंटेल सॉफ्टवेयर डेवलपर मैनुअल
सर्गेई.इक्विओक्सैक्सिस।इवानोव

727

एक आधिकारिक उत्तर से होगा

  1. इंटेल - ब्रांच मिसप्रिंटेड की लागत से बचना
  2. इंटेल - शाखा और गलत पुनर्गठन को रोकने के लिए गलतफहमी
  3. वैज्ञानिक कागजात - शाखा भविष्यवाणी कंप्यूटर वास्तुकला
  4. पुस्तकें: जेएल हेनेसी, डीए पैटरसन: कंप्यूटर वास्तुकला: एक मात्रात्मक दृष्टिकोण
  5. वैज्ञानिक प्रकाशनों में लेख: TY Yeh, YN Patt ने शाखा भविष्यवाणियों पर इनमें से बहुत कुछ किया।

आप इस प्यारे आरेख से भी देख सकते हैं कि शाखा भविष्यवक्ता भ्रमित क्यों हो जाता है।

2-बिट स्टेट आरेख

मूल कोड में प्रत्येक तत्व एक यादृच्छिक मूल्य है

data[c] = std::rand() % 256;

इसलिए भविष्यवक्ता पक्षों को std::rand()उड़ाने के रूप में बदल देगा ।

दूसरी ओर, एक बार जब यह हल हो जाता है, तो भविष्यवक्ता पहले दृढ़ता से नहीं लिया जाने की स्थिति में चलेगा और जब मूल्य उच्च मूल्य में बदल जाता है, तो भविष्यवक्ता तीन रनों में परिवर्तन के माध्यम से सभी तरह से जोरदार तरीके से दृढ़ता से नहीं लिया जाता है।



696

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

if (likely( everything_is_ok ))
{
    /* Do something */
}

या इसी तरह:

if (unlikely(very_improbable_condition))
{
    /* Do something */    
}

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

आम तौर पर इस तरह के अनुकूलन मुख्य रूप से कठिन-वास्तविक समय अनुप्रयोगों या एम्बेडेड सिस्टम में पाए जाते हैं जहां निष्पादन समय मायने रखता है और यह महत्वपूर्ण है। उदाहरण के लिए, यदि आप कुछ त्रुटि स्थिति की जाँच कर रहे हैं जो केवल 1/10000000 बार होती है, तो इस बारे में संकलक को सूचित क्यों न करें? इस तरह, डिफ़ॉल्ट रूप से, शाखा की भविष्यवाणी यह ​​मान लेगी कि हालत झूठी है।


678

C ++ में अक्सर उपयोग किए जाने वाले बूलियन ऑपरेशन संकलित कार्यक्रम में कई शाखाएं बनाते हैं। यदि ये शाखाएं छोरों के अंदर हैं और यह भविष्यवाणी करना कठिन है कि वे निष्पादन को काफी धीमा कर सकते हैं। बूलियन चर के रूप में मूल्य के साथ 8 बिट पूर्णांकों जमा हो जाती है 0के लिए falseऔर 1के लिए true

बूलियन चर अर्थ में overdetermined कर रहे हैं कि सभी ऑपरेटरों इनपुट चेक के रूप में बूलियन चर है कि अगर आदानों के अलावा कोई अन्य मान होना 0या 1, लेकिन ऑपरेटरों आउटपुट के रूप में है कि Booleans के अलावा कोई अन्य मूल्य का उत्पादन कर सकते 0या 1। यह बूलियन चर के साथ संचालन को आवश्यक से कम कुशल इनपुट बनाता है। उदाहरण पर विचार करें:

bool a, b, c, d;
c = a && b;
d = a || b;

यह आमतौर पर कंपाइलर द्वारा निम्नलिखित तरीके से लागू किया जाता है:

bool a, b, c, d;
if (a != 0) {
    if (b != 0) {
        c = 1;
    }
    else {
        goto CFALSE;
    }
}
else {
    CFALSE:
    c = 0;
}
if (a == 0) {
    if (b == 0) {
        d = 0;
    }
    else {
        goto DTRUE;
    }
}
else {
    DTRUE:
    d = 1;
}

यह कोड इष्टतम से बहुत दूर है। गलतफहमी के मामले में शाखाओं को लंबा समय लग सकता है। बूलियन संचालन को और अधिक कुशल बनाया जा सकता है यदि यह निश्चितता के साथ जाना जाता है कि ऑपरेंड के अलावा 0और कोई मूल्य नहीं है 1। कंपाइलर इस तरह की धारणा नहीं बनाता है, इसका कारण यह है कि चर के अन्य मान हो सकते हैं यदि वे अनधिकृत हैं या अज्ञात स्रोतों से आते हैं। उपरोक्त कोड यदि अनुकूलित किया जा सकता है aऔर bमान्य मान के लिए शुरू कर दिया गया है या अगर वे ऑपरेटरों कि बूलियन उत्पादन का उत्पादन से आते हैं। अनुकूलित कोड इस तरह दिखता है:

char a = 0, b = 1, c, d;
c = a & b;
d = a | b;

charके बजाय प्रयोग किया जाता है boolक्रम में यह संभव बिटवाइज़ ऑपरेटर्स (उपयोग करने के लिए बनाने के लिए &और |) के बजाय बूलियन ऑपरेटरों ( &&और ||)। बिटवाइज़ ऑपरेटर एकल निर्देश हैं जो केवल एक घड़ी चक्र लेते हैं। OR ऑपरेटर ( |) काम करता है, भले ही aऔर bके अलावा अन्य मान हो 0या 1। AND ऑपरेटर ( &) और विशेष या ऑपरेटर ( ^यदि ऑपरेंड के अलावा अन्य मान) असंगत परिणाम दे सकता है 0और 1

~नहीं के लिए इस्तेमाल नहीं किया जा सकता है। इसके बजाय, आप एक बूलियन को एक वैरिएबल पर नहीं बना सकते हैं जिसे XOR'ing के साथ 0या उसके 1द्वारा जाना जाता है 1:

bool a, b;
b = !a;

इसके लिए अनुकूलित किया जा सकता है:

char a = 0, b;
b = a ^ 1;

a && bके साथ प्रतिस्थापित नहीं किया जा सकता है a & bअगर bएक अभिव्यक्ति है जिसका मूल्यांकन नहीं किया जाना चाहिए यदि aहै false( &&मूल्यांकन नहीं करेगा b, &होगा)। इसी तरह, के a || bसाथ प्रतिस्थापित नहीं किया जा सकता है a | bअगर bएक अभिव्यक्ति है जिसका मूल्यांकन नहीं किया जाना चाहिए यदि aहै true

यदि ऑपरेंड की तुलना की जाती है, तो ऑपरटिव की तुलना में बिटवेअर ऑपरेटरों का उपयोग करना अधिक फायदेमंद होता है:

bool a; double x, y, z;
a = x > y && z < 5.0;

ज्यादातर मामलों में इष्टतम है (जब तक कि आप &&कई शाखा गलतफहमी उत्पन्न करने के लिए अभिव्यक्ति की उम्मीद नहीं करते हैं )।


341

वह पक्का है!...

ब्रांच प्रेडिक्शन लॉजिक को धीमा बनाता है, स्विचिंग के कारण जो आपके कोड में होता है! यह ऐसा है जैसे आप एक सीधी सड़क या बहुत सारे मोड़ के साथ एक सड़क पर जा रहे हैं, यह सुनिश्चित करने के लिए कि सीधे एक तेज हो रहा है ...!

यदि सरणी को क्रमबद्ध किया गया है, तो आपकी स्थिति पहले चरण में गलत है: data[c] >= 128तो सड़क के अंत तक पूरे रास्ते के लिए एक सही मूल्य बन जाता है। यही कारण है कि आप तेजी से तर्क के अंत तक पहुंचते हैं। दूसरी ओर, एक अनसरे सरणी का उपयोग करते हुए, आपको बहुत सारे मोड़ और प्रसंस्करण की आवश्यकता होती है, जो आपके कोड को सुनिश्चित करने के लिए धीमा कर देते हैं ...

नीचे मैंने आपके लिए बनाई गई छवि को देखा। कौन सी गली तेजी से खत्म होने वाली है?

शाखा की भविष्यवाणी

तो प्रोग्रामेटिक रूप से, शाखा भविष्यवाणी प्रक्रिया धीमी होने का कारण बनती है ...

इसके अलावा, यह जानना अच्छा है कि हमारे पास दो प्रकार की शाखा भविष्यवाणियां हैं, जिनमें से प्रत्येक आपके कोड को अलग तरह से प्रभावित करने वाली है:

1. स्थिर

2. गतिशील

शाखा की भविष्यवाणी

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

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


304

इस सवाल का पहले ही कई बार शानदार जवाब दिया जा चुका है। फिर भी मैं अभी तक एक और दिलचस्प विश्लेषण के लिए समूह का ध्यान आकर्षित करना चाहता हूं।

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

लिंक यहाँ है: http://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/profile/demo.htm


3
यह एक बहुत ही दिलचस्प लेख है (वास्तव में, मैंने अभी तक यह सब पढ़ा है), लेकिन यह प्रश्न का उत्तर कैसे देता है?
पीटर मोर्टेंसन

2
@PeterMortensen मैं आपके सवाल से थोड़ा भड़क गया हूं। उदाहरण के लिए यहां उस टुकड़े से एक प्रासंगिक पंक्ति है: When the input is unsorted, all the rest of the loop takes substantial time. But with sorted input, the processor is somehow able to spend not just less time in the body of the loop, meaning the buckets at offsets 0x18 and 0x1C, but vanishingly little time on the mechanism of looping. लेखक यहां पोस्ट किए गए कोड के संदर्भ में रूपरेखा पर चर्चा करने की कोशिश कर रहा है और इस प्रक्रिया में यह समझाने की कोशिश कर रहा है कि छंटे हुए मामले में इतनी अधिक तेजी क्यों है।
फॉरएवर लर्निंग

260

जैसा कि पहले ही दूसरों द्वारा उल्लेख किया गया है, रहस्य के पीछे क्या है शाखा भविष्यवक्ता

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

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

टू-वे ब्रांचिंग आमतौर पर सशर्त कूद अनुदेश के साथ लागू किया जाता है। एक सशर्त कूद या तो "नहीं लिया जा सकता" और कोड की पहली शाखा के साथ निष्पादन जारी रखता है जो सशर्त कूदने के तुरंत बाद होता है, या इसे "लिया" जा सकता है और प्रोग्राम मेमोरी में एक अलग जगह पर कूद सकता है जहां कोड की दूसरी शाखा है संग्रहीत। यह निश्चित रूप से ज्ञात नहीं है कि क्या सशर्त कूद लिया जाएगा या नहीं लिया जाएगा जब तक कि स्थिति की गणना नहीं की गई है और सशर्त कूदने से अनुदेश पाइपलाइन में निष्पादन चरण पारित हो गया है (अंजीर देखें 1)।

आकृति 1

वर्णित परिदृश्य के आधार पर, मैंने यह दिखाने के लिए एक एनीमेशन डेमो लिखा है कि विभिन्न स्थितियों में निर्देश को एक पाइपलाइन में कैसे निष्पादित किया जाता है।

  1. बिना ब्रांच प्रेडिक्टर के।

शाखा की भविष्यवाणी के बिना, प्रोसेसर को इंतजार करना होगा जब तक कि सशर्त कूद अनुदेश अगले चरण से पहले निष्पादित चरण को पारित नहीं कर लेता है, पाइपलाइन में भ्रूण के चरण में प्रवेश कर सकता है।

उदाहरण में तीन निर्देश हैं और पहला एक सशर्त कूद अनुदेश है। सशर्त कूद निर्देश निष्पादित होने तक बाद के दो निर्देश पाइपलाइन में जा सकते हैं।

शाखा भविष्यवक्ता के बिना

इसे पूरा करने के लिए 3 निर्देशों के लिए 9 घड़ी चक्र लगेंगे।

  1. ब्रांच प्रिडिक्टर का उपयोग करें और सशर्त कूद न लें। मान लेते हैं कि अनुमान सशर्त कूद नहीं रहा है।

यहां छवि विवरण दर्ज करें

3 निर्देशों को पूरा करने के लिए यह 7 घड़ी चक्र लेगा।

  1. ब्रांच प्रेडिक्टर का उपयोग करें और एक सशर्त कूद लें। मान लेते हैं कि अनुमान सशर्त कूद नहीं रहा है।

यहां छवि विवरण दर्ज करें

इसे पूरा करने के लिए 3 निर्देशों के लिए 9 घड़ी चक्र लगेंगे।

एक शाखा के दुरुपयोग के मामले में जो समय बर्बाद किया जाता है वह समय-समय पर पाइप लाइन के चरणों में भ्रूण अवस्था से लेकर निष्पादन अवस्था तक के बराबर होता है। आधुनिक माइक्रोप्रोसेसरों में काफी लंबी पाइपलाइनें होती हैं, ताकि गलतफहमी देरी 10 और 20 घड़ी चक्र के बीच हो। नतीजतन, एक पाइपलाइन बनाने से अधिक उन्नत शाखा भविष्यवक्ता की आवश्यकता बढ़ जाती है।

जैसा कि आप देख सकते हैं, ऐसा लगता है कि हमारे पास ब्रांच प्रिडिक्टर का उपयोग नहीं करने का कोई कारण नहीं है।

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


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

@mckenzm: आउट-ऑफ-ऑर्डर सट्टा निष्पादन शाखा भविष्यवाणी को और भी अधिक मूल्यवान बनाता है; साथ ही भ्रूण / डिकोड बुलबुले को छिपाना, शाखा भविष्यवाणी + सट्टा निष्पादन महत्वपूर्ण पथ विलंबता से नियंत्रण निर्भरता को हटाता है। ब्रांच की स्थिति ज्ञात होने से पहलेif() ब्लॉक के अंदर या बाद में कोड निष्पादित हो सकता है। या जैसे खोज लूप के लिए, या तो व्यवधान ओवरलैप कर सकते हैं। यदि आपको अगली यात्रा के किसी भी चलने से पहले मैच-या-परिणाम नहीं होने का इंतजार करना था, तो आप थ्रूपुट के बजाय कैश लोड + ALU विलंबता पर अड़चन डालेंगे। strlenmemchr
पीटर कॉर्ड्स

209

शाखा-भविष्यवाणी लाभ!

यह समझना महत्वपूर्ण है कि शाखा की गलतफहमी कार्यक्रमों को धीमा नहीं करती है। मिस्ड प्रेडिक्शन की लागत वैसे ही है जैसे कि ब्रांच प्रेडिक्शन मौजूद नहीं था और आप किस कोड को चलाने के लिए एक्सप्रेशन के मूल्यांकन के लिए इंतजार कर रहे थे (अगले पैराग्राफ में आगे की व्याख्या)।

if (expression)
{
    // Run 1
} else {
    // Run 2
}

जब भी एक वहाँ if-else\ switchबयान, अभिव्यक्ति निर्धारित करने के लिए जो ब्लॉक निष्पादित किया जाना चाहिए मूल्यांकन किया जाना है। संकलक द्वारा उत्पन्न विधानसभा कोड में, सशर्त शाखा निर्देश सम्मिलित किए जाते हैं।

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

कहा जा रहा है, संकलक इससे पहले कि वास्तव में मूल्यांकन किया जा रहा है के परिणाम की भविष्यवाणी करने की कोशिश करता है। यह ifब्लॉक से निर्देश लाएगा , और अगर अभिव्यक्ति सच हो जाती है, तो अद्भुत है! हमने इसका मूल्यांकन करने के लिए समय लिया और कोड में प्रगति की; यदि नहीं, तो हम गलत कोड चला रहे हैं, पाइप लाइन को फ्लश किया जाता है, और सही ब्लॉक चलाया जाता है।

दृश्य:

मान लें कि आपको मार्ग 1 या मार्ग चुनने की आवश्यकता है 2. मानचित्र की जांच के लिए अपने साथी की प्रतीक्षा में, आप ## पर रुक गए हैं और प्रतीक्षा कर रहे हैं, या आप बस मार्ग 1 चुन सकते हैं और यदि आप भाग्यशाली थे (मार्ग 1 सही मार्ग है), तब महान आपको अपने साथी के लिए नक्शे की जांच करने के लिए प्रतीक्षा करने की आवश्यकता नहीं थी (आपने उस समय को बचाया है जो उसे नक्शे की जांच करने के लिए ले जाएगा), अन्यथा आप बस वापस मुड़ जाएंगे।

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

 O      Route 1  /-------------------------------
/|\             /
 |  ---------##/
/ \            \
                \
        Route 2  \--------------------------------

जबकि फ्लशिंग पाइपलाइन सुपर फास्ट है वास्तव में नहीं। यह तेजी से कैश की तुलना में DRAM के लिए सभी तरह से याद करता है, लेकिन आधुनिक उच्च-प्रदर्शन x86 (जैसे इंटेल सैंडब्रिज-परिवार) पर यह लगभग एक दर्जन चक्र है। हालांकि तेजी से रिकवरी यह सभी पुराने स्वतंत्र निर्देशों की प्रतीक्षा करने से बचने की अनुमति देता है पुनर्प्राप्ति शुरू करने से पहले सेवानिवृत्ति तक पहुंचने के लिए, आप अभी भी एक मिसप्रिंट पर बहुत सारे फ्रंट-एंड साइकिल खो देते हैं। क्या होता है जब एक स्काइलेक सीपीयू एक शाखा को गलत बताता है? । (और प्रत्येक चक्र काम के लगभग 4 निर्देश हो सकते हैं।) उच्च-थ्रूपुट कोड के लिए खराब।
पीटर कॉर्ड्स

153

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

इस एल्गोरिथ्म के लिए आंतरिक लूप एआरएम विधानसभा भाषा में निम्नलिखित की तरह कुछ दिखेगा:

MOV R0, #0     // R0 = sum = 0
MOV R1, #0     // R1 = c = 0
ADR R2, data   // R2 = addr of data array (put this instruction outside outer loop)
.inner_loop    // Inner loop branch label
    LDRB R3, [R2, R1]     // R3 = data[c]
    CMP R3, #128          // compare R3 to 128
    ADDGE R0, R0, R3      // if R3 >= 128, then sum += data[c] -- no branch needed!
    ADD R1, R1, #1        // c++
    CMP R1, #arraySize    // compare c to arraySize
    BLT inner_loop        // Branch to inner_loop if c < arraySize

लेकिन यह वास्तव में एक बड़ी तस्वीर का हिस्सा है:

CMPopcodes हमेशा प्रोसेसर स्टेटस रजिस्टर (PSR) में स्टेटस बिट्स को अपडेट करते हैं, क्योंकि यह उनका उद्देश्य है, लेकिन अधिकांश अन्य निर्देश PSR को नहीं छूते हैं जब तक कि आप Sनिर्देश में एक वैकल्पिक प्रत्यय नहीं जोड़ते हैं , यह निर्दिष्ट करते हुए कि PSR को अपडेट किया जाना चाहिए निर्देश का परिणाम। 4-बिट स्थिति प्रत्यय की तरह, PSR को प्रभावित किए बिना निर्देशों को निष्पादित करने में सक्षम होना एक ऐसा तंत्र है जो एआरएम पर शाखाओं की आवश्यकता को कम करता है, और हार्डवेयर स्तर पर ऑर्डर प्रेषण से बाहर की सुविधा भी देता है , क्योंकि कुछ ऑपरेशन एक्स के बाद जो अपडेट करता है स्थिति बिट्स, बाद में (या समानांतर में) आप अन्य कार्यों का एक गुच्छा कर सकते हैं जो स्पष्ट रूप से स्थिति बिट्स को प्रभावित नहीं करना चाहिए, फिर आप एक्स द्वारा पहले सेट किए गए स्थिति बिट्स की स्थिति का परीक्षण कर सकते हैं।

स्थिति परीक्षण क्षेत्र और वैकल्पिक "सेट स्थिति बिट" फ़ील्ड को संयुक्त किया जा सकता है, उदाहरण के लिए:

  • ADD R1, R2, R3प्रदर्शन R1 = R2 + R3किसी भी स्थिति बिट्स अपडेट किए बिना।
  • ADDGE R1, R2, R3 केवल एक ही ऑपरेशन करता है यदि स्थिति के बिट्स को प्रभावित करने वाले पिछले निर्देश का परिणाम ग्रेटर या समान स्थिति से अधिक होता है।
  • ADDS R1, R2, R3प्रदर्शन के अलावा और फिर अद्यतन करता है N, Z, Cऔर Vप्रोसेसर स्थिति रजिस्टर पर कि क्या परिणाम नकारात्मक, ज़ीरो गए (अहस्ताक्षरित इसके लिए) था आधारित में झंडे, या (पर हस्ताक्षर किए जाने के लिए) overflowed।
  • ADDSGE R1, R2, R3यदि GEपरीक्षण सत्य है, तो केवल इसके अलावा करता है, और इसके बाद परिणाम के आधार पर स्थिति बिट्स को अद्यतन करता है।

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

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


1
एआरएम में अन्य नवाचार एस इंस्ट्रक्शन प्रत्यय के अतिरिक्त है, यह भी (लगभग) सभी निर्देशों पर वैकल्पिक है, जो यदि अनुपस्थित है, तो स्थिति बिट्स को बदलने से निर्देश रोकता है (सीएमपी निर्देश के अपवाद के साथ, जिसका काम स्थिति बिट्स सेट करना है, तो यह एस प्रत्यय की जरूरत नहीं है)। यह आपको कई मामलों में सीएमपी निर्देशों से बचने की अनुमति देता है, जब तक कि तुलना शून्य या समान के साथ होती है (जैसे। SUBS R0, R0, # 1 सेट होगा Z (शून्य) बिट जब R0 शून्य तक पहुंचता है)। सशर्त और एस प्रत्यय शून्य ओवरहेड है। यह काफी सुंदर ISA है।
ल्यूक हचिसन

2
एस प्रत्यय को न जोड़ने से आप बिना किसी चिंता के एक पंक्ति में कई सशर्त निर्देश दे सकते हैं कि उनमें से कोई भी स्थिति बिट्स को बदल सकता है, जो बाकी सशर्त निर्देशों को छोड़ देने का दुष्प्रभाव हो सकता है।
ल्यूक हचिसन

ध्यान दें कि ओपी उनके माप में सॉर्ट करने के लिए समय सहित नहीं है । एक शाखा x86 लूप चलाने से पहले इसे सॉर्ट करने के लिए संभवतः यह एक समग्र नुकसान है, भले ही गैर-सॉर्ट किया गया मामला लूप को बहुत धीमा चलाता है। लेकिन बड़े सरणी को छाँटने के लिए बहुत काम की आवश्यकता होती है ।
पीटर कॉर्ड्स

BTW, आप सरणी के अंत के सापेक्ष अनुक्रमित करके लूप में एक निर्देश सहेज सकते हैं। लूप से पहले, सेट अप करें R2 = data + arraySize, फिर से शुरू करें R1 = -arraySize। लूप के नीचे adds r1, r1, #1/ बन जाता है bnz inner_loop। कंपाइलर किसी कारण से इस अनुकूलन का उपयोग नहीं करते हैं: / लेकिन वैसे भी, इस तरह से ऐड का प्रेडिकेटेड निष्पादन मौलिक रूप से इस मामले में अलग नहीं है कि आप x86 जैसे अन्य ISAs पर शाखाविहीन कोड के साथ क्या कर सकते हैं cmov। हालाँकि यह उतना अच्छा नहीं है: gcc ऑप्टिमाइज़ेशन फ़्लैग -O3 कोड को धीमा बनाता है -O2
पीटर कॉर्ड्स

1
(एआरएम ने वास्तव में निर्देश को समर्पित किया है, इसलिए आप इसे लोड या स्टोर पर भी उपयोग कर सकते हैं, जो कि cmovएक मेमोरी सोर्स ऑपरेटर के साथ x86 के विपरीत, गलती करेगा । अधिकांश ISAs, AArch64 सहित, केवल ALU चुनिंदा ऑपरेशंस ही हैं। इसलिए ARM सिग्नल शक्तिशाली हो सकता है, और अधिकांश आईएसएएस पर शाखाहीन कोड की तुलना में अधिक कुशलता से उपयोग करने योग्य।)
पीटर कॉर्ड्स

146

यह शाखा की भविष्यवाणी के बारे में है। यह क्या है?

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

  • दूसरी ओर, जटिल शाखा की भविष्यवाणियाँ-या तो तंत्रिका आधारित या दो-स्तरीय शाखा भविष्यवाणी -प्रविद्या की बेहतर भविष्यवाणी सटीकता है, लेकिन वे अधिक शक्ति का उपभोग करती हैं और जटिलता तेजी से बढ़ती है।

  • इसके अतिरिक्त, जटिल भविष्यवाणी तकनीकों में शाखाओं की भविष्यवाणी करने में लगने वाला समय स्वयं ही बहुत अधिक होता है, जो 2 से 5 चक्रों तक होता है-जो वास्तविक शाखाओं के निष्पादन समय के बराबर होता है।

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

वास्तव में तीन अलग-अलग शाखाएँ हैं:

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

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

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

स्टेटिक / डायनेमिक ब्रांच भविष्यवाणी : स्टेटिक ब्रांच की भविष्यवाणी माइक्रोप्रोसेसर द्वारा पहली बार एक सशर्त शाखा का सामना करने के लिए उपयोग की जाती है, और डायनामिक शाखा की भविष्यवाणी का उपयोग सशर्त शाखा कोड के निष्पादन के लिए किया जाता है।

संदर्भ:


145

इस तथ्य के अलावा कि शाखा की भविष्यवाणी आपको धीमा कर सकती है, एक क्रमबद्ध सरणी का एक और फायदा है:

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

 // sort backwards (higher values first), may be in some other part of the code
 std::sort(data, data + arraySize, std::greater<int>());

 for (unsigned c = 0; c < arraySize; ++c) {
       if (data[c] < 128) {
              break;
       }
       sum += data[c];               
 }

1
सही है, लेकिन सरणी को छांटने की सेटअप लागत हे (एन लॉग एन) है, इसलिए जल्दी तोड़ने से आपको मदद नहीं मिलती है यदि आप केवल सरणी को छांट रहे हैं तो जल्दी तोड़ने में सक्षम होना चाहिए। यदि, हालांकि, आपके पास सरणी को पूर्व-सॉर्ट करने के अन्य कारण हैं, तो हाँ, यह मूल्यवान है।
ल्यूक हचिसन

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

2
हां, यह वही बिंदु है जो मैंने अपनी पहली टिप्पणी में किया है :-) आप कहते हैं "शाखा की भविष्यवाणी केवल एक बार याद आएगी।" लेकिन आप ओ (एन लॉग एन) शाखा की भविष्यवाणी को सॉर्ट एल्गोरिथ्म के अंदर याद नहीं कर रहे हैं, जो वास्तव में ओ (एन) शाखा की भविष्यवाणी से अलग है जो अनसोल्ड केस में याद आती है। तो आप को तोड़ने के लिए सॉर्ट किए गए डेटा O (लॉग एन) बार की संपूर्णता का उपयोग करने की आवश्यकता होगी (शायद वास्तव में O (10 लॉग एन के करीब), सॉर्ट एल्गोरिथ्म के आधार पर, उदाहरण के लिए क्विकॉर्ट के लिए, कैश मिस के कारण - विलय अधिक कैश-सुसंगत है, इसलिए आपको O (2 लॉग एन) को भी तोड़ने के लिए उपयोग करने की आवश्यकता होगी।)
ल्यूक हचिसन

एक महत्वपूर्ण अनुकूलन हालांकि केवल "आधा एस्कॉर्ट" करना होगा, केवल 127 के लक्ष्य धुरी मूल्य से कम वस्तुओं को छांटना ( धुरी के बाद सब कुछ कम या बराबर के बराबर माना जाता है)। एक बार जब आप धुरी पर पहुँच जाते हैं, तो धुरी से पहले तत्वों को योग करें। यह O (N log N) के बजाय O (N) स्टार्टअप समय में चलेगा, हालाँकि अभी भी बहुत सारे ब्रांच प्रीडिक्शन मिस होंगे, शायद O (5 N) के ऑर्डर के आधार पर जो मैंने पहले दिए थे, उसके बाद से यह आधा तेज है।
ल्यूक हचिसन

132

शाखा पूर्वानुमान नामक एक घटना के कारण, सॉर्ट किए गए सरणियों को एक अनसुलझी सरणी की तुलना में तेजी से संसाधित किया जाता है।

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

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

आपके प्रश्न का उत्तर बहुत सरल है।

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

क्रमबद्ध एरे: स्ट्रेट रोड ____________________________________________________________________________________________________ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - टीटीटीटीटीटीटीटीटीएसटी, टीटीएसटी, टीटी रोड,

अनरेटेड एरे: कर्व्ड रोड

______   ________
|     |__|

शाखा की भविष्यवाणी: अनुमान लगाना / भविष्यवाणी करना कि कौन सी सड़क सीधी है और बिना जाँच के उसका अनुसरण कर रही है

___________________________________________ Straight road
 |_________________________________________|Longer road

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


इसके अलावा मैं टिप्पणियों से @Simon_Weaver का हवाला देना चाहता हूं :

यह कम भविष्यवाणी नहीं करता है - यह कम गलत भविष्यवाणी करता है। यह अभी भी लूप के माध्यम से प्रत्येक समय के लिए भविष्यवाणी करना है ...


122

मैंने अपने मैकबुक प्रो (Intel i7, 64 bit, 2.4 GHz) के साथ MATLAB 2011 बी के साथ समान कोड की कोशिश की, निम्नलिखित MATLAB कोड के लिए:

% Processing time with Sorted data vs unsorted data
%==========================================================================
% Generate data
arraySize = 32768
sum = 0;
% Generate random integer data from range 0 to 255
data = randi(256, arraySize, 1);


%Sort the data
data1= sort(data); % data1= data  when no sorting done


%Start a stopwatch timer to measure the execution time
tic;

for i=1:100000

    for j=1:arraySize

        if data1(j)>=128
            sum=sum + data1(j);
        end
    end
end

toc;

ExeTimeWithSorting = toc - tic;

उपरोक्त MATLAB कोड के परिणाम निम्नानुसार हैं:

  a: Elapsed time (without sorting) = 3479.880861 seconds.
  b: Elapsed time (with sorting ) = 2377.873098 seconds.

C कोड के परिणाम जैसे @GManNickG मुझे मिलते हैं:

  a: Elapsed time (without sorting) = 19.8761 sec.
  b: Elapsed time (with sorting ) = 7.37778 sec.

इसके आधार पर, ऐसा लगता है कि MATLAB सी कार्यान्वयन के बिना लगभग 175 गुना धीमा है और छंटाई के साथ 350 गुना धीमा है। दूसरे शब्दों में, प्रभाव (शाखा भविष्यवाणी का) MATLAB कार्यान्वयन के लिए 1.46x और C कार्यान्वयन के लिए 2.7x है।


6
पूर्णता के लिए, यह संभवत: यह नहीं है कि आप मतलाब में इसे कैसे लागू करेंगे। मैं शर्त लगाता हूं कि यदि समस्या को सदिश करने के बाद यह बहुत तेजी से होता है।
ysap

1
मतलाब कई स्थितियों में स्वत: समानांतरकरण / वैश्वीकरण करता है लेकिन यहाँ मुद्दा शाखा भविष्यवाणी के प्रभाव की जाँच करना है। Matlab वैसे भी प्रतिरक्षा नहीं है!
शान

1
क्या मैटलैब मूल संख्याओं या एक मैट लैब विशिष्ट कार्यान्वयन (अंकों की अनंत राशि या तो?) का उपयोग करता है
Thorbjørn Ravn Andersen

54

अन्य उत्तरों द्वारा यह धारणा कि डेटा को क्रमबद्ध करने की आवश्यकता नहीं है, सही नहीं है।

निम्न कोड पूरे सरणी को नहीं छाँटता है, लेकिन इसके केवल 200-तत्व खंड हैं, और इस तरह यह सबसे तेज़ चलता है।

केवल k- तत्व अनुभागों को छाँटने से पूरे सरणी को छाँटने के लिए आवश्यक समय के O(n)बजाय , रैखिक समय में पूर्व-प्रसंस्करण O(n.log(n))पूरा हो जाता है।

#include <algorithm>
#include <ctime>
#include <iostream>

int main() {
    int data[32768]; const int l = sizeof data / sizeof data[0];

    for (unsigned c = 0; c < l; ++c)
        data[c] = std::rand() % 256;

    // sort 200-element segments, not the whole array
    for (unsigned c = 0; c + 200 <= l; c += 200)
        std::sort(&data[c], &data[c + 200]);

    clock_t start = clock();
    long long sum = 0;

    for (unsigned i = 0; i < 100000; ++i) {
        for (unsigned c = 0; c < sizeof data / sizeof(int); ++c) {
            if (data[c] >= 128)
                sum += data[c];
        }
    }

    std::cout << static_cast<double>(clock() - start) / CLOCKS_PER_SEC << std::endl;
    std::cout << "sum = " << sum << std::endl;
}

यह "साबित" भी करता है कि इसका किसी भी एल्गोरिदम जैसे कि क्रमबद्ध क्रम से कोई लेना-देना नहीं है, और यह वास्तव में शाखा भविष्यवाणी है।


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

यदि आप बस एल्गोरिथ्म को कुशलतापूर्वक बिना डेटा के कुशलतापूर्वक कार्यान्वित करना चाहते हैं, तो आप उस ऑपरेशन को बिना शाखा (और SIMD के साथ, उदाहरण के लिए x86 के pcmpgtbसाथ अपने उच्च बिट सेट के साथ तत्वों को खोजने के लिए करेंगे, और फिर छोटे तत्वों को शून्य करने के लिए)। वास्तव में किसी भी समय छाँटने से खर्च धीमा होगा। एक शाखा रहित संस्करण में डेटा-स्वतंत्र प्रदर्शन होगा, यह भी साबित होता है कि लागत शाखा के गलत तरीके से आई है। या बस सीधे प्रदर्शन काउंटर का उपयोग करें कि स्काइलेक की तरह int_misc.clear_resteer_cyclesया int_misc.recovery_cyclesगलतफहमी से सामने के अंत के चक्र को गिनने के लिए
पीटर

ऊपर दिए गए दोनों टिप्पणियां सामान्य एल्गोरिदम के मुद्दों और जटिलता को अनदेखा करती दिखती हैं, विशेष मशीन निर्देशों के साथ विशेष हार्डवेयर की वकालत करने के पक्ष में। मुझे लगता है कि यह विशेष रूप से मशीन निर्देशों के अंधे पक्ष में इस जवाब में महत्वपूर्ण सामान्य अंतर्दृष्टि को खारिज कर देता है कि पहले एक विशेष रूप से छोटा है।
user2297550

36

इस सवाल का जवाब बज़्ने स्ट्रॉस्ट्रुप :

यह एक साक्षात्कार प्रश्न लगता है। क्या यह सच है? आप कैसे जानते हैं? पहले कुछ माप किए बिना दक्षता के बारे में सवालों का जवाब देना एक बुरा विचार है, इसलिए यह जानना महत्वपूर्ण है कि कैसे मापना है।

इसलिए, मैंने एक मिलियन पूर्णांक के वेक्टर के साथ प्रयास किया और मिला:

Already sorted    32995 milliseconds
Shuffled          125944 milliseconds

Already sorted    18610 milliseconds
Shuffled          133304 milliseconds

Already sorted    17942 milliseconds
Shuffled          107858 milliseconds

मुझे यकीन है कि कुछ समय के लिए दौड़ा। हां, घटना वास्तविक है। मेरा मुख्य कोड था:

void run(vector<int>& v, const string& label)
{
    auto t0 = system_clock::now();
    sort(v.begin(), v.end());
    auto t1 = system_clock::now();
    cout << label 
         << duration_cast<microseconds>(t1  t0).count() 
         << " milliseconds\n";
}

void tst()
{
    vector<int> v(1'000'000);
    iota(v.begin(), v.end(), 0);
    run(v, "already sorted ");
    std::shuffle(v.begin(), v.end(), std::mt19937{ std::random_device{}() });
    run(v, "shuffled    ");
}

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

एक कारण शाखा की भविष्यवाणी है: सॉर्ट एल्गोरिथ्म में कुंजी ऑपरेशन “if(v[i] < pivot]) …”या समतुल्य है। क्रमबद्ध अनुक्रम के लिए वह परीक्षण हमेशा सत्य होता है जबकि, यादृच्छिक अनुक्रम के लिए, चुनी गई शाखा यादृच्छिक रूप से बदलती है।

एक और कारण यह है कि जब वेक्टर पहले से ही सॉर्ट किया गया है, तो हमें तत्वों को उनकी सही स्थिति में स्थानांतरित करने की आवश्यकता नहीं है। इन छोटे विवरणों का प्रभाव पांच या छह का कारक है जो हमने देखा था।

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

यदि आप कुशल कोड लिखना चाहते हैं, तो आपको मशीन वास्तुकला के बारे में थोड़ा जानना होगा।


27

यह सवाल सीपीयू के ब्रांच प्रेडिक्शन मॉडल में निहित है। मैं इस पत्र को पढ़ने की सलाह दूंगा:

एकाधिक शाखा भविष्यवाणी और एक शाखा पते कैश के माध्यम से निर्देश प्राप्त करने की दर में वृद्धि

जब आपने तत्वों को सॉर्ट किया है, तो IR को सभी CPU निर्देशों को लाने के लिए बार-बार परेशान नहीं किया जा सकता है, यह उन्हें कैश से प्राप्त करता है।


बदमाशों की परवाह किए बिना सीपीयू के एल 1 निर्देश कैश में निर्देश गर्म रहते हैं। समस्या उन्हें सही क्रम में पाइपलाइन में ला रही है , इससे पहले कि तुरंत-पिछले निर्देश डिकोड हो गए और निष्पादन समाप्त हो गया।
पीटर कॉर्ड्स

15

शाखा भविष्यवाणी त्रुटियों से बचने का एक तरीका एक लुकअप तालिका बनाना और डेटा का उपयोग करके इसे अनुक्रमित करना है। स्टीफन डी ब्रुजन ने अपने जवाब में चर्चा की।

लेकिन इस मामले में, हम जानते हैं कि मूल्य सीमा [0, 255] में हैं और हम केवल मूल्यों के बारे में परवाह करते हैं = = 128। इसका मतलब है कि हम आसानी से एक बिट को निकाल सकते हैं जो हमें बताएगा कि क्या हम मूल्य चाहते हैं या नहीं: स्थानांतरण द्वारा दाएं 7 बिट्स के लिए डेटा, हम 0 बिट या 1 बिट के साथ छोड़ दिए जाते हैं, और हम केवल 1 बिट होने पर वैल्यू जोड़ना चाहते हैं। चलो इस बिट को "निर्णय बिट" कहते हैं।

एक सरणी में एक सूचकांक के रूप में निर्णय बिट के 0/1 मूल्य का उपयोग करके, हम कोड बना सकते हैं जो समान रूप से तेज़ होगा चाहे डेटा सॉर्ट किया गया हो या नहीं। हमारा कोड हमेशा एक मूल्य जोड़ देगा, लेकिन जब निर्णय बिट 0 होता है, तो हम उस मूल्य को जोड़ देंगे जहां हम परवाह नहीं करते हैं। यहाँ कोड है:

// परीक्षा

clock_t start = clock();
long long a[] = {0, 0};
long long sum;

for (unsigned i = 0; i < 100000; ++i)
{
    // Primary loop
    for (unsigned c = 0; c < arraySize; ++c)
    {
        int j = (data[c] >> 7);
        a[j] += data[c];
    }
}

double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;
sum = a[1];

यह कोड जोड़ के आधे हिस्से को बर्बाद करता है, लेकिन कभी भी शाखा भविष्यवाणी विफलता नहीं होती है। यह एक वास्तविक विवरण के साथ संस्करण की तुलना में यादृच्छिक डेटा पर बहुत तेजी से होता है।

लेकिन मेरे परीक्षण में, एक स्पष्ट लुकअप तालिका इससे थोड़ी तेज थी, शायद इसलिए कि लुकअप तालिका में अनुक्रमण बिट शिफ्टिंग की तुलना में थोड़ा तेज था। यह दिखाता है कि मेरा कोड कैसे सेट अप करता है और लुकअप टेबल का उपयोग करता है (कोड में "लुकअप टेबल" के लिए अकल्पनीय रूप से लट कहा गया है)। यहाँ C ++ कोड है:

// घोषणा करें और फिर लुकअप टेबल में भरें

int lut[256];
for (unsigned c = 0; c < 256; ++c)
    lut[c] = (c >= 128) ? c : 0;

// Use the lookup table after it is built
for (unsigned i = 0; i < 100000; ++i)
{
    // Primary loop
    for (unsigned c = 0; c < arraySize; ++c)
    {
        sum += lut[data[c]];
    }
}

इस मामले में, लुकअप तालिका केवल 256 बाइट्स थी, इसलिए यह कैश में अच्छी तरह से फिट बैठता है और सभी तेज था। यदि डेटा 24-बिट मान था, तो यह तकनीक अच्छी तरह से काम नहीं करेगी और हम केवल उनमें से आधा चाहते थे ... लुकअप तालिका व्यावहारिक होने के लिए बहुत बड़ी होगी। दूसरी ओर, हम ऊपर दिखाए गए दो तकनीकों को जोड़ सकते हैं: पहले बिट्स को शिफ्ट करें, फिर एक लुकअप टेबल को इंडेक्स करें। 24-बिट मान के लिए, जिसे हम केवल शीर्ष आधा मूल्य चाहते हैं, हम संभावित रूप से डेटा को 12 बिट्स द्वारा दाईं ओर शिफ्ट कर सकते हैं, और टेबल इंडेक्स के लिए 12-बिट मान के साथ छोड़ा जा सकता है। एक 12-बिट टेबल इंडेक्स में 4096 मानों की तालिका होती है, जो व्यावहारिक हो सकती है।

किसी कथन का उपयोग करने के बजाय एक सरणी में अनुक्रमित करने की तकनीक का उपयोग यह तय करने के लिए किया जा सकता है कि किस सूचक का उपयोग करना है। मैंने एक पुस्तकालय देखा जिसमें बाइनरी ट्री को लागू किया गया था, और दो नामित पॉइंटर्स (pLeft और pRight या जो कुछ भी) होने के बजाय पॉइंटर्स की लंबाई -2 सरणी थी और यह तय करने के लिए "निर्णय बिट" तकनीक का उपयोग किया कि किसका पालन करना है। उदाहरण के लिए, इसके बजाय:

if (x < node->value)
    node = node->pLeft;
else
    node = node->pRight;
this library would do something like:

i = (x < node->value);
node = node->link[i];

यह एक अच्छा समाधान है शायद यह काम करेगा


आपने C ++ कंपाइलर / हार्डवेयर का क्या परीक्षण किया और किस कंपाइलर विकल्प के साथ? मुझे आश्चर्य है कि मूल संस्करण अच्छा शाखाहीन SIMD कोड के लिए ऑटो-वेक्टर नहीं हुआ। क्या आपने पूर्ण अनुकूलन सक्षम किया?
पीटर कॉर्डेस

एक 4096 प्रविष्टि लुकअप टेबल पागल लग रहा है। यदि आप किसी भी बिट को स्थानांतरित करते हैं , तो आपको मूल संख्या को जोड़ने के लिए केवल LUT परिणाम का उपयोग करने की आवश्यकता नहीं है । ये सभी ध्वनि आपके संकलक के चारों ओर काम करने के लिए मूर्खतापूर्ण तरकीबों की तरह हैं जो आसानी से शाखा रहित तकनीकों का उपयोग नहीं करते हैं। अधिक सीधा होगा mask = tmp < 128 : 0 : -1UL;/total += tmp & mask;
पीटर कॉर्ड्स
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.