การทดสอบการหารได้เร็วขึ้นกว่าตัวดำเนินการ%


23

ฉันสังเกตเห็นสิ่งที่อยากรู้ในคอมพิวเตอร์ของฉัน *การทดสอบการหารด้วยลายมือเขียนเร็วกว่า%ผู้ปฏิบัติงานอย่างมาก ลองพิจารณาตัวอย่างน้อยที่สุด:

* AMD Ryzen Threadripper 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 เท่า

คำถาม: คุณสามารถบอกฉันว่ารหัสทำงานบนเครื่องของคุณได้อย่างไร พลาดโอกาสในการเพิ่มประสิทธิภาพใน GCC หรือไม่ คุณทำแบบทดสอบนี้เร็วขึ้นได้ไหม


UPDATE: ตามที่ร้องขอนี่เป็นตัวอย่างที่ทำซ้ำได้น้อยที่สุด:

#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 -DNDEBUGบน AMD 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;
}

ความคิดเห็นไม่ได้มีไว้สำหรับการอภิปรายเพิ่มเติม การสนทนานี้ได้รับการย้ายไปแชท
Samuel Liew

ฉันต้องการที่จะเห็นการทดสอบที่คุณยืนยันว่าทั้งสองrที่คุณคำนวณนั้นมีค่าเท่ากัน
Mike Nakis

@ MikeNakis ฉันเพิ่งเพิ่มไป
DaBler

2
การใช้งานในชีวิตจริงส่วนใหญ่a % bมีbขนาดเล็กกว่าaมาก ผ่านการทำซ้ำส่วนใหญ่ในกรณีทดสอบของคุณมีขนาดใกล้เคียงกันหรือbใหญ่กว่าและเวอร์ชันของคุณจะเร็วขึ้นสำหรับซีพียูจำนวนมากในสถานการณ์เหล่านั้น
Matt Timmermans

คำตอบ:


11

สิ่งที่คุณทำเรียกว่าการลดความแข็งแรง: แทนที่การดำเนินการที่มีราคาแพงด้วยชุดของสิ่งที่ถูก

คำสั่ง mod ในซีพียูหลายตัวนั้นช้าเพราะมันไม่ได้ทำการทดสอบในเกณฑ์มาตรฐานทั่วไปและผู้ออกแบบจึงปรับคำแนะนำอื่น ๆ ให้เหมาะสม อัลกอริทึมนี้จะทำงานได้แย่ลงหากต้องทำซ้ำหลาย ๆ ครั้งและ%จะทำงานได้ดีขึ้นใน CPU ที่ต้องการเพียงสองรอบนาฬิกา

สุดท้ายให้ระวังว่ามีทางลัดมากมายในการแบ่งส่วนที่เหลือโดยค่าคงที่ที่เฉพาะเจาะจง (ถึงแม้ว่าคอมไพเลอร์จะดูแลเรื่องนี้ให้คุณ)


ประวัติศาสตร์ไม่ได้ทดสอบในเกณฑ์มาตรฐานทั่วไปหลายประการ - เนื่องจากการแบ่งซ้ำแล้วซ้ำซ้อนและยากที่จะทำให้รวดเร็ว! อย่างน้อยก็เหลือ x86 เป็นส่วนหนึ่งของdiv/ idivซึ่งได้รับความรักใน Intel Penryn, Broadwell และ IceLake (วงเวียนฮาร์ดแวร์ radix ที่สูงกว่า)
Peter Cordes

1
ความเข้าใจของฉันเกี่ยวกับ "การลดความแข็งแรง" คือคุณแทนที่การทำงานหนักในลูปด้วยการทำงานที่เบาเพียงครั้งเดียวเช่นแทนการx = i * constวนซ้ำทุกครั้งที่คุณทำx += constซ้ำทุกครั้ง ฉันไม่คิดว่าการแทนที่การทวีคูณเดี่ยวด้วยการเปลี่ยน / เพิ่มลูปจะเรียกว่าการลดความแข็งแรง en.wikipedia.org/wiki/…กล่าวว่าอาจใช้คำนี้ด้วยวิธีนี้ แต่มีข้อความว่า "เนื้อหานี้มีข้อโต้แย้งมันอธิบายได้ดีกว่าว่าเป็นการเพิ่มประสิทธิภาพช่องมองและการมอบหมายคำสั่ง"
Peter Cordes

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 |
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.