Rust循环性能与Python相同。

4

我在学习Rust时正在使用曼德博集算法,发现一个空的2500万(大约6k图像)循环需要0.5秒。我觉得速度相当慢。于是我去用Python进行测试,结果发现它几乎需要同样的时间。真的是因为Python的for循环几乎没有成本抽象吗?这真的是我在Intel i7上可以得到的最好结果吗?

Rust:

use std::time::Instant;
fn main() {
    let before = Instant::now();

    for i in 0..5000 {
        for j in 0..5000 {}
    }
    println!("Elapsed time: {:.2?}", before.elapsed());
}

>>> Elapsed time: 406.90ms

Python:

import time

s = time.time()

for i in range(5000):
    for j in range(5000):
        pass

print(time.time()-s)
>>> 0.5715351104736328

更新:如果我使用初始化的元组而不是range,Python的速度甚至比Rust更快 -> 0.33秒


Python从来不是为了速度而生的。 - chess_lover_6
我猜你没有读帖子...我是说Python和Rust的速度相同。 - Martin
仍然慢了相当多。正如约翰指出的那样,使用--release构建。 - chess_lover_6
2
我们应该调用 Rust:Rust 不要忘记在 Release 模式下编译。 - Stargateur
3
@Stargateur 如果StackOverflow允许自定义标签,那将非常好。例如,当您在编写问题时使用Rust标签时,会弹出“您是否在发布模式下运行?”的询问,只有回答后才能发布问题... - user4815162342
@Stargateur,其实不完全是这样。即使没有启用--release模式,我的Mandelbrot Rust实现性能已经比Python快了30倍,这已经让我印象深刻了。但是将Python和Rust的空循环进行比较可能会带来一些不确定性。我的意思是,空循环真的是尽可能原始的情况。谁能想到优化可以在那里发挥作用呢?而且谁会想到Python使用元组迭代比未经优化的Rust更快呢? - Martin
2个回答

13
如果您正在进行性能测试,请始终使用--release进行构建。默认情况下,Cargo启用调试信息并禁用优化。优化器将完全消除这些循环。在Playground上,它从975ms降至1.25µs。让我们来看看Godbolt上的汇编,只针对循环,没有计时器:
pub fn main() {
    for i in 0..5000 {
        for j in 0..5000 {}
    }
}

未优化代码

<i32 as core::iter::range::Step>::forward_unchecked:
        push    rax
        mov     eax, esi
        add     edi, eax
        mov     dword ptr [rsp + 4], edi
        mov     eax, dword ptr [rsp + 4]
        mov     dword ptr [rsp], eax
        mov     eax, dword ptr [rsp]
        pop     rcx
        ret

core::intrinsics::copy_nonoverlapping:
        push    rax
        mov     qword ptr [rsp], rsi
        mov     rsi, rdi
        mov     rdi, qword ptr [rsp]
        shl     rdx, 2
        call    memcpy@PLT
        pop     rax
        ret

core::cmp::impls::<impl core::cmp::PartialOrd for i32>::lt:
        mov     eax, dword ptr [rdi]
        cmp     eax, dword ptr [rsi]
        setl    al
        and     al, 1
        movzx   eax, al
        ret

core::mem::replace:
        sub     rsp, 40
        mov     qword ptr [rsp], rdi
        mov     dword ptr [rsp + 12], esi
        mov     byte ptr [rsp + 23], 0
        mov     byte ptr [rsp + 23], 1
        mov     rax, qword ptr [rip + core::ptr::read@GOTPCREL]
        call    rax
        mov     ecx, eax
        mov     dword ptr [rsp + 16], ecx
        jmp     .LBB3_1
.LBB3_1:
        mov     esi, dword ptr [rsp + 12]
        mov     rdi, qword ptr [rsp]
        mov     byte ptr [rsp + 23], 0
        mov     rcx, qword ptr [rip + core::ptr::write@GOTPCREL]
        call    rcx
        jmp     .LBB3_4
.LBB3_2:
        test    byte ptr [rsp + 23], 1
        jne     .LBB3_8
        jmp     .LBB3_7
        mov     rcx, rax
        mov     eax, edx
        mov     qword ptr [rsp + 24], rcx
        mov     dword ptr [rsp + 32], eax
        jmp     .LBB3_2
.LBB3_4:
        mov     eax, dword ptr [rsp + 16]
        add     rsp, 40
        ret
.LBB3_5:
        jmp     .LBB3_2
        mov     rcx, rax
        mov     eax, edx
        mov     qword ptr [rsp + 24], rcx
        mov     dword ptr [rsp + 32], eax
        jmp     .LBB3_5
.LBB3_7:
        mov     rdi, qword ptr [rsp + 24]
        call    _Unwind_Resume@PLT
        ud2
.LBB3_8:
        jmp     .LBB3_7

core::ptr::read:
        sub     rsp, 24
        mov     qword ptr [rsp + 8], rdi
        mov     eax, dword ptr [rsp + 20]
        mov     dword ptr [rsp + 16], eax
        jmp     .LBB4_2
.LBB4_2:
        mov     rdi, qword ptr [rsp + 8]
        lea     rsi, [rsp + 16]
        mov     edx, 1
        call    qword ptr [rip + core::intrinsics::copy_nonoverlapping@GOTPCREL]
        mov     eax, dword ptr [rsp + 16]
        mov     dword ptr [rsp + 4], eax
        mov     eax, dword ptr [rsp + 4]
        add     rsp, 24
        ret

core::ptr::write:
        sub     rsp, 4
        mov     dword ptr [rsp], esi
        mov     eax, dword ptr [rsp]
        mov     dword ptr [rdi], eax
        add     rsp, 4
        ret

core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next:
        push    rax
        call    qword ptr [rip + <core::ops::range::Range<T> as core::iter::range::RangeIteratorImpl>::spec_next@GOTPCREL]
        mov     dword ptr [rsp], eax
        mov     dword ptr [rsp + 4], edx
        mov     edx, dword ptr [rsp + 4]
        mov     eax, dword ptr [rsp]
        pop     rcx
        ret

core::clone::impls::<impl core::clone::Clone for i32>::clone:
        mov     eax, dword ptr [rdi]
        ret

<I as core::iter::traits::collect::IntoIterator>::into_iter:
        mov     edx, esi
        mov     eax, edi
        ret

<core::ops::range::Range<T> as core::iter::range::RangeIteratorImpl>::spec_next:
        sub     rsp, 40
        mov     rsi, rdi
        mov     qword ptr [rsp + 16], rsi
        mov     rdi, rsi
        add     rsi, 4
        call    core::cmp::impls::<impl core::cmp::PartialOrd for i32>::lt
        mov     byte ptr [rsp + 31], al
        mov     al, byte ptr [rsp + 31]
        test    al, 1
        jne     .LBB9_3
        jmp     .LBB9_2
.LBB9_2:
        mov     dword ptr [rsp + 32], 0
        jmp     .LBB9_7
.LBB9_3:
        mov     rdi, qword ptr [rsp + 16]
        call    core::clone::impls::<impl core::clone::Clone for i32>::clone
        mov     dword ptr [rsp + 12], eax
        mov     edi, dword ptr [rsp + 12]
        mov     esi, 1
        call    <i32 as core::iter::range::Step>::forward_unchecked
        mov     dword ptr [rsp + 8], eax
        mov     esi, dword ptr [rsp + 8]
        mov     rdi, qword ptr [rsp + 16]
        call    qword ptr [rip + core::mem::replace@GOTPCREL]
        mov     dword ptr [rsp + 4], eax
        mov     eax, dword ptr [rsp + 4]
        mov     dword ptr [rsp + 36], eax
        mov     dword ptr [rsp + 32], 1
.LBB9_7:
        mov     eax, dword ptr [rsp + 32]
        mov     edx, dword ptr [rsp + 36]
        add     rsp, 40
        ret

example::main:
        sub     rsp, 72
        mov     dword ptr [rsp + 24], 0
        mov     dword ptr [rsp + 28], 5000
        mov     edi, dword ptr [rsp + 24]
        mov     esi, dword ptr [rsp + 28]
        call    qword ptr [rip + <I as core::iter::traits::collect::IntoIterator>::into_iter@GOTPCREL]
        mov     dword ptr [rsp + 16], eax
        mov     dword ptr [rsp + 20], edx
        mov     eax, dword ptr [rsp + 20]
        mov     ecx, dword ptr [rsp + 16]
        mov     dword ptr [rsp + 32], ecx
        mov     dword ptr [rsp + 36], eax
.LBB10_2:
        mov     rax, qword ptr [rip + core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next@GOTPCREL]
        lea     rdi, [rsp + 32]
        call    rax
        mov     dword ptr [rsp + 44], edx
        mov     dword ptr [rsp + 40], eax
        mov     eax, dword ptr [rsp + 40]
        test    rax, rax
        je      .LBB10_5
        jmp     .LBB10_13
.LBB10_13:
        jmp     .LBB10_6
        ud2
.LBB10_5:
        add     rsp, 72
        ret
.LBB10_6:
        mov     dword ptr [rsp + 48], 0
        mov     dword ptr [rsp + 52], 5000
        mov     edi, dword ptr [rsp + 48]
        mov     esi, dword ptr [rsp + 52]
        call    qword ptr [rip + <I as core::iter::traits::collect::IntoIterator>::into_iter@GOTPCREL]
        mov     dword ptr [rsp + 8], eax
        mov     dword ptr [rsp + 12], edx
        mov     eax, dword ptr [rsp + 12]
        mov     ecx, dword ptr [rsp + 8]
        mov     dword ptr [rsp + 56], ecx
        mov     dword ptr [rsp + 60], eax
.LBB10_8:
        mov     rax, qword ptr [rip + core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next@GOTPCREL]
        lea     rdi, [rsp + 56]
        call    rax
        mov     dword ptr [rsp + 68], edx
        mov     dword ptr [rsp + 64], eax
        mov     eax, dword ptr [rsp + 64]
        test    rax, rax
        je      .LBB10_11
        jmp     .LBB10_14
.LBB10_14:
        jmp     .LBB10_12
        ud2
.LBB10_11:
        jmp     .LBB10_2
.LBB10_12:
        jmp     .LBB10_8

__rustc_debug_gdb_scripts_section__:
        .asciz  "\001gdb_load_rust_pretty_printers.py"

DW.ref.rust_eh_personality:
        .quad   rust_eh_personality

使用优化后的代码

example::main:
        ret

谢谢,我现在能看到区别了。为什么会有这样的区别?难道不是在“cargo run”编译时都会产生rust吗? - Martin
2
虽然这是 Rust 部分的一个很好的答案,但我认为更全面的答案应该提到 for 循环在 CPython 中的实现方式:与 while 循环不同,像这样的裸 for 循环的所有迭代和边界检查都是通过 C 实现的,而不是直接通过 Python 字节码实现的。 - kcsquared
1
@kcsquared 很难用一个全面的答案来涵盖 Python 方面的内容,因为 OP 希望 Python 比 Rust 更慢,而实际上确实更慢。 (由于 Rust 在发布模式下优化了整个迭代过程,因此无法确定它比 Python 慢多少。)人们可以以不同级别的详细程度展示 CPython 比想象中的天真实现(其中 for 变成了 while)要快,但这有什么意义呢?请注意,这绝不是对 Python 的批评,我实际上很喜欢它。 :) - user4815162342
1
@user4815162342 那是真的。尽管我的建议来自于对Rust错误的无知,并试图回答“为什么Python的for循环比预期快”,而不是“为什么这段Rust代码比应该慢得多”,但我后来也找到了 Stackoverflow帖子的链接,已经提供了关于Python的完全符合我的要求的解释。,所以一个链接比重复他们的答案更好。 - kcsquared
1
@Martin 编译只是将源代码映射到其他目标的过程,在 Rust 中,这个目标是本地二进制文件。这与编译可能进行的优化完全无关 - 您需要使用“--release”来请求它们。 - GManNickG

4
Python vs. Rust(秒 vs 纳秒)- 性能绝对不同

使用CPython 3.8.10 / rustc 1.55.0(在由10多年前的Mac主机托管的Linux客户端上运行)。

为了确保循环不被过度优化,向Rust代码添加了一些步骤。我认为确保这一点的最好方法是接收用户输入以初始化某些变量,在循环中更新这些变量,并打印到标准输出。它仍将进行优化,但至少循环不会消失。

use std::error::Error;
use std::env::args;
use timeit::timeit_loops;
use timeit::timeit;

fn main() -> Result<(), Box<dyn Error>>
{
    let a = args().skip(1).map(|s| s.parse())
                  .collect::<Result<Vec<usize>, _>>()?;
    let n = a[0];
    let m = a[1];
    let mut d = 0;
   
    // timeit increases the nesting of the loops to get enough
    // samples of the timed code to calculate a good average.
    // The number of loops timeit takes for sampling is included 
    // in the output.
    timeit!({
        for i in 0..5000 {
            d += m * i;
            for j in 0..5000 {
                d += n * j;
            }
        }
    });
    println!("d: {}", d);
    Ok(())
}

输出(每个timeit循环的平均纳秒数):

$ cargo run --release -- 52 3
1000000 loops: 0.000029 ns
    :

相应的Python代码,设置为运行1个timeit循环 - 我没有耐心让它执行更多。在Python上,这些嵌套循环非常缓慢。

import sys
import timeit

if __name__ == '__main__':
    a = list(map(int, sys.argv[1:]))
    n = a[0]
    m = a[1]
    d = 0
   
    def loops():
        global n, m, d
        for i in range(5000):
            d += m * i;
            for j in range(5000):
                d += n * j;

    # The Rust timeit version iterated 1000000; Python is too
    # slow to let timeit run a fraction of that number.
    print(timeit.timeit(loops, number=1)) 
    print(d)

输出(以秒为单位):

$ python loops.py 52 3
3.8085460959700868
    :

运行Rust程序的调试版本也超过了Python:811毫秒 vs. 3.8秒

在PyPy 3.6.9上运行相同的程序(Rust获胜)

$ pypy3 loops.py 52 3
0.05104120302712545
    :

PyPy3击败了Rust程序的调试版本,但距离发布版本差距很大。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接