簡體   English   中英

為什么在使用240個或更多元素循環數組時會產生很大的性能影響?

[英]Why is there a large performance impact when looping over an array with 240 or more elements?

當在Rust中的數組上運行求和循環時,我注意到當CAPACITY > = 240時性能下降很大CAPACITY = 239大約快80倍。

是否有特殊的編譯優化Rust正在為“短”數組做什么?

rustc -C opt-level=3編譯。

use std::time::Instant;

const CAPACITY: usize = 240;
const IN_LOOPS: usize = 500000;

fn main() {
    let mut arr = [0; CAPACITY];
    for i in 0..CAPACITY {
        arr[i] = i;
    }
    let mut sum = 0;
    let now = Instant::now();
    for _ in 0..IN_LOOPS {
        let mut s = 0;
        for i in 0..arr.len() {
            s += arr[i];
        }
        sum += s;
    }
    println!("sum:{} time:{:?}", sum, now.elapsed());
}

總結 :在240以下,LLVM完全展開內循環,讓它注意到它可以優化重復循環,打破你的基准。



您找到了一個魔術閾值,高於該閾值LLVM停止執行某些優化 閾值是8字節* 240 = 1920字節(您的數組是usize的數組,因此假設x86-64 CPU,長度乘以8字節)。 在這個基准測試中,一個特定的優化 - 僅對長度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 Compiler 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

這就是所謂的循環展開 :LLVM粘貼循環體一堆時間,以避免必須執行所有那些“循環管理指令”,即遞增循環變量,檢查循環是否已經結束以及跳轉到循環的開始。

如果你想知道: paddq和類似的指令是SIMD指令,它允許並行地匯總多個值。 此外,並行使用兩個16字節SIMD寄存器( xmm0xmm1 ),以便CPU的指令級並行性基本上可以同時執行這些指令中的兩個。 畢竟,他們是彼此獨立的。 最后,將兩個寄存器相加,然后水平求和到標量結果。

現代主流x86 CPU(不是低功耗Atom)在L1d緩存中實際上每個時鍾可以執行2個向量加載,並且paddq吞吐量每個時鍾至少2個,在大多數CPU上有1個周期延遲。 請參閱https://agner.org/optimize/以及有關多個累加器的問答,以隱藏延遲(針對點積的FP FMA)和吞吐量的瓶頸。

LLVM在未完全展開時會展開一些小循環,並且仍使用多個累加器。 通常,即使沒有完全展開,前端帶寬和后端延遲瓶頸對於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_LOOPS更改為CAPACITY + IN_LOOPS 這是造成巨大性能差異的原因。


另外一個注意事項:你能對此做些什么嗎? 並不是的。 LLVM必須具有這樣的魔術閾值,如果沒有它們,LLVM優化可能需要永遠完成某些代碼。 但我們也同意這段代碼非常人為。 在實踐中,我懷疑會發生如此巨大的差異。 在這些情況下,由於完整循環展開的差異通常不是因子2。 所以不必擔心真實的用例。

作為關於慣用Rust代碼的最后一個注釋: arr.iter().sum()是一種更好的方法來總結數組的所有元素。 在第二個示例中更改此設置不會導致發出的組件有任何顯着差異。 您應該使用簡短版本和慣用版本,除非您測量它會損害性能。

除了Lukas的回答,如果你想使用迭代器,試試這個:

const CAPACITY: usize = 240;
const IN_LOOPS: usize = 500000;

pub fn bar() -> usize {
    (0..CAPACITY).sum::<usize>() * IN_LOOPS
}

感謝@Chris Morgan關於范圍模式的建議。

優化的組裝非常好:

example::bar:
        movabs  rax, 14340000000
        ret

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM