【问题标题】:Why does moving a value generate LLVM IR that includes a value copy?为什么移动值会生成包含值副本的 LLVM IR?
【发布时间】:2021-07-26 15:23:19
【问题描述】:

如果我将一个变量(具有没有复制语义的类型)分配给第二个变量,即使第一个变量不能再使用/访问,rustc 也会创建该值的副本。

例如:

#[derive(Debug)]
struct T {
    val: i32,
}
fn main() {
    let v1 = T { val: 123 };
    let v2 = v1;
    // println!("{:?}", v1); error!
    println!("{:?}", v2);
}

这会生成包含两个存储操作的 LLVM IR:

  %v2 = alloca i32, align 4
  %v1 = alloca i32, align 4
  store i32 123, i32* %v1, align 4
  store i32 123, i32* %v2, align 4

如果第一个副本不能再使用,用移动语义复制值不是不必要的工作量吗?

【问题讨论】:

    标签: rust move


    【解决方案1】:

    LLVM 专门用于执行优化。前端将尝试对它所知道的有关 LLVM IR 中正在执行的操作的所有内容进行编码,然后在 LLVM IR 上调用优化。

    如果你开启优化,你确实只会得到:

      %v2 = alloca i32, align 4
      %0 = bitcast i32* %v2 to i8*, !dbg !36
      call void @llvm.lifetime.start.p0i8(i64 4, i8* nonnull %0), !dbg !36
      store i32 123, i32* %v2, align 4, !dbg !37
    

    %v1 已被完全优化掉。

    https://godbolt.org/z/fW9457eEn

    【讨论】:

    • 但是为什么首先要对它进行编码呢?编译器知道第一个副本不能再使用,因为它被语义禁止。所以根本没有理由生成这样的指令。如果 llvm 无法检测到“垃圾副本”,那不会引入不必要的副本吗?
    • @Sebi2020 为什么编译器会选择重新实现 LLVM 的所有优化?
    • @Sebi2020 我说的是一般。在这种特殊情况下,rust 编译器似乎没有对这条信息进行编码。编译器以这种方式工作的原因通常是通过抽象来降低复杂性。编译器是庞大而复杂的软件。语义分析通常完全独立于代码生成而发生。如果 LLVM 已经可以处理这种情况并且只会降低可维护性,那么再次实现这种逻辑就没有任何意义。编写编译器通常需要在可维护性和优化每个极端情况之间进行权衡。
    • @Shepmaster 没有人说 LLVM 的每个优化都应该由编译器实现。我只是想知道为什么编译器将移动转换为副本。这对我来说毫无意义。
    • @idmean 是的,我明白分离关注点是个好主意,但在某些情况下这种分离会失败。例如,我发现 stackoverflow 上的线程显示 llvm 无法对大型数组进行这种优化,但编译器(它的 sem.analyzer 部分)知道我们只是为相同的值使用了一个新别名(通过移动它)。我只是想知道,因为移动语义是一种基本的语言结构(它甚至有自己的关键字move),所以将它作为一个单独的操作是有意义的,这可以反映在代码生成阶段,但不是t.
    【解决方案2】:

    因为复制与移动是语言语义,而不是在生成的代码中如何完成。 Rust 总是(也许已经改变了?)生成副本,然后逻辑上不允许使用移动的值。它依赖于 LLVM 删除不需要的副本的能力。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多