当前位置: 首页 > 知识库问答 >
问题:

为什么在包含范围内的迭代会在Rust中生成更长的装配?

祁飞飙
2023-03-14

这两个回路在C和Rust中等效:

#include <cstdint>
uint64_t sum1(uint64_t n) {  
    uint64_t sum = 0;
    for (uint64_t j = 0; j <= n; ++j) {
        sum += 1;
    }
    return sum;
}
pub fn sum1(num: u64) -> u64 {
    let mut sum: u64 = 0;
    for j in 0u64..=num {
        sum += 1;
    }
    return sum;
}

但是,C版本生成了一个非常简洁的程序集:

sum1(unsigned long):                               # @sum1(unsigned long)
        xor     eax, eax
.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        add     rax, 1
        cmp     rax, rdi
        jbe     .LBB0_1
        ret

而Rust的版本很长,循环中有两个检查,而不是一个:

example::sum1:
        xor     eax, eax
        xor     ecx, ecx
.LBB0_1:
        mov     rdx, rcx
        cmp     rcx, rdi
        adc     rcx, 0
        add     rax, 1
        cmp     rdx, rdi
        jae     .LBB0_3
        cmp     rcx, rdi
        jbe     .LBB0_1
.LBB0_3:
        ret

C无忧无虑地想阻止什么?

锁销:https://godbolt.org/z/xYW94qxjK

共有3个答案

容修贤
2023-03-14

@user3840170正确识别了差异:在Rust中进行溢出检查,而在C中没有。

然而,问题仍然是为什么在Rust版本中每个循环有2个检查而不是1个,答案是LLVM不够智能和/或Range包容性设计不适合LLVM1.

短循环的最佳代码生成是分割循环,转换:

for j in 0u64..=num {
    sum += 1;
}

进入:

for j in 0u64..num { // equivalent to for (auto j = 0; j < num; ++j)
    sum += 1;
}

if 0 <= num {
    sum += 1;
}

这将导致主循环中只有一个分支,并允许LLVM将其切换到闭合公式2

循环拆分优化适用于RangeInclusive和大多数其他Chain迭代器,因为实际上RangeInclusive可以被视为一次迭代器和半排他范围迭代器(按任意顺序)的链。这并不总是一个胜利:与内联一样,它意味着复制循环体的代码,如果较大,可能会导致代码大小的显著开销。

不幸的是,LLVM无法分割循环。我不确定这是因为优化完全缺失了,还是因为某种原因无法在这里应用它。我期待着rustc_codegen_gcc后端,因为gcc7向gcc添加了循环拆分,它可能能够在那里生成更好的代码。

1查看我对RangeInclusive性能问题留下的评论。2019年,我花了大量时间研究这个问题,我非常希望RangeInclusive(以及所有范围)不是Iterator;这是一个代价高昂的错误。

2链迭代器通常使用执行得更好。对于每个(…) ,特别是因为存在(手动)拆分循环。有关(0..=num)的信息,请参见操场。对于每个(| | sum=1)减少到num 1

刁钧
2023-03-14

这两个回路在C和Rust中是等效的

你的两个被剪断的代码不具有相同的行为<代码>用于(uint64_t j=0;j

防锈等效代码可以是:

pub fn sum1(num: u64) -> u64 {
    let mut sum: u64 = 0;
    let mut j = 0;
    while j <= num {
        sum = sum.wrapping_add(1);
        j = j.wrapping_add(1);
    }
    sum
}

目前生产以下asm锁紧螺栓:

example::sum1:
        xor     eax, eax
.LBB0_1:
        add     rax, 1
        cmp     rax, rdi
        jbe     .LBB0_1
        ret
严信瑞
2023-03-14

迭代器状态下的溢出。

当给定足够大的输入时,C版本将永远循环:

#include <cstdint>

[[gnu::noinline]]
std::uint64_t sum1(std::uint64_t n) {  
    std::uint64_t sum = 0;
    for (std::uint64_t j = 0; j <= n; ++j) {
        sum += 1;
    }
    return sum;
}

#include <iostream>

int main() {
    std::cout << sum1(UINT64_C(0xffffffff'ffffffff)) << std::endl;

    return 0;
}

这是因为当循环计数器j最终达到0xffffffff'ffffffffff时,将其递增到0,这意味着循环不变量j

严格来说,使用0xffffffff'ffffffff调用sum1会引发未定义的行为,尽管这并不是因为溢出本身,而是因为没有外部可见副作用的无限循环是UB([intro.progress]/1)。这就是为什么我将[[gnu::noinline]]属性应用到函数中,以防止编译器在优化过程中利用这一点。

另一方面,Rust版本不仅定义完美,而且迭代次数与范围的基数相同:

use std::num::Wrapping;

fn sum1(num: u64) -> u64 {
    let mut sum = Wrapping(0);
    for _ in 0..=num {
        sum += Wrapping(1);
    }
    return sum.0;
}

fn main() {
    println!("{}", sum1(0xffffffff_ffffffff));
}

上面的程序(稍微修改以避免陷入调试与发布模式的差异相对于求和)将在18446744073709551616迭代和打印0之后终止;Rust版本小心地维护迭代器状态,以便溢出不会发生在迭代器。

 类似资料:
  • 编者注:此代码示例来自Rust 1.0之前的版本,在语法上不是有效的Rust 1.0代码。此代码的更新版本会产生不同的错误,但答案仍然包含有价值的信息。 我遇到了下面的例子,说明了如何使用Rust生成一个随机数,但它似乎不起作用。这个例子没有显示它适用于哪个版本的Rust,所以可能它已经过时了,或者可能我搞错了什么。 当我试图编译此文件时,会出现以下错误: 在同一页(上面)上还有另一个例子(如下所

  • 问题内容: Swift 2.2可以实现以下功能: 使用3.0,我们会收到以下错误: 类型“范围”(又名“范围”)不符合协议“序列” 我正在尝试对字符串进行快速的非常简单的操作-只需遍历字符串的前半部分(或更常见的问题:遍历字符串的范围)。 我可以执行以下操作: 但是这里我并没有真正遍历字符串。所以问题是:如何遍历给定字符串的范围。喜欢: 问题答案: 您可以使用以下属性来遍历字符串: 从“ 字符串和

  • 我正在尝试用maven-shade插件2.1构建一个uber-jar。我希望它包括我的jar和依赖jar中的所有类。但我发现它不包括依赖jar中的类。我可能做错了什么?以下是我在pom.xml中对maven-shade插件的用法。可能是因为finalName与project.artifactid相同吗?

  • 我一直在尝试在listview生成器的末尾添加一个按钮。我试着做这个问题中建议的事情:Flutter:如何在ListView的末尾添加一个按钮小部件。包含其他类型小部件的生成器?。但如果我这样做,我会得到:“RangeError(index):无效值:不在包含范围0..49:50中 我试图寻找问题,也有这个问题,但我找不到一个答案,解决它。

  • 问题内容: 我试图找出一种方法来更改每个打印的步骤值 我尝试过的 问题答案: 我建议为此使用循环实现自己的生成器。范例- 然后,您可以将其用作- 我们这样做是可以进行任何迭代的。 演示-

  • 问题内容: 我看过很多文章,解释了这个问题,但是他们都使用整数值,老实说,我并没有完全理解它,所以这个问题: 我正在尝试在Java中生成从(-1554900.101)到(52952058699.3098)范围的随机数,我想知道是否有人可以向我解释这一点,因为我真的很想理解它。 我的想法:这是正确的方法吗?1)生成一个在我指定范围内的随机整数2)将生成的数除以pi以得到float / double随