在Swift中,在循环中声明变量有多糟糕

时间:2018-11-05 01:12:41

标签: swift

我不了解Swift的所有机制,也不了解它如何处理变量。

我总是更喜欢在进入forwhile循环之前声明变量,而与语言无关,而不是一遍又一遍地在循环中声明它们。

但是重新声明变量是否很糟糕?迭代是否会影响性能? 特别是Swift 如何处理此行为?

示例:

while i < 100 {
  let a = someFunc()
  i += 1
}

VS

let a: MyObj
while i < 100 {
 a = someFunc()
 i += 1
}

2 个答案:

答案 0 :(得分:6)

Swift可以像大多数语言一样处理它。局部变量在堆栈上声明,并在退出定义它们的作用域时从堆栈中弹出。压入和弹出堆栈是一种非常低成本的操作。

Swift使用的LLVM编译器使用了相当高级的代码优化,尤其是在发布模式下。在您的简单示例中,您正在使用的变量可能会被优化,因为它们实际上并没有用于任何事情。

编辑:

总而言之,两个变量之间没有明显的性能差异,第一个将变量放入循环中的方法更加简洁,如rmaddy在其评论中所述。最好在尽可能狭窄的范围内定义变量。它显示了您对变量的意图,并避免了意外的后果。

答案 1 :(得分:6)

这不会影响性能,因此高度推荐使用版本1。即使会影响性能,您也需要在精确的代码上进行演示,然后再考虑使用除版本1外的其他任何选项。在处理优化编译器时,没有通用的性能答案。如果您未对代码进行深入探索,则执行任何不寻常的“性能”操作很可能会使情况变得更糟。正常情况是最优化的情况。

(我知道我夸大了这一点。肯定有一些方法可以查看代码并说“这将是非常低效的。”而且在Swift的一些古怪部分中,看起来不错的事情实际上是不好的,大多数特别是使用+组合字符串,或使用pre-Swift4 reduce创建数组。但是在那些很重要的情况下,您会很快发现它,因为它们是 真的很糟糕。

但是我们不必猜测任何一个。我们可以问编译器。

// inside.swift
import Foundation

func runme() {
    var i = 0
    while i < 100 {
      let a = Int.random(in: 0...10)
      print(a)
      i += 1
    }
}


// outside.swift
import Foundation

func runme() {
    var i = 0
    var a: Int
    while i < 100 {
      a = Int.random(in: 0...10)
      print(a)
      i += 1
    }
}

首先,请注意,我将它们放在一个函数中。那很重要将它们放在顶层会使a在一种情况下成为全局变量,并且全局变量具有特殊处理,包括线程安全的初始化,这使“外部”情况比在更常规的用法中看起来更加昂贵和复杂。 。 (很难以一种可以得出一般的“更快”结论的方式正确地测试微优化,这是非常困难的。)

第二个注意print。我们需要确保以副作用的方式使用a,否则优化程序可能会完全删除它。 print非常好,即使很复杂。您还可以使用结果来修改全局变量,但是编译器肯定可以更加积极地优化它,并且可以消除我们希望看到的内容。 (您真的 真的必须在您关心的实际情况下测试这些东西。)

现在,我们可以使用swiftc -O -emit-sil来了解Swift将如何处理这些问题。 -O是至关重要的。如此多的人试图在不打开优化器的情况下进行性能测试,而这些结果毫无意义。

那么SIL是什么样的? (快速中级语言。这是将程序转换为机器代码的第一步。如果两件事生成相同的SIL,它们将生成同一机器代码。)

SIL有点长(8000行),所以我要对其进行一些修剪。我在<>中的评论。这将有点乏味,因为探索这些东西非常挑剔。如果要跳过它,则TL-DR是:这两段代码之间没有区别。不是“没关系的小差异”。从字面上看(除对调试器的提示外),没有区别。

// runme()
sil hidden @$S4main5runmeyyF : $@convention(thin) () -> () {
bb0:
  ... <define a bunch of variables and function calls> ...

<compute the random number and put it in %29>
// %19                                            // user: %49
bb1(%19 : $Builtin.Int64):                        // Preds: bb5 bb0
  %20 = alloc_stack $SystemRandomNumberGenerator  // users: %23, %30, %21
  store %2 to %20 : $*SystemRandomNumberGenerator // id: %21
  br bb2                                          // id: %22

bb2:                                              // Preds: bb3 bb1
  %23 = apply %6<SystemRandomNumberGenerator>(%20, %5) : $@convention(method) <τ_0_0 where τ_0_0 : RandomNumberGenerator> (@inout τ_0_0, @thin UInt.Type) -> UInt // user: %24
  %24 = struct_extract %23 : $UInt, #UInt._value  // users: %28, %25
  %25 = builtin "cmp_ult_Int64"(%24 : $Builtin.Int64, %4 : $Builtin.Int64) : $Builtin.Int1 // user: %26
  cond_br %25, bb3, bb4                           // id: %26

bb3:                                              // Preds: bb2
  br bb2                                          // id: %27

bb4:                                              // Preds: bb2
  %28 = builtin "urem_Int64"(%24 : $Builtin.Int64, %3 : $Builtin.Int64) : $Builtin.Int64 // user: %29
  %29 = struct $Int (%28 : $Builtin.Int64)        // users: %42, %31
  dealloc_stack %20 : $*SystemRandomNumberGenerator // id: %30


< *** Note that %29 is called "a" *** >

  debug_value %29 : $Int, let, name "a"           // id: %31

... < The print call. This is a lot more code than you think it is...> ...

< Add one to i and check for overflow >

  %49 = builtin "sadd_with_overflow_Int64"(%19 : $Builtin.Int64, %8 : $Builtin.Int64, %13 : $Builtin.Int1) : $(Builtin.Int64, Builtin.Int1) // users: %51, %50
  %50 = tuple_extract %49 : $(Builtin.Int64, Builtin.Int1), 0 // users: %55, %53
  %51 = tuple_extract %49 : $(Builtin.Int64, Builtin.Int1), 1 // user: %52
  cond_fail %51 : $Builtin.Int1                   // id: %52


< Loop if i < 100 >
  %53 = builtin "cmp_slt_Int64"(%50 : $Builtin.Int64, %1 : $Builtin.Int64) : $Builtin.Int1 // user: %54
  cond_br %53, bb5, bb6                           // id: %54

bb5:                                              // Preds: bb4
  br bb1(%50 : $Builtin.Int64)                    // id: %55

bb6:                                              // Preds: bb4
  %56 = tuple ()                                  // user: %57
  return %56 : $()                                // id: %57
} // end sil function '$S4main5runmeyyF'

“外部”代码几乎相同。有什么不同?注意上面的代码中***标记了对debug_value的调用吗?因为“ a被定义为函数变量而不是块变量,所以在”外部”中缺少该变量。

知道这两个都缺少什么? alloc_stack调用“ a”。这是一个整数;它可以放在寄存器中。无论是将其存储在寄存器还是堆栈中,都​​取决于较低级别的编译器。优化器发现“ a”不会转义代码的此区域,因此它包含调试器的提示,但实际上并不需要为它进行存储,甚至不在堆栈上。它只需要获取Random的返回寄存器并将其移至print的参数寄存器。这一切取决于LLVM及其优化程序。

这一切的教训是,它实际上与性能无关紧要。在不太重要的情况下(例如a是全局的),版本1会更有效,我认为这与您的预期相反。