<< >> गुणन और विभाजन की गति


9

आप संख्याओं <<को गुणा करने के लिए और >>अजगर में संख्याओं को विभाजित करने के लिए उपयोग कर सकते हैं जब मुझे लगता है कि मैं उन्हें बाइनरी शिफ्ट के तरीके का उपयोग कर पाता हूं तो यह नियमित तरीके से विभाजित या गुणा करने की तुलना में 10 गुना तेज है।

क्यों का उपयोग कर रहा है <<और >>बहुत तेजी से *और से /?

बनाने *और /इतनी धीमी गति से होने वाली दृश्य प्रक्रियाओं के पीछे क्या हैं ?


2
बिट शिफ्ट सभी भाषाओं में तेज है, न कि केवल पायथन। कई प्रोसेसर में एक देशी बिट शिफ्ट निर्देश होता है जो इसे एक या दो घड़ी चक्रों में पूरा करेगा।
रॉबर्ट हार्वे

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

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

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

2
में इस सवाल StackOverflow पर एक microbenchmark थोड़ा पाया बेहतर प्रदर्शन अजगर 3 में गुणन के लिए 2 से एक बराबर बाईं बदलाव के लिए की तुलना में, छोटे पर्याप्त संख्या के लिए। मुझे लगता है कि मैंने इस कारण को छोटी-छोटी गुणाओं (वर्तमान में) से घटाकर अलग-अलग रूप में बदल दिया है। बस यह दिखाने के लिए जाता है कि आप थ्योरी के आधार पर तेजी से भाग सकते हैं।
डैन गेट्ज़

जवाबों:


15

चलो दो छोटे सी कार्यक्रमों को देखते हैं जो थोड़ा बदलाव और एक विभाजन करते हैं।

#include <stdlib.h>

int main(int argc, char* argv[]) {
        int i = atoi(argv[0]);
        int b = i << 2;
}
#include <stdlib.h>

int main(int argc, char* argv[]) {
        int i = atoi(argv[0]);
        int d = i / 4;
}

इसके बाद प्रत्येक gcc -Sको यह देखने के लिए संकलित किया जाता है कि वास्तविक विधानसभा क्या होगी।

बिट शिफ्ट संस्करण के साथ, कॉल atoiसे वापस आने के लिए:

    callq   _atoi
    movl    $0, %ecx
    movl    %eax, -20(%rbp)
    movl    -20(%rbp), %eax
    shll    $2, %eax
    movl    %eax, -24(%rbp)
    movl    %ecx, %eax
    addq    $32, %rsp
    popq    %rbp
    ret

जबकि विभाजन संस्करण:

    callq   _atoi
    movl    $0, %ecx
    movl    $4, %edx
    movl    %eax, -20(%rbp)
    movl    -20(%rbp), %eax
    movl    %edx, -28(%rbp)         ## 4-byte Spill
    cltd
    movl    -28(%rbp), %r8d         ## 4-byte Reload
    idivl   %r8d
    movl    %eax, -24(%rbp)
    movl    %ecx, %eax
    addq    $32, %rsp
    popq    %rbp
    ret

बस इसे देखकर बिट शिफ्ट की तुलना में डिवाइड संस्करण में कई और निर्देश हैं।

कुंजी है कि वे क्या करते हैं?

बिट शिफ्ट संस्करण में प्रमुख निर्देश है shll $2, %eaxजो कि एक बदलाव है जो तार्किक रूप से छोड़ा गया है - इसमें विभाजन होता है, और बाकी सब बस मानों को घूम रहा है।

डिवाइड संस्करण में, आप देख सकते हैं idivl %r8d- लेकिन इसके ठीक ऊपर एक cltd(कन्वर्ट टू लॉन्ग टू डबल) और स्पिल और रीलोड के आसपास कुछ अतिरिक्त तर्क हैं। यह अतिरिक्त कार्य, यह जानना कि हम बिट्स के बजाय एक गणित के साथ काम कर रहे हैं, अक्सर बिट बिट गणित करके विभिन्न त्रुटियों से बचने के लिए आवश्यक है।

कुछ त्वरित गुणा करने दें:

#include <stdlib.h>

int main(int argc, char* argv[]) {
    int i = atoi(argv[0]);
    int b = i >> 2;
}
#include <stdlib.h>

int main(int argc, char* argv[]) {
    int i = atoi(argv[0]);
    int d = i * 4;
}

इन सब से गुजरने के बजाय, एक लाइन अलग है:

$ भिन्न बहु। bit.s
24c24
> shll $ 2,% eax
---
<sarl $ 2,% eax

यहां कंपाइलर यह पहचानने में सक्षम था कि गणित को एक बदलाव के साथ किया जा सकता है, हालांकि एक तार्किक बदलाव के बजाय यह एक अंकगणितीय बदलाव करता है। यदि हम इन्हें चलाते हैं तो इन दोनों के बीच का अंतर स्पष्ट होगा - sarlसंकेत को संरक्षित करता है। ताकि -2 * 4 = -8जब shllन हो।

आइए इसे एक त्वरित पर्ल स्क्रिप्ट में देखें:

#!/usr/bin/perl

$foo = 4;
print $foo << 2, "\n";
print $foo * 4, "\n";

$foo = -4;
print $foo << 2, "\n";
print $foo * 4, "\n";

आउटपुट:

16
16
18446744073709551600
-16

उम ... -4 << 2है 18446744073709551600जो वास्तव में नहीं है क्या आप की संभावना की उम्मीद कर रहे हैं जब गुणा और भाग के साथ काम कर। इसका अधिकार है, लेकिन इसका पूर्णांक गुणन नहीं है।

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


12
यह स्पष्ट हो सकता है कि शिफ्ट दिशाओं को प्रत्येक उदाहरण के << 2साथ जोड़े रखने के लिए * 4और उसके >> 2साथ जोड़ा जाए / 4
ग्रेग हेवगिल

5

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

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

तो चलिए विस्तार से देखते हैं "पूर्ण" ऑपरेटरों की तरह गुणा और स्थानांतरण।

स्थानांतरण

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

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

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

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

गुणन

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

वे महत्वपूर्ण हैं कि आधुनिक लैपटॉप चिप्स (या बेहतर) पर, गुणा एक तेज ऑपरेशन है, और 3 या 4 निर्देश अनुक्रम की तुलना में तेज होने की संभावना है जो एक संकलक ताकत कम पारियों के लिए "गोलाई" को सही करने के लिए जारी करेगा। इंटेल पर परिवर्तनशील बदलावों के लिए, उपर्युक्त मुद्दों के कारण गुणा को भी आमतौर पर पसंद किया जाएगा।

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

फूट डालो

विभाजित करना गुणन की तुलना में अधिक जटिल ऑपरेशन, हार्डवेयर-वार दोनों है, और वास्तविक कोड में भी बहुत कम सामान्य है - जिसका अर्थ है कि कम संसाधनों को इसके लिए आवंटित किया जाता है। आधुनिक चिप्स में रुझान अभी भी तेजी से डिवाइडर की ओर है, लेकिन यहां तक ​​कि आधुनिक टॉप-ऑफ-द-लाइन चिप्स एक विभाजन करने के लिए 10-40 चक्र लेते हैं, और वे केवल आंशिक रूप से पाइपलाइज्ड होते हैं। सामान्य तौर पर, 64-बिट डिवाइसेज़ 32-बिट डिवाइसेज़ की तुलना में धीमे होते हैं। अधिकांश अन्य परिचालनों के विपरीत, विभाजन तर्कों के आधार पर चक्रों की एक चर संख्या ले सकता है।

विभाजनों से बचें और पाली के साथ बदलें (या संकलक को ऐसा करने दें, लेकिन आपको विधानसभा की जांच करने की आवश्यकता हो सकती है) यदि आप कर सकते हैं!


2

BINARY_LSHIFT और BINARY_RSHIFT सरल रूप से BINARY_MULTIPLY और BINARY_FLOOR_DIVIDE की तुलना में अलग-अलग प्रक्रियाएं हैं और कम-घड़ी चक्र ले सकती हैं। यही कारण है कि यदि आपके पास कोई बाइनरी नंबर है और एन द्वारा बिटशिफ्ट करने की आवश्यकता है, तो आपको बस इतना करना है कि कई स्थानों पर अंकों को स्थानांतरित करना है और शून्य से बदलना है। बाइनरी गुणा सामान्य रूप से अधिक जटिल है , हालांकि दद्दा गुणक जैसी तकनीक इसे काफी तेज बनाती है।

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

>>> dis.dis(lambda x: x*4)
  1           0 LOAD_FAST                0 (x)
              3 LOAD_CONST               1 (4)
              6 BINARY_MULTIPLY     
              7 RETURN_VALUE        

>>> dis.dis(lambda x: x<<2)
  1           0 LOAD_FAST                0 (x)
              3 LOAD_CONST               1 (2)
              6 BINARY_LSHIFT       
              7 RETURN_VALUE        


>>> dis.dis(lambda x: x//2)
  1           0 LOAD_FAST                0 (x)
              3 LOAD_CONST               1 (2)
              6 BINARY_FLOOR_DIVIDE 
              7 RETURN_VALUE        

>>> dis.dis(lambda x: x>>1)
  1           0 LOAD_FAST                0 (x)
              3 LOAD_CONST               1 (1)
              6 BINARY_RSHIFT       
              7 RETURN_VALUE        

हालाँकि, मेरे प्रोसेसर पर, मुझे गुणा और बाएँ / दाएँ शिफ्ट में समान समय लगता है, और फर्श विभाजन (दो की शक्ति से) लगभग 25% धीमा है:

>>> import timeit

>>> timeit.repeat("z=a + 4", setup="a = 37")
[0.03717184066772461, 0.03291916847229004, 0.03287005424499512]

>>> timeit.repeat("z=a - 4", setup="a = 37")
[0.03534698486328125, 0.03207516670227051, 0.03196907043457031]

>>> timeit.repeat("z=a * 4", setup="a = 37")
[0.04594111442565918, 0.0408930778503418, 0.045324087142944336]

>>> timeit.repeat("z=a // 4", setup="a = 37")
[0.05412912368774414, 0.05091404914855957, 0.04910898208618164]

>>> timeit.repeat("z=a << 2", setup="a = 37")
[0.04751706123352051, 0.04259490966796875, 0.041903018951416016]

>>> timeit.repeat("z=a >> 2", setup="a = 37")
[0.04719185829162598, 0.04201006889343262, 0.042105913162231445]
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.