क्यों (* * b! = 0) जावा में (! = 0 && b! = 0) से अधिक तेज़ है?


412

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

मैं इसके साथ मूल्यांकन कर सकता हूं

if (a != 0 && b != 0) { /* Some code */ }

या वैकल्पिक रूप से

if (a*b != 0) { /* Some code */ }

क्योंकि मुझे उम्मीद है कि कोड के उस टुकड़े को प्रति बार लाखों बार चलाना होगा, मैं सोच रहा था कि कौन सा तेज होगा। मैंने एक विशाल यादृच्छिक रूप से उत्पन्न सरणी पर उनकी तुलना करके प्रयोग किया था, और मैं यह देखने के लिए भी उत्सुक था कि सरणी की स्पार्सिटी (डेटा का अंश = 0) परिणामों को कैसे प्रभावित करेगा:

long time;
final int len = 50000000;
int arbitrary = 0;
int[][] nums = new int[2][len];

for (double fraction = 0 ; fraction <= 0.9 ; fraction += 0.0078125) {
    for(int i = 0 ; i < 2 ; i++) {
        for(int j = 0 ; j < len ; j++) {
            double random = Math.random();

            if(random < fraction) nums[i][j] = 0;
            else nums[i][j] = (int) (random*15 + 1);
        }
    }

    time = System.currentTimeMillis();

    for(int i = 0 ; i < len ; i++) {
        if( /*insert nums[0][i]*nums[1][i]!=0 or nums[0][i]!=0 && nums[1][i]!=0*/ ) arbitrary++;
    }
    System.out.println(System.currentTimeMillis() - time);
}

और परिणाम बताते हैं कि यदि आप "ए" या "बी" की अपेक्षा करते हैं, तो a*b != 0यह समय के ~ 3% से अधिक 0 के बराबर है, यह तेजी से होता है a!=0 && b!=0:

AND b गैर-शून्य के परिणामों का ग्राफिकल ग्राफ

मैं यह जानने के लिए उत्सुक हूं कि क्यों। क्या कोई प्रकाश डाल सकता है? क्या यह संकलक है या यह हार्डवेयर स्तर पर है?

संपादित करें: जिज्ञासा से बाहर ... अब जब मैंने शाखा की भविष्यवाणी के बारे में जाना, तो मैं सोच रहा था कि ओआर या बी के लिए एनालॉग तुलना क्या दिखाएगी गैर-शून्य:

एक या बी गैर-शून्य का ग्राफ

हम उम्मीद के अनुसार शाखा भविष्यवाणी का एक ही प्रभाव देखते हैं, दिलचस्प है कि ग्राफ एक्स-एक्सिस के साथ कुछ हद तक फ़्लिप किया गया है।

अपडेट करें

1- मैंने !(a==0 || b==0)विश्लेषण में जोड़ा कि क्या होता है।

2- मैंने शाखा भविष्यवाणी के बारे में जानने के बाद a != 0 || b != 0, (a+b) != 0और (a|b) != 0जिज्ञासा से बाहर भी शामिल किया । लेकिन वे तार्किक रूप से अन्य अभिव्यक्तियों के समतुल्य नहीं होते हैं, क्योंकि सत्य होने के लिए केवल एक या बी को गैर-शून्य होने की आवश्यकता होती है, इसलिए उन्हें प्रसंस्करण दक्षता के लिए तुलना करने के लिए नहीं है।

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

4- कुछ लोग भविष्यवाणी के a != 0 & b != 0विपरीत शामिल करने का सुझाव दे रहे थे a != 0 && b != 0, इस भविष्यवाणी के साथ कि यह अधिक निकटता से व्यवहार करेगा a*b != 0क्योंकि हम शाखा पूर्वानुमान प्रभाव को हटा देंगे। मुझे नहीं पता था कि &बूलियन चर के साथ इस्तेमाल किया जा सकता है, मुझे लगा कि यह केवल पूर्णांक के साथ द्विआधारी संचालन के लिए उपयोग किया गया था।

नोट: इस संदर्भ में कि मैं इस सब पर विचार कर रहा था, int overflow कोई समस्या नहीं है, लेकिन यह सामान्य संदर्भों में एक महत्वपूर्ण विचार है।

CPU: Intel Core i7-3610QM @ 2.3GHz

जावा संस्करण: 1.8.0_45
जावा (TM) एसई रनटाइम एनवायरनमेंट (बिल्ड 1.8.0_45-b14)
जावा हॉटस्पॉट (TM) 64-बिट सर्वर VM (बिल्ड 25.45-b02, मिश्रित मोड)


11
किस बारे में if (!(a == 0 || b == 0))? माइक्रोबेन्चमार्क बहुत ही अविश्वसनीय रूप से अविश्वसनीय हैं, यह वास्तव में औसत दर्जे का होने की संभावना नहीं है (~ 3% मेरे लिए त्रुटि के एक मार्जिन की तरह लगता है)।
इलियट फ्रिश

9
या a != 0 & b != 0
लुई वासरमैन

16
यदि भविष्यवाणी की गई शाखा गलत है तो ब्रांचिंग धीमी है। a*b!=0एक कम शाखा है
इरविन बोलविएट

19
(1<<16) * (1<<16) == 0अभी तक दोनों शून्य से अलग हैं।
कोडइन्चोज

13
@Gene: आपका प्रस्तावित अनुकूलन मान्य नहीं है। यहां तक कि अतिप्रवाह अनदेखी, a*bशून्य अगर है एक की aऔर bशून्य है; a|bदोनों हैं तो ही शून्य है।
हमखोलम ने मोनिका

जवाबों:


240

मैं इस मुद्दे की अनदेखी कर रहा हूं कि आपकी बेंचमार्किंग त्रुटिपूर्ण हो सकती है, और परिणाम का सामना कर सकते हैं।

क्या यह संकलक है या यह हार्डवेयर स्तर पर है?

मुझे लगता है कि बाद में:

  if (a != 0 && b != 0)

2 मेमोरी लोड और दो सशर्त शाखाओं को संकलित करेगा

  if (a * b != 0)

2 मेमोरी लोड, एक गुणा और एक सशर्त शाखा के लिए संकलित करेगा।

यदि हार्डवेयर स्तर की शाखा की भविष्यवाणी अप्रभावी है तो गुणा दूसरी सशर्त शाखा से तेज होने की संभावना है। जैसे-जैसे आप अनुपात बढ़ाते हैं ... शाखा की भविष्यवाणी कम प्रभावी होती जा रही है।

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

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


हालांकि, एक बात यह है कि इस अनुकूलन के साथ आपको सावधान रहने की आवश्यकता है। क्या कोई मूल्य हैं जहां a * b != 0गलत उत्तर देंगे? उन मामलों पर विचार करें जहां कंप्यूटिंग के परिणाम से पूर्णांक ओवरफ़्लो होता है।


अपडेट करें

आपके ग्राफ़ मैं क्या कहा पुष्टि करने के लिए करते हैं।

  • सशर्त शाखा a * b != 0मामले में एक "शाखा भविष्यवाणी" प्रभाव भी है , और यह रेखांकन में सामने आता है।

  • यदि आप X- अक्ष पर 0.9 से आगे घटता प्रोजेक्ट करते हैं, तो यह 1 जैसा दिखता है) वे लगभग 1.0 और 2 पर मिलेंगे) बैठक बिंदु X = 0.0 के समान लगभग Y मूल्य पर होगा।


अद्यतन २

मुझे समझ में नहीं आता कि क्यों घटता है a + b != 0और a | b != 0मामलों के लिए अलग हैं । वहाँ हो सकता है कुछ शाखा भविष्यवक्ताओं तर्क में चालाक। या यह कुछ और संकेत कर सकता है।

(ध्यान दें कि इस तरह की चीज एक विशेष चिप मॉडल नंबर या यहां तक ​​कि संस्करण के लिए विशिष्ट हो सकती है। आपके बेंचमार्क के परिणाम अन्य सिस्टम पर हो सकते हैं।)

हालांकि, वे दोनों के सभी गैर नकारात्मक मूल्यों के लिए काम करने का लाभ है aऔर b


1
@DebosmitRay - 1) कोई SW नहीं होना चाहिए। मध्यवर्ती परिणाम एक रजिस्टर में रखे जाएंगे। 2) दूसरे मामले में, दो उपलब्ध शाखाएं हैं: एक "कुछ कोड" को निष्पादित करने के लिए और दूसरा उसके बाद अगले स्टेटमेंट पर जाने के लिए if
स्टीफन सी

1
@StephenC आप के बारे में ए + बी और एक भ्रमित होने की सही कह रहे हैं | ख, क्योंकि घटता हैं ही, मुझे लगता है कि यह रंग वास्तव में करीब होने है। अंधे लोगों को रंगने के लिए माफी!
मलजम

3
@ njzk2 संभावना के नजरिए से उन मामलों में 50% (के शून्य की संभावना पर धुरी के अनुसार सममित होना चाहिए a&bऔर a|b)। वे हैं, लेकिन पूरी तरह से नहीं, यही पहेली है।
एंटोनिन लेजसेक

3
@StephenC क्यों a*b != 0और a+b != 0बेंचमार्क अलग-अलग है इसका कारण a+b != 0यह है कि यह बिल्कुल भी समान नहीं है और इसे कभी भी बेंचमार्क नहीं किया जाना चाहिए। उदाहरण के लिए, a = 1, b = 0पहली अभिव्यक्ति झूठी का मूल्यांकन करती है, लेकिन दूसरा सच का मूल्यांकन करता है। गुणा एक और ऑपरेटर की तरह कार्य करता है , जबकि ऐड किसी ऑपरेटर या ऑपरेटर की तरह काम करता है ।
जेएस 1

2
@ AntonínLejsek मुझे लगता है कि संभावनाएँ भिन्न होंगी। यदि आपके पास nशून्य है तो दोनों की संभावना aऔर bशून्य के साथ बढ़ जाती है n। एक ANDऑपरेशन में, nउनमें से एक के गैर-शून्य होने की संभावना बढ़ जाती है और स्थिति पूरी हो जाती है। यह एक ORऑपरेशन के लिए विपरीत है (दोनों में से किसी एक की संभावना शून्य के साथ बढ़ जाती है n)। यह गणितीय दृष्टिकोण पर आधारित है। मुझे यकीन नहीं है कि अगर हार्डवेयर कैसे काम करता है।
WYSIWYG

70

मुझे लगता है कि आपके बेंचमार्क में कुछ खामियां हैं और वास्तविक कार्यक्रमों के बारे में बताने के लिए यह उपयोगी नहीं हो सकता है। यहाँ मेरे विचार हैं:

  • (a|b)!=0और (a+b)!=0परीक्षण करें यदि दोनों में से कोई भी मूल्य शून्य-रहित है, a != 0 && b != 0और (a*b)!=0यदि दोनों गैर-शून्य हैं तो परीक्षण करें । तो आप सिर्फ अंकगणित के समय की तुलना नहीं कर रहे हैं: यदि स्थिति अधिक बार सच है, तो यह ifशरीर के अधिक निष्पादन का कारण बनता है, जिसमें अधिक समय भी लगता है।

  • (a+b)!=0 सकारात्मक और नकारात्मक मानों के लिए गलत काम करेंगे जो शून्य के बराबर हैं, इसलिए आप इसे सामान्य मामले में उपयोग नहीं कर सकते, भले ही यह यहां काम करता हो।

  • इसी तरह, (a*b)!=0उन मूल्यों के लिए गलत काम करेंगे जो अतिप्रवाह करते हैं। (रैंडम उदाहरण: 196608 * 327680 0 है क्योंकि सही परिणाम 2 32 से विभाज्य होता है , इसलिए इसके कम 32 बिट 0 हैं, और ये बिट्स आप सभी को मिलेंगे यदि यह एक intऑपरेशन है।)

  • वीएम बाहरी ( fraction) लूप के पहले कुछ रनों के दौरान अभिव्यक्ति का अनुकूलन करेगा , जब fraction0 होता है, जब शाखाएं लगभग कभी नहीं ली जाती हैं। यदि आप fraction0.5 से शुरू करते हैं, तो ऑप्टिमाइज़र अलग-अलग काम कर सकता है ।

  • जब तक वीएम यहां कुछ सरणी सीमा की जांच को समाप्त करने में सक्षम नहीं है, तब तक सीमा की जांच के कारण अभिव्यक्ति में चार अन्य शाखाएं हैं, और यह एक जटिल कारक है जब यह पता लगाने की कोशिश की जा रही है कि निम्न स्तर पर क्या हो रहा है। आप अलग अलग परिणाम प्राप्त हो सकता है अगर आप दो फ्लैट विन्यास में, दो आयामी सरणी विभाजित है, बदल रहा है nums[0][i]और nums[1][i]करने के लिए nums0[i]और nums1[i]

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

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

  • System.currentTimeMillis()अधिकांश प्रणालियों पर +/- 10 एमएस से अधिक सटीक नहीं है। System.nanoTime()आमतौर पर अधिक सटीक होता है।

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

यदि आप वीएम द्वारा उत्पन्न मशीन कोड को अलग कर सकते हैं , तो यह अनुमान लगाने की कोशिश करें कि यह क्या करता है!


24

यहाँ उत्तर अच्छे हैं, हालाँकि मुझे इस बात का अंदाज़ा था कि इससे चीज़ें बेहतर हो सकती हैं।

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

bool aNotZero = (nums[0][i] != 0);
bool bNotZero = (nums[1][i] != 0);
if (aNotZero && bNotZero) { /* Some code */ }

यह करने के लिए भी काम कर सकते हैं

int a = nums[0][i];
int b = nums[1][i];
if (a != 0 && b != 0) { /* Some code */ }

इसका कारण यह है कि शॉर्ट सर्किटिंग के नियमों के अनुसार, यदि पहला बूलियन गलत है, तो दूसरे का मूल्यांकन नहीं किया जाना चाहिए। यह मूल्यांकन करने से बचने के लिए एक अतिरिक्त शाखा का पालन किया है nums[1][i], तो nums[0][i]गलत था। अब, आप परवाह नहीं कर सकते हैं कि nums[1][i]मूल्यांकन किया जाता है, लेकिन संकलक निश्चित नहीं हो सकता है कि यह आपके द्वारा किए गए सीमा या अशक्त रेफरी को बाहर नहीं फेंकेगा। यदि ब्लॉक को सरल बूल्स पर कम करके, कंपाइलर को यह महसूस करने के लिए पर्याप्त स्मार्ट हो सकता है कि दूसरे बूलियन का मूल्यांकन अनावश्यक रूप से नकारात्मक दुष्प्रभाव नहीं होगा।


3
हालांकि मुझे लग रहा है कि यह काफी सवाल का जवाब नहीं है
पियरे अरलाउड

3
यह गैर-शाखा से तर्क को बदलने के बिना एक शाखा शुरू करने का एक तरीका है (यदि आप जिस तरह से प्राप्त हुए हैं aऔर bइसके साइड-इफेक्ट हैं जिन्हें आपने रखा होगा)। आपके पास अभी भी है &&तो आपके पास अभी भी एक शाखा है।
जॉन हैना

11

जब हम गुणन लेते हैं, भले ही एक संख्या 0 हो, तब उत्पाद 0. होता है जबकि लिखते हैं

    (a*b != 0)

यह उत्पाद के परिणाम का मूल्यांकन करता है, जिससे 0. से शुरू होने वाली पुनरावृत्ति की पहली कुछ घटनाओं को समाप्त किया जाता है। इसके परिणामस्वरूप तुलना उस स्थिति से कम है जब

   (a != 0 && b != 0)

जहां हर तत्व की तुलना 0 से की जाती है और उसका मूल्यांकन किया जाता है। इसलिए आवश्यक समय कम है। लेकिन मेरा मानना ​​है कि दूसरी स्थिति आपको अधिक सटीक समाधान दे सकती है।


4
दूसरी अभिव्यक्ति में यदि aशून्य है तो bमूल्यांकन की आवश्यकता नहीं है क्योंकि पूरी अभिव्यक्ति पहले से ही झूठी है। इसलिए हर तत्व की तुलना सही नहीं है।
कुबा व्येरोसेक

9

आप यादृच्छिक इनपुट डेटा का उपयोग कर रहे हैं जो शाखाओं को अप्रत्याशित बनाता है। व्यवहारिक शाखाओं में प्रायः (~ 90%) पूर्वानुमेय होता है, इसलिए वास्तविक कोड में शाखात्मक कोड तेजी से होने की संभावना है।

ने कहा कि। मैं नहीं देखता कि कैसे a*b != 0तेजी से हो सकता है (a|b) != 0। आम तौर पर पूर्णांक गुणन बिटवाइस या की तुलना में अधिक महंगा होता है। लेकिन इस तरह की चीजें कभी-कभी अजीब हो जाती हैं। उदाहरण के लिए देखें "उदाहरण 7: हार्डवेयर जटिलताएँ" उदाहरण कैस्टर इफेक्ट्स की गैलरी से


2
&एक "बिटवाइज़ या" नहीं है, लेकिन (इस मामले में) एक "तार्किक और" है क्योंकि दोनों ऑपरेंड |
बूलियन

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