After reading this post by @Karl, the question popped in my mind, and I decided to find out.
Given:
// Let U be some type
func f (_ u: U) {...}
let uv: [U] = ...
let M = uv.count
Which one of these loops is more efficient?
// (1)
for u in uv {
f (u)
}
// (2)
for i in 0..<M {
f (uv[i])
}
// (3)
var i: Int = 0
while i < M {
f (uv[i])
i += 1
}
Measurements
// Built with Xcode 16.0, optimize for Speed [-O]
// On a MacMini 3.2 GHz 6-Core Intel Core i7
T0() trigger lazy initialization of uv...
initialize uv...
M = 1000000 T0 took 0.112677642 seconds // Trigger lazy init of uv
M = 1000000 T1 took 0.000593867 seconds // for loop - for u in uv {
M = 1000000 T2 took 0.000543171 seconds // for loop - for i in 0..<M {
M = 1000000 T3 took 0.000509566 seconds // while loop
M = 1000000 T4 took 0.00048985 seconds // reduce
M = 1000000 T5 took 0.000427039 seconds // while loop inside withUnsafeBufferPointer
Code
import Foundation
@main
enum ForOrWhileLoop {
static func main () {
print (type(of: Self.self), "...")
measure (prefix: "T0", f: T0)
measure (prefix: "T1", f: T1)
measure (prefix: "T2", f: T2)
measure (prefix: "T3", f: T3)
measure (prefix: "T4", f: T4)
measure (prefix: "T5", f: T5)
}
}
let M = 1_000_000
let uv: [Int] = .init (unsafeUninitializedCapacity: M) { buffer, initializedCount in
print ("initialize uv...")
for i in 0..<M {
buffer [i] = Int.random(in: 0..<1024)
}
initializedCount = M
}
func measure (prefix: String, f: () -> Int) {
let c = ContinuousClock ()
let d = c.measure {
_ = f ()
}
print ("M = \(M)", prefix, "took", d)
}
func T0 () -> Int {
print (#function, "trigger lazy initialization of uv...")
var sum: Int = 0
for u in uv {
sum += u
}
return sum
}
func T1 () -> Int {
var sum: Int = 0
for u in uv {
sum += u
}
return sum
}
func T2 () -> Int {
var sum: Int = 0
for i in 0..<M {
sum += uv [i]
}
return sum
}
func T3 () -> Int {
var sum: Int = 0
var i: Int = 0
while i < M {
sum += uv [i]
i += 1
}
return sum
}
func T4 () -> Int {
let sum = uv.reduce (0) { partialResult, u in
partialResult + u
}
return sum
}
func T5 () -> Int {
var sum: Int = 0
uv.withUnsafeBufferPointer {
var i: Int = 0
while i < M {
sum += $0 [i]
i += 1
}
}
return sum
}
Looks like the good, old while loop
is the winner.
Edit: My initial observation was incorrect as I wasn't aware of the effect of the lazy initialization of the global variable uv
. See @ahti's post below. I have modified the test code and the results. Looks like there isn't a big difference after all. @jrose, @Alejandro, @Karl, @asdf_bro, @David_Smith. Thank you all.