【问题标题】:Rust loop performance same as python [duplicate]Rust循环性能与python相同[重复]
【发布时间】:2021-11-18 01:14:41
【问题描述】:

我正在研究 mandelbrot 算法来学习 Rust,我发现空的 25mil(大约 6k 图像)循环需要 0.5 秒。我发现它很慢。所以我去用python测试它,发现它几乎需要同样的时间。真的 python 的 for 循环几乎是零成本抽象吗?这真的是我用英特尔 i7 能得到的最好的吗?

生锈:

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

更新: 如果我使用初始化元组而不是范围,python 甚至比 rust -> 0.33s

【问题讨论】:

  • Python 从来不是为了速度而生的。
  • 我猜你没看帖子...我是说python和rust速度一样
  • 它仍然慢了很多。正如 john 指出的那样,使用 --release 构建。
  • 我们应该调用 rust: Rust Don't Forget To Compile In Release
  • @Stargateur 如果 StackOverflow 允许以这样的方式自定义标签,那就太好了,当您在编写问题时使用 Rust 标签时,会询问“您是否在发布模式下运行??”弹出,你不能在回答之前发布问题......

标签: python performance for-loop rust


【解决方案1】:

如果您要进行性能测试,请始终使用 --release 构建。默认情况下,Cargo 构建时启用调试信息并禁用优化。优化器将完全消除这些循环。在Playground 上,它从 975ms 下降到 1.25µs。

让我们看一下Godbolt 上的程序集,仅包含循环,没有计时器:

pub fn main() {
    for i in 0..5000 {
        for j in 0..5000 {}
    }
}

Without optimization:

<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

With optimization

example::main:
        ret

【讨论】:

  • 谢谢,我现在可以看到区别了。为什么会有这样的差异? 'cargo run' 不会生锈吗?
  • 虽然这对于 Rust 的一半事情来说是一个很好的答案,但我认为更完整的答案可以提到在 CPython 中如何实现 for 循环:与 while 循环不同,所有的迭代和边界检查对于像这样的裸 for 循环是在 C 中完成的,而不是直接通过 Python 字节码
  • @kcsquared 很难用一个全面的答案来涵盖 Python 方面,因为 OP 预计 Python 会比 Rust 慢,而事实证明它确实更慢。 (不可能说慢了多少,因为 Rust 在发布模式下优化了整个迭代。)人们可以在不同的细节层次上展示 CPython 如何比想象中的幼稚实现更快,其中for 脱糖为while,但是重点是什么?请注意,这绝不是对 Python 的批评,我实际上喜欢它。 :)
  • @user4815162342 确实如此。尽管我的建议来自对 Rust 错误的无知并试图回答“为什么 Python 的 for 比预期的要快”而不是“为什么这个 Rust 代码比它应该的要慢得多”,但我也找到了链接到Stackoverflow posts already providing exactly my requested explanation about Python.,所以链接到那些比重复他们的答案更好。
  • @Martin 编译只是意味着获取源代码并将其映射到其他目标,在 Rust 中,该目标是本机二进制文件。这与此类编译可能进行的优化完全正交 - 您需要使用 --release 询问它们。
【解决方案2】:

Python vs. Rust(秒 vs. 皮秒)- 性能绝对不一样

CPython 3.8.10 / rustc 1.55.0(在 10+ yo 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 程序的调试版本,但与发布版本相差甚远。

【讨论】:

    猜你喜欢
    • 2013-07-18
    • 2015-08-28
    • 2016-11-25
    • 1970-01-01
    • 2014-09-10
    • 2016-04-13
    • 2021-02-25
    • 1970-01-01
    • 2020-12-24
    相关资源
    最近更新 更多