เหตุใด C คอมไพเลอร์จึงปรับสวิตช์ให้เหมาะสมและหากต่างกัน


9

ฉันกำลังทำงานในโครงการส่วนบุคคลเมื่อเร็ว ๆ นี้เมื่อฉันเจอปัญหาแปลก ๆ

ในวงที่แน่นมากฉันมีจำนวนเต็มที่มีค่าระหว่าง 0 ถึง 15 ฉันต้องได้รับ -1 สำหรับค่า 0, 1, 8, และ 9 และ 1 สำหรับค่า 4, 5, 12, และ 13

ฉันหันไปใช้ godbolt เพื่อตรวจสอบตัวเลือกสองสามตัวและรู้สึกประหลาดใจที่ดูเหมือนว่าคอมไพเลอร์ไม่สามารถปรับคำสั่งสวิตช์ให้เหมาะสมเช่นเดียวกับโซ่ถ้า

ลิงก์อยู่ที่นี่: https://godbolt.org/z/WYVBFl

รหัสคือ:

const int lookup[16] = {-1, -1, 0, 0, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 0, 0};

int a(int num) {
    return lookup[num & 0xF];
}

int b(int num) {
    num &= 0xF;

    if (num == 0 || num == 1 || num == 8 || num == 9) 
        return -1;

    if (num == 4 || num == 5 || num == 12 || num == 13)
        return 1;

    return 0;
}

int c(int num) {
    num &= 0xF;
    switch (num) {
        case 0: case 1: case 8: case 9: 
            return -1;
        case 4: case 5: case 12: case 13:
            return 1;
        default:
            return 0;
    }
}

ฉันคิดว่า b และ c จะให้ผลลัพธ์ที่เหมือนกันและฉันหวังว่าฉันจะอ่านบิตแฮ็คเพื่อให้เกิดการใช้งานที่มีประสิทธิภาพด้วยตัวเองตั้งแต่โซลูชัน (คำสั่งสวิตช์ - ในรูปแบบอื่น) ค่อนข้างช้า

ผิดปกติbเรียบเรียงเป็นบิต - แฮ็กในขณะที่cทั้งสวยมากไม่ได้รับการเพิ่มประสิทธิภาพหรือลดลงเป็นกรณีที่แตกต่างกันaขึ้นอยู่กับฮาร์ดแวร์เป้าหมาย

ใครช่วยอธิบายได้ว่าทำไมถึงมีความคลาดเคลื่อนนี้? วิธี 'ถูกต้อง' ในการเพิ่มประสิทธิภาพข้อความค้นหานี้คืออะไร

แก้ไข:

การอธิบาย

ฉันต้องการโซลูชันสวิตช์ให้เร็วที่สุดหรือโซลูชัน "สะอาด" ในทำนองเดียวกัน อย่างไรก็ตามเมื่อรวบรวมกับการเพิ่มประสิทธิภาพในเครื่องของฉันถ้าวิธีการแก้ปัญหาได้เร็วขึ้นอย่างมีนัยสำคัญ

ฉันเขียนโปรแกรมด่วนเพื่อสาธิตและ TIO มีผลลัพธ์เช่นเดียวกับที่พบในท้องถิ่น: ลองออนไลน์!

ด้วยตารางstatic inlineการค้นหาความเร็วเพิ่มขึ้นเล็กน้อย: ลองออนไลน์!


4
ฉันสงสัยว่าคำตอบคือ "คอมไพเลอร์ไม่ได้เลือกอย่างมีเหตุผล" ฉันเพียงแค่รวบรวมรหัสของคุณไปยังวัตถุที่มี GCC 8.3.0 ที่มี-O3และจะรวบรวมcเพื่อสิ่งที่น่าจะเลวร้ายยิ่งกว่าaหรือb( cมีสองเงื่อนไขกระโดดบวกกิจวัตรบิตไม่กี่เทียบกับเพียงหนึ่งกระโดดเงื่อนไขและ Manip บิตง่ายสำหรับb) แต่ยังคง ดีกว่ารายการที่ไร้เดียงสาโดยการทดสอบรายการ ฉันไม่แน่ใจว่าคุณขออะไรที่นี่จริง ๆ ; ความจริงง่ายๆก็คือการเพิ่มประสิทธิภาพคอมไพเลอร์สามารถเปิดใด ๆของเหล่านี้เข้าไปในใด ๆของคนอื่น ๆ ถ้าต้องการและไม่มีกฎอย่างหนักและรวดเร็วสำหรับสิ่งที่มันจะหรือจะไม่ทำ
ShadowRanger

ปัญหาของฉันคือฉันต้องการให้มันเร็ว แต่วิธีแก้ปัญหาถ้าไม่สามารถบำรุงรักษาได้มากเกินไป มีวิธีใดที่จะทำให้คอมไพเลอร์เพิ่มประสิทธิภาพโซลูชันที่สะอาดเพียงพอหรือไม่? ใครช่วยอธิบายได้ว่าทำไมในกรณีนี้ไม่สามารถทำได้
LambdaBeta

ฉันจะเริ่มต้นด้วยการกำหนดอย่างน้อยฟังก์ชั่นเป็นแบบคงที่หรือสิบเอ็ดดีกว่าพวกเขาอินไลน์
wildplasser

@wildplasser เร่งความเร็วขึ้น แต่ifยังคงเต้นswitch(การค้นหาที่แปลกประหลาดยิ่งเร็วขึ้น) [TIO ที่จะติดตาม]
LambdaBeta

@LambdaBeta ไม่มีทางที่จะบอกคอมไพเลอร์เพื่อปรับให้เหมาะสมในวิธีที่เฉพาะเจาะจง คุณจะทราบว่าเสียงดังกราวและ msvc สร้างรหัสที่แตกต่างอย่างสิ้นเชิงสำหรับสิ่งเหล่านี้ หากคุณไม่สนใจและต้องการสิ่งที่ดีที่สุดใน gcc ให้เลือกสิ่งนั้น การปรับให้เหมาะสมของคอมไพเลอร์ขึ้นอยู่กับการวิเคราะห์พฤติกรรมและสิ่งเหล่านั้นไม่ได้ผลลัพธ์ที่ดีที่สุดในทุกกรณี พวกเขากำลังพยายามที่จะดีในกรณีทั่วไปไม่ดีที่สุดในทุกกรณี
ลูกบาศก์

คำตอบ:


6

หากคุณระบุทุกกรณีอย่างชัดเจน gcc มีประสิทธิภาพมาก:

int c(int num) {
    num &= 0xF;
    switch (num) {
        case 0: case 1: case 8: case 9: 
            return -1;
        case 4: case 5: case 12: case 13:
            return 1;
            case 2: case 3: case 6: case 7: case 10: case 11: case 14: case 15: 
        //default:
            return 0;
    }
}

รวบรวมเพียงในสาขาที่จัดทำดัชนีอย่างง่าย:

c:
        and     edi, 15
        jmp     [QWORD PTR .L10[0+rdi*8]]
.L10:
        .quad   .L12
        .quad   .L12
        .quad   .L9
        .quad   .L9
        .quad   .L11
        .quad   .L11
        .quad   .L9
        .quad   .L9
        .quad   .L12
etc...

โปรดทราบว่าหากdefault:ไม่ใส่เครื่องหมายแสดงความคิดเห็น gcc จะเปลี่ยนกลับเป็นเวอร์ชันสาขาที่ซ้อนอยู่


1
@ LambdaBeta คุณควรพิจารณาที่จะยอมรับคำตอบของฉันและยอมรับสิ่งนี้เพราะ Intel CPUs ที่ทันสมัยสามารถทำหน่วยความจำแบบขนานที่มีดัชนีสองครั้งอ่าน / รอบในขณะที่ปริมาณของเคล็ดลับของฉันน่าจะเป็น 1 การค้นหา / รอบ ในทางกลับกันแฮ็คของฉันอาจคล้อยตามเวกเตอร์ 4 ทิศทางด้วย SSE2 pslld/ psradหรือเทียบเท่า 8-way AVX2 ของพวกเขา มากขึ้นอยู่กับลักษณะเฉพาะอื่น ๆ ของรหัสของคุณ
Iwillnotexist Idonotexist

4

คอมไพเลอร์ C มีกรณีพิเศษswitchเพราะพวกเขาคาดหวังว่าโปรแกรมเมอร์จะเข้าใจสำนวนswitchและการใช้ประโยชน์จากมัน

รหัสเช่น:

if (num == 0 || num == 1 || num == 8 || num == 9) 
    return -1;

if (num == 4 || num == 5 || num == 12 || num == 13)
    return 1;

จะไม่ผ่านการตรวจสอบโดยตัวแปลงสัญญาณ C ที่มีความสามารถ; ผู้ตรวจสอบสามหรือสี่คนจะร้องอุทานพร้อมกัน "นี่ควรจะเป็นswitch!"

มันไม่คุ้มค่าที่คอมไพเลอร์ C จะวิเคราะห์โครงสร้างของifข้อความสั่งสำหรับการแปลงเป็นตารางกระโดด เงื่อนไขสำหรับการที่จะต้องถูกต้องและปริมาณของการเปลี่ยนแปลงที่เป็นไปได้ในกลุ่มของifงบเป็นดาราศาสตร์ การวิเคราะห์มีทั้งความซับซ้อนและมีแนวโน้มที่จะเกิดผลเสีย (เช่น: "ไม่เราไม่สามารถแปลงifs เหล่านี้เป็นswitch" ได้)


ฉันรู้ว่านั่นคือเหตุผลที่ฉันเริ่มต้นด้วยสวิตช์ อย่างไรก็ตามการแก้ปัญหาถ้าเป็นอย่างรวดเร็วในกรณีของฉัน โดยทั่วไปฉันถามว่ามีวิธีการโน้มน้าวใจคอมไพเลอร์ให้ใช้โซลูชันที่ดีกว่าสำหรับสวิตช์หรือไม่เพราะสามารถหารูปแบบใน ifs ได้ แต่ไม่ใช่สวิตช์ (ฉันไม่ชอบไอเอฟเอเฉพาะเพราะพวกเขาไม่ชัดเจนหรือบำรุงรักษา)
LambdaBeta

เพิ่มขึ้น แต่ไม่ได้รับการยอมรับเนื่องจากความเชื่อมั่นเป็นเหตุผลว่าทำไมฉันทำคำถามนี้ ฉันต้องการใช้สวิตช์ แต่ในกรณีของฉันช้าเกินไปฉันต้องการหลีกเลี่ยงifถ้าเป็นไปได้ทั้งหมด
LambdaBeta

@LambdaBeta: มีเหตุผลบางอย่างที่จะหลีกเลี่ยงตารางการค้นหาหรือไม่? สร้างstaticและใช้ตัวกำหนดค่าเริ่มต้น C99หากคุณต้องการทำให้ชัดเจนยิ่งขึ้นในสิ่งที่คุณมอบหมาย
ShadowRanger

1
ฉันจะเริ่มต้นอย่างน้อยทิ้งบิตต่ำเพื่อให้มีประสิทธิภาพน้อยกว่าสำหรับเครื่องมือเพิ่มประสิทธิภาพที่ต้องทำ
. GitHub หยุดช่วยน้ำแข็ง

@ShadowRanger น่าเสียดายที่ยังช้ากว่าif(ดูการแก้ไข) @R .. ฉันได้หาวิธีแก้ปัญหาบิตคอมพิวติ้งแบบเต็มสำหรับคอมไพเลอร์ซึ่งเป็นสิ่งที่ฉันใช้ตอนนี้ น่าเสียดายที่ในกรณีของฉันเหล่านี้เป็นenumค่าไม่ใช่จำนวนเต็มเปล่าดังนั้นแฮ็กบิตในระบบจึงไม่สามารถบำรุงรักษาได้มาก
LambdaBeta

4

รหัสต่อไปนี้จะคำนวณการค้นหาของคุณ branchfree, LUT-free, ใน ~ 3 รอบนาฬิกา, คำแนะนำที่มีประโยชน์ ~ 4 และ ~ 13 ไบต์ของinlineรหัสเครื่อง x86 ที่มีความน่าเชื่อถือสูง

มันขึ้นอยู่กับการเป็นตัวแทนจำนวนเต็ม 2 ของ

อย่างไรก็ตามคุณต้องตรวจสอบให้แน่ใจว่าu32และs32typedefs ชี้ไปที่ประเภทเลขจำนวนเต็ม 32 บิตที่ไม่ได้ลงชื่อและลงชื่อ stdint.hประเภทuint32_tและint32_tจะเหมาะสม แต่ฉันไม่รู้ว่าส่วนหัวพร้อมให้คุณใช้งานหรือไม่

const int lookup[16] = {-1, -1, 0, 0, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 0, 0};

int a(int num) {
    return lookup[num & 0xF];
}


int d(int num){
    typedef unsigned int u32;
    typedef signed   int s32;

    // const int lookup[16]     = {-1, -1, 0, 0, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 0, 0};
    // 2-bit signed 2's complement: 11 11 00 00 01 01 00 00 11 11 00 00 01 01 00 00
    // Hexadecimal:                   F     0     5     0     F     0     5     0
    const u32 K = 0xF050F050U;

    return (s32)(K<<(num+num)) >> 30;
}

int main(void){
    for(int i=0;i<16;i++){
        if(a(i) != d(i)){
            return !0;
        }
    }
    return 0;
}

ดูตัวคุณเองที่นี่: https://godbolt.org/z/AcJWWf


ในการเลือกค่าคงที่

การค้นหาของคุณเป็นค่าคงที่ขนาดเล็กมากที่มีค่าตั้งแต่ 16 ถึง 1 ถึง +1 แต่ละตัวมีขนาดไม่เกิน 2 บิตและมี 16 ชิ้นซึ่งเราสามารถจัดโครงสร้างได้ดังต่อไปนี้:

// const int lookup[16]     = {-1, -1, 0, 0, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 0, 0};
// 2-bit signed 2's complement: 11 11 00 00 01 01 00 00 11 11 00 00 01 01 00 00
// Hexadecimal:                   F     0     5     0     F     0     5     0
u32 K = 0xF050F050U;

ด้วยการวางดัชนีที่ใกล้เคียงกับบิตที่มีความสำคัญมากที่สุด 0 การเปลี่ยนแปลงเพียงครั้งเดียว2*numจะวางบิตของหมายเลข 2 บิตของคุณลงในบิตของการลงทะเบียน เลื่อนไปทางขวาหมายเลข 2 บิตโดย 32-2 = 30 บิตเครื่องหมายขยายไปถึงเต็มintทำให้เคล็ดลับ


นี่อาจเป็นวิธีที่สะอาดที่สุดในการทำพร้อมกับmagicความคิดเห็นที่อธิบายถึงวิธีการสร้างใหม่ คุณช่วยอธิบายได้ว่าคุณคิดยังไงกับมัน?
LambdaBeta

ได้รับการยอมรับตั้งแต่สิ่งนี้สามารถทำให้ 'สะอาด' ในขณะที่ยังรวดเร็ว (ผ่านเวทย์มนตร์ preprocessor :) < xkcd.com/541 >)
LambdaBeta

1
เอาชนะความพยายามไร้สาขาของฉัน:!!(12336 & (1<<x))-!!(771 & (1<<x));
technosaurus

0

คุณสามารถสร้างเอฟเฟกต์เดียวกันได้โดยใช้คณิตศาสตร์เท่านั้น:

// produces : -1 -1 0 0 1 1 0 0 -1 -1 0 0 1 1 0 0 ...
int foo ( int x )
{
    return 1 - ( 3 & ( 0x46 >> ( x & 6 ) ) );
}

แม้ว่าในทางเทคนิคแล้วนี่ยังเป็นการค้นหา (บิต)

หากสิ่งที่กล่าวมาข้างต้นดูลึกลับเกินไปคุณสามารถทำสิ่งต่อไปนี้

int foo ( int x )
{
    int const y = x & 6;
    return (y == 4) - !y;
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.