% संचालक की तुलना में तेज़ विभाज्यता परीक्षण?


23

मैंने अपने कंप्यूटर पर एक उत्सुक चीज़ देखी। * हस्तलिखित विभाज्यता परीक्षण %ऑपरेटर की तुलना में काफी तेज है । न्यूनतम उदाहरण पर विचार करें:

* एएमडी राईजन थ्रेडिपर 2990WX, GCC 9.2.0

static int divisible_ui_p(unsigned int m, unsigned int a)
{
    if (m <= a) {
        if (m == a) {
            return 1;
        }

        return 0;
    }

    m += a;

    m >>= __builtin_ctz(m);

    return divisible_ui_p(m, a);
}

उदाहरण विषम aऔर द्वारा सीमित है m > 0। हालांकि, यह आसानी से सभी के लिए सामान्यीकृत किया जा सकता है aऔर m। कोड सिर्फ विभाजन को परिवर्धन की श्रृंखला में परिवर्तित करता है।

अब इस बात पर विचार करें कि परीक्षण कार्यक्रम किसके साथ संकलित है -std=c99 -march=native -O3:

    for (unsigned int a = 1; a < 100000; a += 2) {
        for (unsigned int m = 1; m < 100000; m += 1) {
#if 1
            volatile int r = divisible_ui_p(m, a);
#else
            volatile int r = (m % a == 0);
#endif
        }
    }

... और मेरे कंप्यूटर पर परिणाम:

| implementation     | time [secs] |
|--------------------|-------------|
| divisible_ui_p     |    8.52user |
| builtin % operator |   17.61user |

इसलिए 2 गुना से अधिक तेजी से।

प्रश्न: क्या आप मुझे बता सकते हैं कि कोड आपके मशीन पर कैसे व्यवहार करता है? क्या यह जीसीसी में अनुकूलन अवसर चूक गया है? क्या आप यह परीक्षण और भी तेजी से कर सकते हैं?


अद्यतन: अनुरोध के रूप में, यहाँ एक न्यूनतम प्रतिलिपि प्रस्तुत करने योग्य उदाहरण है:

#include <assert.h>

static int divisible_ui_p(unsigned int m, unsigned int a)
{
    if (m <= a) {
        if (m == a) {
            return 1;
        }

        return 0;
    }

    m += a;

    m >>= __builtin_ctz(m);

    return divisible_ui_p(m, a);
}

int main()
{
    for (unsigned int a = 1; a < 100000; a += 2) {
        for (unsigned int m = 1; m < 100000; m += 1) {
            assert(divisible_ui_p(m, a) == (m % a == 0));
#if 1
            volatile int r = divisible_ui_p(m, a);
#else
            volatile int r = (m % a == 0);
#endif
        }
    }

    return 0;
}

साथ gcc -std=c99 -march=native -O3 -DNDEBUGAMD Ryzen Threadripper 2990WX पर संकलित

gcc --version
gcc (Gentoo 9.2.0-r2 p3) 9.2.0

UPDATE2: अनुरोध के अनुसार, वह संस्करण जो किसी को भी संभाल सकता है aऔर m(यदि आप पूर्णांक ओवरफ़्लो से बचना चाहते हैं, तो परीक्षण को पूर्णांक प्रकार के साथ दो बार इनपुट पूर्णांक के साथ लागू किया जाना है):

int divisible_ui_p(unsigned int m, unsigned int a)
{
#if 1
    /* handles even a */
    int alpha = __builtin_ctz(a);

    if (alpha) {
        if (__builtin_ctz(m) < alpha) {
            return 0;
        }

        a >>= alpha;
    }
#endif

    while (m > a) {
        m += a;
        m >>= __builtin_ctz(m);
    }

    if (m == a) {
        return 1;
    }

#if 1
    /* ensures that 0 is divisible by anything */
    if (m == 0) {
        return 1;
    }
#endif

    return 0;
}

टिप्पणियाँ विस्तारित चर्चा के लिए नहीं हैं; इस वार्तालाप को बातचीत में स्थानांतरित कर दिया गया है ।
शमूएल ल्यू

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

@ मायकेनकिस मैंने अभी जोड़ा है।
डब्लर

2
के अधिकांश वास्तविक जीवन का उपयोग करता है a % bहै bतुलना में काफी छोटा a। आपके परीक्षण के मामले में अधिकांश पुनरावृत्तियों के माध्यम से, वे समान आकार के होते हैं, या bबड़े होते हैं, और आपका संस्करण उन स्थितियों में कई सीपीयू पर तेज हो सकता है।
मैट टिम्मरमैन

जवाबों:


11

आप जो कर रहे हैं उसे ताकत में कमी कहा जाता है: सस्ते ऑपरेशन की जगह सस्ते वाले की जगह।

कई सीपीयू पर मॉड निर्देश धीमा है, क्योंकि यह ऐतिहासिक रूप से कई सामान्य बेंचमार्क में परीक्षण नहीं किया गया था और इसलिए डिजाइनरों ने इसके बजाय अन्य निर्देशों को अनुकूलित किया। यदि यह कई पुनरावृत्तियों करना है, और यह एल्गोरिथ्म बदतर प्रदर्शन करेगा% सीपीयू पर बेहतर प्रदर्शन करेगा जहां इसे केवल दो घड़ी चक्रों की आवश्यकता होती है।

अंत में, ध्यान रखें कि विशिष्ट स्थिरांक द्वारा विभाजन के शेष भाग को लेने के लिए कई शॉर्टकट हैं। (हालांकि संकलक आमतौर पर आपके लिए इसका ध्यान रखेंगे।)


ऐतिहासिक रूप से कई सामान्य बेंचमार्क में परीक्षण नहीं किया गया था - इसलिए भी कि विभाजन स्वाभाविक रूप से चलने के लिए कठिन और कठिन है! x86 कम से कम div/ के भाग के रूप में शेष है, idivजिसने इंटेल पेरीएन, ब्रॉडवेल और आइसलेक (उच्च मूलांक वाले हार्डवेयर डिवाइडर) में कुछ प्यार पा लिया है
पीटर कॉर्ड्स

1
"ताकत में कमी" की मेरी समझ यह है कि आप एक लूप में एक भारी ऑपरेशन को एक एकल लाइटर ऑपरेशन से बदल देते हैं, जैसे कि आपके द्वारा किए गए x = i * constप्रत्येक पुनरावृत्ति के बजायx += const हर पुनरावृत्ति करते हैं। मुझे नहीं लगता कि शिफ्ट / ऐड लूप के साथ किसी एक को गुणा करने पर उसे शक्ति-कमी कहा जाएगा। en.wikipedia.org/wiki/… कहते हैं कि इस शब्द का इस्तेमाल शायद इस तरह किया जा सकता है, लेकिन एक नोट के साथ "यह सामग्री विवादित है। इसे बेहतर ढंग से पीपहोल ऑप्टिमाइज़ेशन और इंस्ट्रक्शन असाइनमेंट के रूप में वर्णित किया गया है।"
पीटर कॉर्डेस

9

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

निम्नलिखित कार्यान्वयन पर विचार करें

int divisible_ui_p(unsigned int m, unsigned int a)
{
    while (m > a) {
        m += a;
        m >>= __builtin_ctz(m);
    }

    if (m == a) {
        return 1;
    }

    return 0;
}

और सरणियाँ

unsigned int A[100000/2];
unsigned int M[100000-1];

for (unsigned int a = 1; a < 100000; a += 2) {
    A[a/2] = a;
}
for (unsigned int m = 1; m < 100000; m += 1) {
    M[m-1] = m;
}

जो हैं / फेरबदल फ़ंक्शन का उपयोग करके फेरबदल नहीं किए जाते हैं ।

फेरबदल के बिना, परिणाम अभी भी हैं

| implementation     | time [secs] |
|--------------------|-------------|
| divisible_ui_p     |    8.56user |
| builtin % operator |   17.59user |

हालाँकि, एक बार जब मैं इन सरणियों को बदल देता हूं, तो परिणाम भिन्न होते हैं

| implementation     | time [secs] |
|--------------------|-------------|
| divisible_ui_p     |   31.34user |
| builtin % operator |   17.53user |
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.