在Rust中对数组运行求和循环时,我注意到当capacity
>=240时性能会有很大的下降。容量
=239大约快80倍。
使用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
s的数组,因此长度乘以8字节,假设CPU为x86-64))。在这个基准中,一个特定的优化--仅对长度239执行--是造成巨大速度差异的原因。但让我们慢慢开始:
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编译器资源管理器。下面是装配的一小部分:
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寄存器(XMM0
和XMM1
),使得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编译器资源管理器上)
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()
是总结数组的所有元素的更好方法。并且在第二个示例中更改这一点不会导致发射组件的任何显著差异。您应该使用简短和惯用的版本,除非您测量到它会损害性能。
问题内容: String s = “”; for(i=0;i<....){ s = some Assignment; } 要么 我不需要在循环外再次使用“ s”。第一个选项可能更好,因为不会每次都初始化一个新的String。但是,第二个结果将导致变量的范围仅限于循环本身。 编辑:回应米尔豪斯的回答。在循环中将String分配给常量是没有意义的吗?不,这里的“某些分配”是指从要迭代的列表中获得的变化
为什么我只得到while循环外数组的最后一个元素?如何在循环之外获取数组中的所有元素?
问题内容: 所以我有一个查询,在SELECT中需要一堆CASE语句。这不是原始的设计,而是折衷的一部分。 因此查询看起来像这样: 我的问题是,将所有这些CASE语句更改为直接列引用会对性能产生什么影响。 换句话说:如果我将每个CASE语句更改为一个列名,并从查询中删除了所有CASE语句,那么会对性能产生很大影响,为什么? 我正在对此进行测试,因此我可以弄清楚性能是否受到影响,但是我对WHY的细节也
问题内容: 我编写了以下JavaScript: 此代码声明一个变量并将其设置为数组值。然后,它声明第二个变量并将其设置为。它对进行操作,然后向和发出警报。不知何故,当我对执行操作时,似乎对执行了相同的操作。 然后,代码对数字值执行相同的操作:声明一个变量并将其设置为数字值。然后,它声明第二个变量并将其设置为。它对进行操作,然后向和发出警报。在这里,我得到预期的行为:对不同的价值观和。 数组与Jav
我有一个包含复杂条目的数组列表的结构,我想将它们转换为浮点数。虚部可以丢弃,这很好。 我想知道,为什么这不起作用,而另一方面,在创建列表“数组”之前将类型更改为float会起作用。 这是一个非常基本的问题,但如果有人能分享他或她的想法,我将非常高兴。 感谢提前:)
如果你用移动设备在这个网页上点击它们,它们就会变小。我怎么能阻止它? 和我的CSS样式部分有关系吗?