परिणाम क्या है कोई फर्क नहीं पड़ता शून्य से सबसे तेज़ पूर्णांक विभाजन सहायक विभाग क्या है?


109

सारांश:

मैं गणना करने के लिए सबसे तेज़ तरीका ढूंढ रहा हूं

(int) x / (int) y

के लिए एक अपवाद प्राप्त किए बिना y==0। इसके बजाय मैं सिर्फ एक मनमाना परिणाम चाहता हूं।


पृष्ठभूमि:

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


विवरण:

मैं कुछ इस तरह की तलाश में हूँ:

result = (y==0)? 0 : x/y;

या

result = x / MAX( y, 1 );

x और y धनात्मक पूर्णांक हैं। कोड को नेस्टेड लूप में कई बार निष्पादित किया जाता है, इसलिए मैं सशर्त ब्रांचिंग से छुटकारा पाने का एक तरीका ढूंढ रहा हूं।

जब y बाइट रेंज से अधिक नहीं है, तो मैं समाधान से खुश हूं

unsigned char kill_zero_table[256] = { 1, 1, 2, 3, 4, 5, 6, 7, [...] 255 };
[...]
result = x / kill_zero_table[y];

लेकिन यह स्पष्ट रूप से बड़ी रेंज के लिए अच्छी तरह से काम नहीं करता है।

मुझे लगता है कि अंतिम प्रश्न यह है: सबसे तेज़ बिट ट्विगलिंग हैक 0 को किसी अन्य पूर्णांक मान में बदल रहा है, जबकि अन्य सभी मूल्यों को अपरिवर्तित छोड़ रहा है?


स्पष्टीकरण

मुझे 100% यकीन नहीं है कि ब्रांचिंग बहुत महंगी है। हालांकि, विभिन्न संकलक का उपयोग किया जाता है, इसलिए मैं थोड़ा अनुकूलन (जो वास्तव में संदिग्ध है) के साथ बेंचमार्किंग पसंद करता हूं।

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

कोड पूरी तरह से सी संगत होना चाहिए, मुख्य प्लेटफॉर्म लिनक्स 64 बिट हैं gcc और क्लैंग और मैकओएस के साथ।


22
आपने यह कैसे निर्धारित किया है कि यदि शाखा बहुत महंगी है?
djechlin

7
आप कैसे निर्धारित किया है कि वहाँ है एक शाखा?
leemes

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

8
"व्हाट्सएप सबसे तेज बिट्स हैकिंग ..." हो सकता है y += !y? गणना करने के लिए किसी शाखा की आवश्यकता नहीं है। आप के x / (y + !y)खिलाफ तुलना कर सकते हैं x / max(y, 1)और शायद भी y ? (x/y) : 0। मुझे लगता है कि उनमें से किसी में भी कोई शाखा नहीं होगी, कम से कम अनुकूलन के साथ चालू।
leemes

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

जवाबों:


107

कुछ टिप्पणियों से प्रेरित होकर मैंने अपने पेंटियम और gccसंकलक का उपयोग करके शाखा से छुटकारा पा लिया

int f (int x, int y)
{
        y += y == 0;
        return x/y;
}

कंपाइलर मूल रूप से यह पहचानता है कि यह जोड़ में परीक्षण के एक शर्त ध्वज का उपयोग कर सकता है।

विधानसभा के अनुरोध के अनुसार:

.globl f
    .type   f, @function
f:
    pushl   %ebp
    xorl    %eax, %eax
    movl    %esp, %ebp
    movl    12(%ebp), %edx
    testl   %edx, %edx
    sete    %al
    addl    %edx, %eax
    movl    8(%ebp), %edx
    movl    %eax, %ecx
    popl    %ebp
    movl    %edx, %eax
    sarl    $31, %edx
    idivl   %ecx
    ret

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

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

एआरएम संकलक के साथ फिलिप का संस्करण:

f PROC
        CMP      r1,#0
        BNE      __aeabi_idivmod
        MOVEQ    r0,#0
        BX       lr

जीसीसी के साथ फिलिप का संस्करण:

f:
        subs    r3, r1, #0
        str     lr, [sp, #-4]!
        moveq   r0, r3
        ldreq   pc, [sp], #4
        bl      __divsi3
        ldr     pc, [sp], #4

एआरएम संकलक के साथ मेरा कोड:

f PROC
        RSBS     r2,r1,#1
        MOVCC    r2,#0
        ADD      r1,r1,r2
        B        __aeabi_idivmod

GCC के साथ मेरा कोड:

f:
        str     lr, [sp, #-4]!
        cmp     r1, #0
        addeq   r1, r1, #1
        bl      __divsi3
        ldr     pc, [sp], #4

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


क्या आप हमें परिणामी कोडांतरक कोड दिखा सकते हैं? या आपने यह कैसे निर्धारित किया कि कोई शाखा नहीं है?
हाटसची

1
बहुत बढ़िया। बनाया जा सकता है constexprऔर इस तरह के अनावश्यक प्रकारों से बच सकते हैं : template<typename T, typename U> constexpr auto fdiv( T t, U u ) -> decltype(t/(u+!u)) { return t/(u+!u); } और यदि आप चाहते हैं 255, तो(lhs)/(rhs+!rhs) & -!rhs
यक - एडम नेवरुमोंट

1
@leemes लेकिन मेरा मतलब |यह नहीं था &। Ooops - यदि है , और यदि ( (lhs)/(rhs+!rhs) ) | -!rhsआपका मान सेट करना चाहिए । 0xFFFFFFFrhs0lhs/rhsrhs!=0
यक्क - एडम नेवरुमोंट

1
यह बहुत चालाक था।
थियोडोरोस चटजिआनकिंस

1
बहुत बढ़िया जवाब! मैं आमतौर पर इस प्रकार की चीजों के लिए असेंबली का सहारा लेता हूं, लेकिन यह हमेशा बनाए रखने के लिए भयानक है (कम पोर्टेबल का उल्लेख नहीं है;))।
सिंह

20

GCC 4.7.2 का उपयोग करते हुए विंडोज पर कुछ ठोस नंबर दिए गए हैं:

#include <stdio.h>
#include <stdlib.h>

int main()
{
  unsigned int result = 0;
  for (int n = -500000000; n != 500000000; n++)
  {
    int d = -1;
    for (int i = 0; i != ITERATIONS; i++)
      d &= rand();

#if CHECK == 0
    if (d == 0) result++;
#elif CHECK == 1
    result += n / d;
#elif CHECK == 2
    result += n / (d + !d);
#elif CHECK == 3
    result += d == 0 ? 0 : n / d;
#elif CHECK == 4
    result += d == 0 ? 1 : n / d;
#elif CHECK == 5
    if (d != 0) result += n / d;
#endif
  }
  printf("%u\n", result);
}

ध्यान दें कि मैं जानबूझकर फोन नहीं कर रहा हूं srand(), ताकि rand()हमेशा वही परिणाम आए। ध्यान दें कि -DCHECK=0केवल शून्य को गिनता है, ताकि यह स्पष्ट हो कि कितनी बार दिखाई दिया।

अब, इसे विभिन्न तरीकों से संकलित और समयबद्ध करना:

$ for it in 0 1 2 3 4 5; do for ch in 0 1 2 3 4 5; do gcc test.cc -o test -O -DITERATIONS=$it -DCHECK=$ch && { time=`time ./test`; echo "Iterations $it, check $ch: exit status $?, output $time"; }; done; done

एक तालिका में संक्षेपित किया जा सकने वाला आउटपुट दिखाता है:

Iterations  | 0        | 1        | 2        | 3         | 4         | 5
-------------+-------------------------------------------------------------------
Zeroes       | 0        | 1        | 133173   | 1593376   | 135245875 | 373728555
Check 1      | 0m0.612s | -        | -        | -         | -         | -
Check 2      | 0m0.612s | 0m6.527s | 0m9.718s | 0m13.464s | 0m18.422s | 0m22.871s
Check 3      | 0m0.616s | 0m5.601s | 0m8.954s | 0m13.211s | 0m19.579s | 0m25.389s
Check 4      | 0m0.611s | 0m5.570s | 0m9.030s | 0m13.544s | 0m19.393s | 0m25.081s
Check 5      | 0m0.612s | 0m5.627s | 0m9.322s | 0m14.218s | 0m19.576s | 0m25.443s

यदि शून्य दुर्लभ हैं, तो -DCHECK=2संस्करण खराब प्रदर्शन करता है। जैसे-जैसे शून्य अधिक दिखाई देने लगते हैं, वैसे-वैसे -DCHECK=2मामला बेहतर प्रदर्शन करने लगता है। अन्य विकल्पों में से, वास्तव में बहुत अंतर नहीं है।

के लिए -O3, हालांकि, यह एक अलग कहानी है:

Iterations  | 0        | 1        | 2        | 3         | 4         | 5
-------------+-------------------------------------------------------------------
Zeroes       | 0        | 1        | 133173   | 1593376   | 135245875 | 373728555
Check 1      | 0m0.646s | -        | -        | -         | -         | -
Check 2      | 0m0.654s | 0m5.670s | 0m9.905s | 0m14.238s | 0m17.520s | 0m22.101s
Check 3      | 0m0.647s | 0m5.611s | 0m9.085s | 0m13.626s | 0m18.679s | 0m25.513s
Check 4      | 0m0.649s | 0m5.381s | 0m9.117s | 0m13.692s | 0m18.878s | 0m25.354s
Check 5      | 0m0.649s | 0m6.178s | 0m9.032s | 0m13.783s | 0m18.593s | 0m25.377s

वहां, चेक 2 में अन्य चेक की तुलना में कोई कमी नहीं है, और यह लाभ को बनाए रखता है क्योंकि जीरो अधिक सामान्य हो जाता है।

आपको वास्तव में यह देखने के लिए मापना चाहिए कि आपके संकलक और आपके प्रतिनिधि नमूना डेटा के साथ क्या होता है।


4
50% प्रविष्टियाँ d=0बेतरतीब ढंग से करें, बजाय इसे लगभग हमेशा बनाने के d!=0, और आपको अधिक शाखा पूर्वानुमान विफलताएँ दिखाई देंगी। शाखा की भविष्यवाणी बहुत अच्छी है अगर एक शाखा का लगभग हमेशा पालन किया जाता है, या यदि एक शाखा या दूसरे का अनुसरण वास्तव में
गुप्त है

@Yakk dयात्रा आंतरिक लूप है, इसलिए d == 0मामलों को समान रूप से वितरित किया जाता है। और 50% मामलों को d == 0यथार्थवादी बना रहा है?

2
0.002%मामलों के d==0यथार्थवादी बना रही है ? वे भर में वितरित कर रहे हैं, हर 65000 पुनरावृत्तियों आप अपने d==0मामले मारा । जबकि 50%हो सकता है अक्सर ऐसा नहीं, 10%या 1%आसानी से हो सकता है, या यहाँ तक कि 90%या 99%। परीक्षण केवल वास्तव में परीक्षण के रूप में प्रदर्शित किया जाता है "यदि आप मूल रूप से कभी नहीं, कभी भी एक शाखा के नीचे जाते हैं, तो क्या शाखा की भविष्यवाणी शाखा को निरर्थक बना देती है?", जिसका उत्तर "हां, लेकिन यह दिलचस्प नहीं है"।
यक्क - एडम नेवरुमोंट

1
नहीं, क्योंकि शोर के कारण अंतर प्रभावी रूप से अदृश्य हो जाएगा।
जो

3
शून्य का वितरण प्रश्न पूछने वाले की स्थिति में पाए जाने वाले वितरण से संबंधित नहीं है। 0 अल्फा और अन्य के मिश्रण वाली छवियों में छेद या अनियमित आकार होता है, लेकिन (आमतौर पर) यह शोर नहीं होता है। यह मानने के लिए कि आप डेटा के बारे में कुछ नहीं जानते (और इसे शोर मानते हैं) एक गलती है। यह वास्तविक चित्रों के साथ एक वास्तविक विश्व अनुप्रयोग है जिसमें 0 अल्फ़ा हो सकता है। और चूंकि पिक्सल की एक पंक्ति में सभी = 0 या सभी ए 0 होने की संभावना है, इसलिए शाखा पूर्वानुमान का लाभ उठाना सबसे अच्छी तरह से सबसे तेज़ हो सकता है, खासकर जब = 0 बहुत अधिक होता है और (धीमा) विभाजन (15+ चक्र) !) से बचा जाता है।
DDS

13

प्लेटफ़ॉर्म को जानने के बिना सटीक सबसे कुशल विधि जानने का कोई तरीका नहीं है, हालांकि, एक सामान्य प्रणाली पर यह इष्टतम के करीब हो सकता है (इंटेल कोडांतरक सिंटैक्स का उपयोग करके):

(मान लें कि विभाजक में है ecxऔर लाभांश में है eax)

mov ebx, ecx
neg ebx
sbb ebx, ebx
add ecx, ebx
div eax, ecx

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


विभाजन कहाँ है
यक्क - एडम नेवरुमोंट

1
यह विभाजन नहीं करता है यह सिर्फ भाजक को प्रदूषित करता है ताकि शून्य से विभाजन असंभव हो
टायलर डर्डन

@ जेन्स टिमरमैन सॉरी, मैंने लिखा है कि इससे पहले कि मैं div स्टेटमेंट जोड़ूं। मैंने टेक्स्ट अपडेट कर दिया है।
टायलर डर्डन

1

इस लिंक के अनुसार , आप केवल SIGFPE सिग्नल को ब्लॉक कर सकते हैं sigaction()(मैंने खुद इसे आज़माया नहीं है, लेकिन मेरा मानना ​​है कि इसे काम करना चाहिए)।

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

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

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