【问题标题】:Why Swift using subscript syntax in a for-in loops is faster than using direct access to the element?为什么 Swift 在 for-in 循环中使用下标语法比使用直接访问元素更快?
【发布时间】:2018-12-09 02:34:48
【问题描述】:

我阅读了著名的Why is it faster to process a sorted array than an unsorted array?,并决定尝试使用其他语言,例如 Swift。我对 2 个非常相似的 sn-ps 代码之间的运行时间差异感到惊讶。

在 Swift 中,可以直接访问数组中的元素,也可以在 for-in 循环中使用下标访问元素。例如这段代码:

for i in 0..<size {
    sum += data[i]
}

可以写成:

for element in data {
    sum += element
}

size 具有 data 长度和 data 可求和元素数组。

所以,我刚刚在 Swift(下面的代码)中实现了与我在第一段中提到的问题相同的算法,令我惊讶的是,第一种方法比第二种方法快大约 5 倍。

我不太了解后台下标的实现,但我认为直接访问 Swift for-in 循环中的元素只是语法糖。


问题

我的问题是这两种for-in 语法有什么区别,为什么使用下标更快?

这里是计时器的详细信息。我在带有 Commande Line 项目的 2015 年初 MacBook Air 上使用 Xcode 9.4.1 和 Swift 4.1。

// Using Direct Element Access
Elapsed Time: 8.506288427
Sum: 1051901000

// Using Subscript
Elapsed Time: 1.483967902
Sum: 1070388000

额外问题:为什么 Swift 的执行速度比 C++ 慢 100 倍(两者都在同一个 Mac 上的 n Xcode 项目中执行)?例如,C++ 中 100,000 次重复所花费的时间几乎与 Swift 中 1,000 次重复所花费的时间相同。我的第一个猜测是 Swift 是一种比 C++ 更高级的语言,例如 Swift 会进行更多的安全检查。


这是我使用的 Swift 代码,我只修改了第二个嵌套循环:

import Foundation
import GameplayKit

let size = 32_768
var data = [Int]()
var sum  = 0
var rand = GKRandomDistribution(lowestValue: 0, highestValue: 255)

for _ in 0..<size {
    data.append(rand.nextInt())
}

// data.sort()

let start = DispatchTime.now()

for _ in 0..<1_000 {
    // Only the following for-in loop changes
    for i in 0..<size {
        if data[i] <= 128 {
            sum += data[i]
        }
    }
}

let stop     = DispatchTime.now()
let nanoTime = stop.uptimeNanoseconds - start.uptimeNanoseconds
let elapsed  = Double(nanoTime) / 1_000_000_000

print("Elapsed Time: \(elapsed)")
print("Sum: \(sum)")

【问题讨论】:

  • 您是在 Swift Playground 中进行测试还是在编译后的应用中进行测试?
  • 我正在使用已编译的应用程序(命令行项目)。
  • 我怀疑您没有进行优化编译。使用-O,我最多只能看到大约 10% 的成本,而不是 10 倍。如果要与 C++ 进行比较,还需要与 -Ounchecked 进行比较。
  • 除非您使用-0unchecked,否则每个基本算术运算都会执行一个分支(如果检查溢出,并崩溃而不是允许使用溢出的结果)
  • @LouisLac 性能测试毫无意义,除非您进行优化构建。默认设置是为了方便开发人员(快速编译时间、调试符号)而不是运行时性能。 for 循环中的迭代涉及多个函数调用 (Sequence.makeIterator(), IteratorProtocol.next()),如果它们没有被优化出来(它们在 -O 中是这样)会减慢速度

标签: swift performance optimization branch-prediction


【解决方案1】:

整体性能输出很大程度上取决于编译器所做的优化。如果您在启用优化的情况下编译代码,您会发现两种解决方案之间的差异很小。

为了证明这一点,我更新了您的代码,添加了两种方法,一种使用subscripting,另一种使用for-in

import Foundation
import GameplayKit

let size = 32_768
var data = [Int]()
var sum  = 0
var rand = GKRandomDistribution(lowestValue: 0, highestValue: 255)

for _ in 0..<size {
    data.append(rand.nextInt())
}

// data.sort()

func withSubscript() {
  let start = DispatchTime.now()

  for _ in 0..<1_000 {
      for i in 0..<size {
          if data[i] <= 128 {
              sum += data[i]
          }
      }
  }

  let stop    = DispatchTime.now()
  let elapsed = Double(stop.uptimeNanoseconds - start.uptimeNanoseconds) / 1_000_000_000

  print("With subscript:")
  print("- Elapsed Time: \(elapsed)")
  print("- Sum: \(sum)")
}

func withForIn() {
  let start = DispatchTime.now()

  for _ in 0..<1_000 {
      for element in data {
          if element <= 128 {
              sum += element
          }
      }
  }

  let stop    = DispatchTime.now()
  let elapsed = Double(stop.uptimeNanoseconds - start.uptimeNanoseconds) / 1_000_000_000

  print("With for-in:")
  print("- Elapsed Time: \(elapsed)")
  print("- Sum: \(sum)")
}

withSubscript()
withForIn()

我将该代码保存到一个名为 array-subscripting.swift 的文件中。

然后,我们可以从命令行运行它而无需任何优化,如下所示:

$ swift array-subscripting.swift 
With subscript:
- Elapsed Time: 0.924554249
- Sum: 1057062000
With for-in:
- Elapsed Time: 5.796038213
- Sum: 2114124000

正如您在帖子中提到的,性能存在很大差异。

当使用优化编译代码时,这种差异可以忽略不计:

$ swiftc array-subscripting.swift -O
$ ./array-subscripting 
With subscript:
- Elapsed Time: 0.110622556
- Sum: 1054578000
With for-in:
- Elapsed Time: 0.11670454
- Sum: 2109156000

如您所见,这两种解决方案都比以前快得多,而且执行时间非常相似。

回到您最初的问题,subscripting 提供对内存的直接访问,这在连续数组的情况下非常有效,其中元素彼此相邻存储在内存中。

另一方面,for-in 循环会为数组中的每个元素创建一个不可变副本,这会导致性能下降。

【讨论】:

  • ehm,你应该在运行每个方法之前将sum 设置为0。我花了同样的时间才明白为什么你的两个总和不一样。
  • 哈哈,是的。我怀疑这会对结果产生任何影响,但你是对的,我错过了。
  • 好吧,我不清楚 for-in 循环创建不可变副本,而下标只是访问内存的一种方式。那就是我想要的答案,谢谢!
  • 如果你想创建一个可变副本,你也可以使用for var
猜你喜欢
  • 1970-01-01
  • 2022-08-06
  • 1970-01-01
  • 1970-01-01
  • 2022-06-12
  • 1970-01-01
  • 2010-09-29
  • 1970-01-01
  • 2021-10-01
相关资源
最近更新 更多