为什么在for-in循环中使用Swift下标语法比直接访问元素更快?

5
我看了著名的为什么处理排序数组比未排序数组更快?,并决定尝试使用其他语言如Swift进行实验。我对两个非常相似的代码片段之间的运行时间差异感到惊讶。
在Swift中,可以通过直接方式或下标方式访问数组中的元素,在for-in循环中。例如,这个代码:
for i in 0..<size {
    sum += data[i]
}

可以写成:

for element in data {
    sum += element
}

通过 size 参数来指定 data 数组中可求和的元素个数。

所以,我在 Swift 中实现了与我在第一段提到的问题相同的算法(下面是代码),让我惊讶的是第一种方法大约比第二种方法快 5 倍。
我并不真正知道后台的下标实现方式,但我认为在 Swift for-in 循环中直接访问元素只是语法糖。

问题

我的问题是两种 for-in 语法之间有什么区别,为什么使用下标会更快?
以下是计时器的详细信息。我正在使用带有 Swift 4.1 的 Xcode 9.4.1,在一台早期的 2015 MacBook Air 上运行命令行项目。
// Using Direct Element Access
Elapsed Time: 8.506288427
Sum: 1051901000

vs

// Using Subscript
Elapsed Time: 1.483967902
Sum: 1070388000
奖励问题: 为什么在Swift中执行速度比C++慢100倍 (两者在同一台Mac上在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)")

我正在使用一个已编译的应用程序(命令行项目)。 - Louis Lac
2
我怀疑你没有使用优化进行编译。使用“-O”选项,最多只会有大约10%的成本增加,而不是10倍。如果要与C++进行比较,您还需要将其与“-Ounchecked”进行比较。 - Rob Napier
好的@RobNapier,我使用-O得到了与您相同的结果。因此,总结一下,当未经优化时,第二个循环方法比使用下标要慢得多。 - Louis Lac
4
如果不进行优化,则性能测试毫无意义。默认设置是为了开发人员方便(快速编译时间,调试符号),而不是提高运行时性能。在for循环中迭代涉及多个函数调用(Sequence.makeIterator(),IteratorProtocol.next()),如果它们没有被优化掉(在-O中已经优化掉了),则会拖慢运行速度。 - Alexander
好的,我更好地理解了所有这些是如何协同工作的,谢谢。 - Louis Lac
显示剩余2条评论
1个回答

8

整体性能输出高度取决于编译器的优化。如果启用了优化来编译代码,您会发现两种解决方案之间的差异很小。

为了证明这一点,我更新了您的代码,添加了两种方法,一种使用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循环则会创建数组中每个元素的不可变副本,这会导致性能下降。


1
嗯,在运行每个方法之前,您应该将“sum”设置为“0”。我花了同样的时间来理解为什么您的两个总和不相同。 - Sulthan
哈哈,是的。我怀疑这不会对结果产生任何影响,但你是正确的,我错过了那个。 - Eneko Alonso
好的,我之前并不清楚for-in循环会创建一个不可变的副本,而下标访问只是一种访问内存的方式。这就是我想要的答案,谢谢! - Louis Lac
如果你想创建一个可变的副本,你也可以使用for var - Eneko Alonso

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接