ข้อมูลสรุป : ต่ำกว่า 240, LLVM คลายการวนรอบด้านในอย่างเต็มที่และช่วยให้สังเกตได้ว่าสามารถเพิ่มประสิทธิภาพวนซ้ำซ้ำเพื่อทำลายมาตรฐานของคุณ
คุณพบเกณฑ์มายากลข้างต้นซึ่ง LLVM หยุดการดำเนินการเพิ่มประสิทธิภาพบางอย่าง ขีด จำกัด คือ 8 ไบต์ * 240 = 1920 ไบต์ (อาเรย์ของคุณเป็นอาร์เรย์ของusizes ดังนั้นความยาวจะถูกคูณด้วย 8 ไบต์โดยถือว่า x86-64 CPU) ในการวัดประสิทธิภาพนี้การเพิ่มประสิทธิภาพเฉพาะอย่างเดียวซึ่งดำเนินการเฉพาะสำหรับความยาว 239 เท่านั้นจะเป็นตัวกำหนดความแตกต่างของความเร็วอย่างมาก แต่เริ่มช้า:
(รหัสทั้งหมดในคำตอบนี้ถูกรวบรวมด้วย-C opt-level=3)
pub fn foo() -> usize {
    let arr = [0; 240];
    let mut s = 0;
    for i in 0..arr.len() {
        s += arr[i];
    }
    s
}
รหัสง่าย ๆ นี้จะสร้างคร่าวๆที่แอสเซมบลีที่คาดไว้: ห่วงเพิ่มองค์ประกอบ แต่ถ้าคุณเปลี่ยน240ไป239, ที่ปล่อยออกมาชุมนุมแตกต่างกันค่อนข้างมาก เห็นมันบน Godbolt คอมไพเลอร์ Explorer ที่ นี่เป็นส่วนเล็ก ๆ ของการประชุม:
movdqa  xmm1, xmmword ptr [rsp + 32]
movdqa  xmm0, xmmword ptr [rsp + 48]
paddq   xmm1, xmmword ptr [rsp]
paddq   xmm0, xmmword ptr [rsp + 16]
paddq   xmm1, xmmword ptr [rsp + 64]
; more stuff omitted here ...
paddq   xmm0, xmmword ptr [rsp + 1840]
paddq   xmm1, xmmword ptr [rsp + 1856]
paddq   xmm0, xmmword ptr [rsp + 1872]
paddq   xmm0, xmm1
pshufd  xmm1, xmm0, 78
paddq   xmm1, xmm0
นี่คือสิ่งที่เรียกว่าloop unrolling : LLVM วางเนื้อความไว้หลาย ๆ ครั้งเพื่อหลีกเลี่ยงการเรียกใช้คำสั่ง "loop management" ทั้งหมดเช่นการเพิ่มตัวแปร loop ตรวจสอบว่าวนรอบได้สิ้นสุดลงแล้วหรือข้ามไปยังจุดเริ่มต้นของวนรอบ .
ในกรณีที่คุณสงสัย: paddqคำแนะนำและคล้ายกันคือคำแนะนำ SIMD ซึ่งช่วยให้รวมค่าหลายค่าในแบบคู่ขนาน นอกจากนี้ยังมีการใช้ 16- ไบต์ SIMD 16 รีจิสเตอร์ ( xmm0และxmm1) ในแบบคู่ขนานเพื่อให้ระดับการเรียนการสอนที่ขนานกันของซีพียูสามารถดำเนินการตามคำแนะนำทั้งสองนี้ได้ในเวลาเดียวกัน ท้ายที่สุดพวกเขาเป็นอิสระจากกัน ในท้ายที่สุดการลงทะเบียนทั้งสองจะถูกรวมเข้าด้วยกันแล้วรวมเข้ากับผลลัพธ์สเกลาร์ในแนวนอน
กระแสหลัก x86 ซีพียู (ไม่ใช่ Atom ที่ใช้พลังงานต่ำ) สามารถโหลด 2 เวกเตอร์ต่อนาฬิกาได้จริง ๆ เมื่อพวกเขาเข้าสู่แคช L1d และpaddqปริมาณงานอย่างน้อย 2 ต่อนาฬิกาโดยมีความหน่วงรอบ 1 รอบสำหรับซีพียูส่วนใหญ่ ดูhttps://agner.org/optimize/และคำถามและคำตอบเกี่ยวกับตัวสะสมหลายตัวเพื่อซ่อนเวลาแฝง (ของ FP FMA สำหรับผลิตภัณฑ์ดอท) และปัญหาคอขวดในปริมาณงานแทน
LLVM ไม่เล็กเหยียด loops บางเมื่อมันไม่ได้อย่างเต็มที่ unrolling และยังคงใช้สะสมหลาย ดังนั้นโดยทั่วไปแบนด์วิดท์หน้าและปัญหาคอขวดเวลาแฝงท้ายไม่ใช่ปัญหาใหญ่สำหรับการวนซ้ำที่สร้างโดย LLVM แม้ว่าจะไม่มีการคลี่เต็ม
แต่การคลายลูปจะไม่รับผิดชอบต่อความแตกต่างของปัจจัย 80! อย่างน้อยก็ไม่วนแยกออกจากกันโดยลำพัง ลองดูที่โค้ดการเปรียบเทียบจริงซึ่งใส่ลูปหนึ่งเข้าไปในอีกอันหนึ่ง:
const CAPACITY: usize = 239;
const IN_LOOPS: usize = 500000;
pub fn foo() -> usize {
    let mut arr = [0; CAPACITY];
    for i in 0..CAPACITY {
        arr[i] = i;
    }
    let mut sum = 0;
    for _ in 0..IN_LOOPS {
        let mut s = 0;
        for i in 0..arr.len() {
            s += arr[i];
        }
        sum += s;
    }
    sum
}
( บน Godbolt Compiler Explorer )
ชุดประกอบCAPACITY = 240ดูปกติ: ลูปซ้อนกันสองลูป (ในตอนเริ่มต้นของฟังก์ชั่นนั้นมีโค้ดบางส่วนสำหรับการเริ่มต้นซึ่งเราจะไม่สนใจ) สำหรับ 239 อย่างไรก็ตามมันดูแตกต่างกันมาก! เราเห็นว่าการเตรียมใช้งานลูปเริ่มต้นและลูปด้านในไม่ได้ถูกม้วน: จนถึงตอนนี้
ข้อแตกต่างที่สำคัญคือในปี 239 LLVM สามารถคิดได้ว่าผลลัพธ์ของลูปภายในไม่ได้ขึ้นอยู่กับลูปภายนอก! ด้วยเหตุนี้ LLVM จึงปล่อยโค้ดที่โดยพื้นฐานแล้วเรียกใช้งานวงภายในเท่านั้น (คำนวณผลรวม) จากนั้นจำลองวงรอบนอกโดยเพิ่มsumจำนวนครั้ง!
ครั้งแรกที่เราเห็นการชุมนุมเกือบเหมือนเดิมข้างต้น (การชุมนุมที่เป็นตัวแทนของวงใน) หลังจากนั้นเราจะเห็นสิ่งนี้ (ฉันแสดงความคิดเห็นเพื่ออธิบายการชุมนุมความเห็นที่*มีความสำคัญอย่างยิ่ง):
        ; at the start of the function, `rbx` was set to 0
        movq    rax, xmm1     ; result of SIMD summing up stored in `rax`
        add     rax, 711      ; add up missing terms from loop unrolling
        mov     ecx, 500000   ; * init loop variable outer loop
.LBB0_1:
        add     rbx, rax      ; * rbx += rax
        add     rcx, -1       ; * decrement loop variable
        jne     .LBB0_1       ; * if loop variable != 0 jump to LBB0_1
        mov     rax, rbx      ; move rbx (the sum) back to rax
        ; two unimportant instructions omitted
        ret                   ; the return value is stored in `rax`
อย่างที่คุณเห็นที่นี่ผลลัพธ์ของลูปภายในถูกนำมารวมกันบ่อยครั้งเท่าที่ลูปด้านนอกจะวิ่งแล้วกลับมา LLVM สามารถทำการปรับให้เหมาะสมนี้ได้เท่านั้นเพราะมันเข้าใจว่าลูปด้านในเป็นอิสระจากตัวนอก
วิธีการนี้จะเปลี่ยนรันไทม์จากCAPACITY * IN_LOOPSCAPACITY + IN_LOOPSไป และนี่เป็นสาเหตุที่ทำให้เกิดความแตกต่างอย่างมาก
หมายเหตุเพิ่มเติม: คุณสามารถทำอะไรกับสิ่งนี้ได้ไหม ไม่ได้จริงๆ LLVM ต้องมีขีด จำกัด ของเวทมนตร์เช่นนี้หากไม่มีการปรับให้เหมาะสม LLVM อาจใช้เวลาตลอดไปในการสร้างรหัสให้สมบูรณ์ แต่เราสามารถตกลงกันได้ว่ารหัสนี้เป็นของปลอมอย่างมาก ในทางปฏิบัติฉันสงสัยว่าจะเกิดความแตกต่างอย่างมาก ความแตกต่างเนื่องจากการวนรอบเต็มการคลี่คลายมักไม่ใช่ปัจจัยที่ 2 ในกรณีเหล่านี้ ดังนั้นไม่จำเป็นต้องกังวลเกี่ยวกับกรณีการใช้งานจริง
ในฐานะที่เป็นโน้ตสุดท้ายเกี่ยวกับรหัส Rust สำนวน: arr.iter().sum()เป็นวิธีที่ดีกว่าในการสรุปองค์ประกอบทั้งหมดของอาร์เรย์ และการเปลี่ยนแปลงนี้ในตัวอย่างที่สองไม่ได้นำไปสู่ความแตกต่างที่โดดเด่นใด ๆ ในชุดประกอบที่ปล่อยออกมา คุณควรใช้เวอร์ชันสั้นและเป็นสำนวนเว้นแต่ว่าคุณวัดว่ามันเจ็บประสิทธิภาพ