【问题标题】:What optimization techniques are applied to Rust code that sums up a simple arithmetic sequence?哪些优化技术应用于总结简单算术序列的 Rust 代码?
【发布时间】:2019-03-28 10:45:30
【问题描述】:

代码很幼稚:

use std::time;

fn main() {
    const NUM_LOOP: u64 = std::u64::MAX;
    let mut sum = 0u64;
    let now = time::Instant::now();
    for i in 0..NUM_LOOP {
        sum += i;
    }
    let d = now.elapsed();
    println!("{}", sum);
    println!("loop: {}.{:09}s", d.as_secs(), d.subsec_nanos());
}

输出是:

$ ./test.rs.out
9223372036854775809
loop: 0.000000060s
$ ./test.rs.out
9223372036854775809
loop: 0.000000052s
$ ./test.rs.out
9223372036854775809
loop: 0.000000045s
$ ./test.rs.out
9223372036854775809
loop: 0.000000041s
$ ./test.rs.out
9223372036854775809
loop: 0.000000046s
$ ./test.rs.out
9223372036854775809
loop: 0.000000047s
$ ./test.rs.out
9223372036854775809
loop: 0.000000045s

程序几乎立即结束。我还使用 for 循环在 C 中编写了一个等效代码,但它运行了很长时间。我想知道是什么让 Rust 代码如此之快。

C 代码:

#include <stdint.h>
#include <time.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>

double time_elapse(struct timespec start) {
    struct timespec now;
    clock_gettime(CLOCK_MONOTONIC, &now);
    return now.tv_sec - start.tv_sec +
           (now.tv_nsec - start.tv_nsec) / 1000000000.;
}

int main() {
    const uint64_t NUM_LOOP = 18446744073709551615u;
    uint64_t sum = 0;
    struct timespec now;
    clock_gettime(CLOCK_MONOTONIC, &now);

    for (int i = 0; i < NUM_LOOP; ++i) {
        sum += i;
    }

    double t = time_elapse(now);
    printf("value of sum is: %llu\n", sum);
    printf("time elapse is: %lf sec\n", t);

    return 0;
}

Rust 代码使用-O 编译,C 代码使用-O3 编译。 C 代码运行速度很慢,还没有停止。

在修复了 visibleman 和 Sandeep 发现的错误后,两个程序几乎立即打印了相同的数字。我试图将NUM_LOOP 减一,考虑到溢出,结果似乎是合理的。此外,使用NUM_LOOP = 1000000000,两个程序都不会溢出并立即产生正确的答案。这里使用了哪些优化?我知道我们可以使用像(0 + NUM_LOOP - 1) * NUM_LOOP / 2 这样的简单方程来计算结果,但我不认为这种计算是由编译器在溢出情况下完成的......

【问题讨论】:

  • 除了-O3,还值得添加-Wall -Wextra

标签: c rust llvm-codegen


【解决方案1】:

您的 Rust 代码(没有打印和计时)编译为 (On Godbolt):

movabs rax, -9223372036854775807
ret

LLVM 只是对整个函数进行 const 折叠并为您计算最终值。

让我们将上限设为动态(非常数)以避免这种激进的常数折叠:

pub fn foo(num: u64) -> u64 {
    let mut sum = 0u64;
    for i in 0..num {
        sum += i;
    }

    sum
}

这会导致 (Godbolt):

  test rdi, rdi            ; if num == 0
  je .LBB0_1               ; jump to .LBB0_1
  lea rax, [rdi - 1]       ; sum = num - 1
  lea rcx, [rdi - 2]       ; rcx = num - 2
  mul rcx                  ; sum = sum * rcx
  shld rdx, rax, 63        ; rdx = sum / 2
  lea rax, [rdx + rdi]     ; sum = rdx + num
  add rax, -1              ; sum -= 1
  ret
.LBB0_1:
  xor eax, eax             ; sum = 0
  ret

如您所见,优化器理解您将所有数字相加,从 0 到 num,并将循环替换为常数公式:((num - 1) * (num - 2)) / 2 + num - 1。至于上面的例子:优化器可能首先将代码优化成这个常量公式,然后进行常量折叠。

补充说明

  • 另外两个答案已经指出了您在 C 程序中的错误。修复后,clanggenerates exactly the same assembly(不出所料)。但是,GCC 似乎并不知道这个优化和generates pretty much the assembly you would expect (a loop)
  • 在 Rust 中,编写代码的更简单、更惯用的方法是 (0..num).sum()。尽管这使用了更多的抽象层(即迭代器),但编译器生成的代码与上面完全相同。
  • 要在 Rust 中打印 Duration,您可以使用 {:?} 格式说明符。 println!("{:.2?}", d); 以最合适的单位打印持续时间,精度为 2。这是为几乎所有类型的基准测试打印时间的好方法。

【讨论】:

    【解决方案2】:

    由于int 永远不会像您的NUM_LOOP 一样大,因此程序将永远循环。

    const uint64_t NUM_LOOP = 18446744073709551615u;
    
    for (int i = 0; i < NUM_LOOP; ++i) { // Change this to an uint64_t
    

    如果您修复了 int 错误,编译器将在这两种情况下优化掉这些循环。

    【讨论】:

    • 抱歉,忘记那部分了。非常感谢您的帮助。我还有一个问题:如果编译器优化了循环,程序从哪里得到数字?两个程序都打印相同的数字,我尝试将 NUM_LOOP 减一,两个程序的结果都是 9223372036854775811。考虑到溢出,这是有道理的。如果循环被优化出来,我们如何获得数字?我还尝试了 NUM_LOOP=1000000000,它不会产生溢出,并且两个程序的结果几乎都是 499999999500000000。程序如何做到这一点?
    • 实际上,我打算更新我的答案。循环可以用常数时间算术表示。编译器足够聪明地做到这一点。但是我之前测试过,如果进行这种优化,结果并不清楚,这取决于编译器版本和 NUM_LOOP 常量的值。
    【解决方案3】:

    您的代码陷入了无限循环。

    比较 i &lt; NUM_LOOP 将始终返回 true,因为 int i 将在到达 NUM_LOOP 之前回绕

    【讨论】:

      猜你喜欢
      • 2017-02-18
      • 1970-01-01
      • 2010-09-22
      • 1970-01-01
      • 2010-09-07
      • 1970-01-01
      • 2022-09-23
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多