เหตุใดสวิตช์จึงไม่ปรับให้เหมาะสมในลักษณะเดียวกับการผูกมัดในกรณีอื่นใน c / c ++


39

การดำเนินการตามตารางต่อไปนี้จะสร้างชุดของคำสั่ง cmp / je อย่างที่ฉันคาดหวังว่าจะถูกล่ามโซ่หากข้อความ:

int square(int num) {
    if (num == 0){
        return 0;
    } else if (num == 1){
        return 1;
    } else if (num == 2){
        return 4;
    } else if (num == 3){
        return 9;
    } else if (num == 4){
        return 16;
    } else if (num == 5){
        return 25;
    } else if (num == 6){
        return 36;
    } else if (num == 7){
        return 49;
    } else {
        return num * num;
    }
}

และต่อไปนี้สร้างตารางข้อมูลสำหรับการส่งคืน:

int square_2(int num) {
    switch (num){
        case 0: return 0;
        case 1: return 1;
        case 2: return 4;
        case 3: return 9;
        case 4: return 16;
        case 5: return 25;
        case 6: return 36;
        case 7: return 49;
        default: return num * num;
    }
}

เหตุใด gcc จึงไม่สามารถปรับค่าตัวแรกให้อยู่ในอันดับต้น ๆ

ถอดแยกชิ้นส่วนเพื่อการอ้างอิง: https://godbolt.org/z/UP_igi

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


3
คุณหมายถึง "พฤติกรรมที่ไม่ได้กำหนด" หมายถึงอะไร ตราบใดที่พฤติกรรมที่สังเกตได้เหมือนกันคอมไพเลอร์สามารถสร้างแอสเซมบลี / รหัสเครื่องใด ๆ ที่ต้องการ
bolov

2
@ user207421 ไม่สนใจreturns; กรณีที่ไม่มีbreaksดังนั้นสวิตช์ยังมีคำสั่งเฉพาะของการดำเนินการ ห่วงโซ่ if / else มีผลตอบแทนในทุกสาขาความหมายในกรณีนี้เทียบเท่า การเพิ่มประสิทธิภาพไม่ได้เป็นไปไม่ได้ ในฐานะที่เป็นตัวอย่างICCไม่ได้เพิ่มประสิทธิภาพฟังก์ชั่นใด ๆ
user1810087

9
บางทีคำตอบที่ง่ายที่สุด ... gcc อาจไม่เห็นโครงสร้างนี้และปรับให้เหมาะสม (ยัง)
user1810087

3
ฉันเห็นด้วยกับ @ user1810087 คุณเพิ่งค้นพบขอบเขตปัจจุบันของกระบวนการปรับแต่งคอมไพเลอร์ sub-sub-case ที่ปัจจุบันไม่ได้รับการยอมรับว่าปรับให้เหมาะสม (โดยคอมไพเลอร์บางตัว) ในความเป็นจริงไม่ใช่โซ่ทุกอย่างถ้าสามารถปรับให้เหมาะสมด้วยวิธีนั้น แต่จะมีการทดสอบชุดย่อยที่ตัวแปร SAME เท่านั้นที่จะทดสอบกับค่าคงที่
Roberto Caboni

1
if-else มีลำดับการดำเนินการที่แตกต่างจากบนลงล่าง แต่ถึงกระนั้นการแทนที่รหัสด้วยหากข้อความไม่ได้ปรับปรุงรหัสเครื่อง ในทางกลับกันสวิตช์ไม่มีลำดับการดำเนินการที่กำหนดไว้ล่วงหน้าและเป็นเพียงแค่ตารางข้ามไปยังการกระโดดข้ามที่ได้รับเกียรติ ที่ถูกกล่าวว่าคอมไพเลอร์ได้รับอนุญาตให้เหตุผลเกี่ยวกับพฤติกรรมที่สังเกตได้ที่นี่ดังนั้นการเพิ่มประสิทธิภาพที่น่าสงสารของรุ่น if-else ค่อนข้างน่าผิดหวัง
Lundin

คำตอบ:


29

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

ตอนนี้มาถึงif-elseมันเป็นสิ่งที่ตรงกันข้าม ในขณะที่switch-caseดำเนินการในเวลาคงที่โดยไม่คำนึงถึงจำนวนสาขาif-elseจะได้รับการปรับให้เหมาะสมกับสาขาจำนวนน้อย ที่นี่คุณคาดหวังว่าคอมไพเลอร์จะสร้างชุดการเปรียบเทียบตามลำดับที่คุณเขียนไว้

ดังนั้นถ้าฉันได้ใช้if-elseเพราะผมคาดหวังว่าสายมากที่สุดเพื่อsquare()ที่จะให้0หรือ1และไม่ค่อยมีค่าอื่น ๆ แล้ว 'เพิ่มประสิทธิภาพ' นี้ให้ตารางการค้นหาได้จริงสาเหตุรหัสของฉันทำงานช้าลงกว่าที่ผมคาดหวังเอาชนะวัตถุประสงค์ของฉันสำหรับใช้ifแทน switchของ ดังนั้นแม้ว่าจะเป็นที่ถกเถียงกันอยู่ แต่ฉันรู้สึกว่า GCC กำลังทำสิ่งที่ถูกต้อง

ในความคิดเห็นมีคนแบ่งปันลิงค์ที่เสียงดังกราวทำการเพิ่มประสิทธิภาพนี้และสร้างรหัสตามตารางการค้นหาif-elseเช่นกัน สิ่งที่น่าสังเกตเกิดขึ้นเมื่อเราลดจำนวนผู้ป่วยลงเหลือเพียงสองคน (และค่าเริ่มต้น) ด้วยเสียงดังกราว มันสร้างรหัสที่เหมือนกันอีกครั้งสำหรับทั้ง if และ switch แต่คราวนี้ สลับไปที่การเปรียบเทียบและย้ายแทนการค้นหาในตารางทั้งสอง ซึ่งหมายความว่าแม้แต่เสียงดังกังวานที่นิยมใช้สวิตช์ก็รู้ว่ารูปแบบ 'if' นั้นเหมาะสมที่สุดเมื่อจำนวนเคสมีน้อย!

โดยสรุปลำดับของการเปรียบเทียบif-elseและตารางกระโดดสำหรับswitch-caseเป็นรูปแบบมาตรฐานที่คอมไพเลอร์มีแนวโน้มที่จะติดตามและนักพัฒนามักจะคาดหวังเมื่อพวกเขาเขียนรหัส อย่างไรก็ตามสำหรับบางกรณีพิเศษคอมไพเลอร์บางคนอาจเลือกที่จะทำลายรูปแบบนี้ซึ่งพวกเขารู้สึกว่ามันมีการเพิ่มประสิทธิภาพที่ดีขึ้น คอมไพเลอร์อื่น ๆ อาจเลือกที่จะยึดตามแพทเทิร์นต่อไปแม้ว่าจะเห็นได้ชัดว่าดีที่สุดย่อยเชื่อใจนักพัฒนาที่จะรู้ว่าเขาต้องการอะไร ทั้งคู่เป็นแนวทางที่ถูกต้องโดยมีข้อดีและข้อเสีย


2
ใช่การเพิ่มประสิทธิภาพเป็นดาบที่มีหลายขอบ: สิ่งที่พวกเขาเขียนสิ่งที่พวกเขาต้องการสิ่งที่พวกเขาได้รับและสิ่งที่เราสาปแช่ง
Deduplicator

1
"... จากนั้น 'การเพิ่มประสิทธิภาพ' สิ่งนี้กับการค้นหาแบบตารางจะทำให้โค้ดของฉันทำงานช้ากว่าที่ฉันคาดไว้จริง ๆ ... "คุณช่วยให้เหตุผลนี้ได้ไหม? ทำไมตารางกระโดดจึงช้ากว่ากิ่งที่มีเงื่อนไขสองแบบ (เพื่อตรวจสอบอินพุต0และ1)
Cody Grey

@CodyGray ฉันต้องยอมรับว่าฉันไม่ได้ไปถึงระดับของการนับรอบ - ฉันเพิ่งผ่านไปด้วยความรู้สึกว่าการโหลดจากหน่วยความจำผ่านตัวชี้อาจใช้เวลามากกว่ารอบเปรียบเทียบและกระโดด แต่ฉันผิด อย่างไรก็ตามฉันหวังว่าคุณจะเห็นด้วยกับฉันว่าแม้ในกรณีนี้อย่างน้อยสำหรับ '0' ifจะเห็นได้ชัดว่าเร็วขึ้นหรือไม่ ตอนนี้นี่เป็นตัวอย่างของแพลตฟอร์มที่ทั้ง 0 และ 1 จะเร็วขึ้นเมื่อใช้ifมากกว่าเมื่อใช้สวิตช์: godbolt.org/z/wcJhvS (โปรดทราบว่ามีการเพิ่มประสิทธิภาพอื่น ๆ อีกมากมายในการเล่นที่นี่เช่นกัน)
th33lf

1
การนับรอบไม่ได้ทำงานกับสถาปัตยกรรม OOO ที่ทันสมัยกว่า :-) โหลดจากหน่วยความจำจะไม่ช้ากว่ากิ่งที่มีการคาดการณ์ผิดดังนั้นคำถามก็คือมีแนวโน้มว่าสาขาจะถูกคาดการณ์อย่างไร คำถามนั้นใช้ได้กับสาขาที่มีเงื่อนไขทุกประเภทไม่ว่าจะสร้างโดยifคำสั่งที่ชัดเจนหรือโดยคอมไพเลอร์โดยอัตโนมัติ ฉันไม่ใช่ผู้เชี่ยวชาญเกี่ยวกับ ARM ดังนั้นฉันจึงไม่แน่ใจว่าการอ้างสิทธิ์ของคุณเกี่ยวกับswitchการเป็นเร็วกว่าที่ifเป็นจริงหรือไม่ มันจะขึ้นอยู่กับการลงโทษสำหรับสาขา mispredicted และที่จริงจะขึ้นอยู่กับที่ ARM
Cody Grey

0

เหตุผลหนึ่งที่เป็นไปได้คือหากค่าที่ต่ำของnumมีแนวโน้มมากกว่าเช่นเสมอ 0 รหัสที่สร้างขึ้นสำหรับรหัสแรกอาจเร็วขึ้น รหัสที่สร้างขึ้นสำหรับสวิตช์ใช้เวลาเท่ากันสำหรับค่าทั้งหมด

เปรียบเทียบกรณีที่ดีที่สุดตามตารางนี้ ดูคำตอบนี้สำหรับคำอธิบายของตาราง

ถ้าnum == 0, สำหรับ "if" คุณมี xor, ทดสอบ, je (with jump), ret. ความหน่วง: 1 + 1 + กระโดด อย่างไรก็ตาม xor และการทดสอบมีความเป็นอิสระดังนั้นความเร็วในการเรียกใช้จริงจะเร็วกว่า 1 + 1 รอบ

ถ้าnum < 7สำหรับ "สวิตช์" คุณมี mov, cmp, ja (โดยไม่ต้องกระโดด), mov, ret ความหน่วง: 2 + 1 + ไม่กระโดด + 2

คำสั่งการกระโดดที่ไม่ส่งผลต่อการกระโดดเร็วกว่าคำสั่งการกระโดด อย่างไรก็ตามตารางไม่ได้กำหนดเวลาแฝงสำหรับการกระโดดดังนั้นจึงไม่ชัดเจนสำหรับฉันว่าอันไหนดีกว่า เป็นไปได้ว่าสุดท้ายจะดีกว่าเสมอและ GCC ไม่สามารถปรับให้เหมาะสมได้


1
อืมทฤษฎีที่น่าสนใจ แต่สำหรับ ifs กับสวิตช์คุณมี: xor, test, jmp vs mov, cmp jmp สามคำแนะนำแต่ละคำสั่งด้วยการกระโดดครั้งสุดท้าย ดูเหมือนจะเท่ากันในกรณีที่ดีที่สุดใช่ไหม?
chacham15

3
msgstr "คำสั่งการกระโดดที่ไม่ส่งผลให้กระโดดเร็วกว่าการกระโดด". มันคือการทำนายสาขาที่สำคัญ
geza
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.