In case you haven’t heard, the traditional c-style for
loop has been deprecated and is slated for removal in Swift 3.0. More info about that can be found here: New Features in Swift 2.2.
I’m not a fan, at all.
The fundamental reason I’m not a fan is quite simple: the only way to write a for
loop now is by leveraging abstractions. Personally, I really dislike being required to use abstractions when they are not necessary.
The defense I hear all the time is this:
Well, the compiler will close that gap or remove the abstraction cost all together.
That’s nice in theory, but it’s patently false in practice. The optimizer can remove some of the abstractions, but it cannot guarantee to remove all of the cost of the abstraction every time.
Here’s the real-world cost of abstractions (not necessarily specific to just this for-loop construct):
Language: C, Optimization: -Os Avg (ms)
---------------------------------------------------------------------------------
RenderGradient (Pointer Math) 9.582
RenderGradient (SIMD) 4.608
Language: Swift, Optimization: -O Avg (ms)
---------------------------------------------------------------------------------
RenderGradient ([Pixel]) 22.51406
RenderGradient ([UInt32]) 18.39304
RenderGradient (UnsafeMutablePointer) 20.67769
RenderGradient (UnsafeMutablePointer<UInt32>) 15.29333
RenderGradient ([Pixel].withUnsafeMutablePointer) 22.51703
RenderGradient ([UInt32].withUnsafeMutablePointer) 19.27868
RenderGradient ([UInt32].withUnsafeMutablePointer (SIMD)) 15.63351
RenderGradient ([Pixel].withUnsafeMutablePointer (SIMD)) 24.48129
Source: https://github.com/owensd/swift-perf/blob/swift-v3/reports/swift_3_0-march.txt
At best, under an optimized build, we’re looking at a 4x cost in performance. With unchecked builds, it’s possible to get the performance down to equivalent timings. With non-optimized builds, we are talking anywhere from 3 to 88 (!!) times slower than the equivalent C code.
It’s not that I don’t think that the for-in
style loop isn’t useful. I do. I also completely agree that it should be the one used the majority of the time. However, please don’t force me to use abstractions when I don’t want to or when they are not appropriate.
Here’s the before and after with the upcoming changes of some real code:
for var y = 0, height = buffer.height; y < height; ++y {
let green = min(int4(Int32(y)) &+ yoffset, 255)
for var x: Int32 = 0, width = buffer.width; x < width; x += 4 {
let blue = min(int4(x, x + 1, x + 2, x + 3) &+ xoffset, 255)
p[offset++] = Pixel(red: 0, green: green.x, blue: blue.x, alpha: 255)
p[offset++] = Pixel(red: 0, green: green.y, blue: blue.y, alpha: 255)
p[offset++] = Pixel(red: 0, green: green.z, blue: blue.z, alpha: 255)
p[offset++] = Pixel(red: 0, green: green.w, blue: blue.w, alpha: 255)
}
}
for y in 0..<buffer.height {
let green = min(int4(Int32(y)) &+ yoffset, 255)
for x in 0.stride(to: buffer.width, by: 4) {
let x32 = Int32(x)
let blue = min(int4(x32, x32 + 1, x32 + 2, x32 + 3) &+ xoffset, 255)
p[offset] = Pixel(red: 0, green: green.x, blue: blue.x, alpha: 255)
offset += 1
p[offset] = Pixel(red: 0, green: green.y, blue: blue.y, alpha: 255)
offset += 1
p[offset] = Pixel(red: 0, green: green.z, blue: blue.z, alpha: 255)
offset += 1
p[offset] = Pixel(red: 0, green: green.w, blue: blue.w, alpha: 255)
offset += 1
}
}
I personally don’t consider that a readability win.
- It’s more code.
- It requires type coercion for
x
as theInt32
type isn’t stridable. stride(to:by:)
is ambiguous compared the<
operator.
And finally, this is not an acceptable alternative in my opinion:
var y = 0
var height = buffer.height
while y < height {
var x: Int32 = 0
var width = buffer.width
while x < Int32(width) {
let x32 = Int32(x)
let blue = min(int4(x32, x32 + 1, x32 + 2, x32 + 3) &+ xoffset, 255)
p[offset] = Pixel(red: 0, green: green.x, blue: blue.x, alpha: 255)
offset += 1
p[offset] = Pixel(red: 0, green: green.y, blue: blue.y, alpha: 255)
offset += 1
p[offset] = Pixel(red: 0, green: green.z, blue: blue.z, alpha: 255)
offset += 1
p[offset] = Pixel(red: 0, green: green.w, blue: blue.w, alpha: 255)
offset += 1
x += 4
}
y += 1
}
Why you might ask?
- It’s extremely easy to forget the increment (I actually did a few momements ago).
- The iterator variables are being leaked out of scope.
- All of the loop parts (initialization, condition, and increment) are scattered throughout the construct.
- The suggested pattern of using
defer
for incrementing is fundamentally flawed:
var i = 0
while i < 10 \{
defer { i += 1 }
if i == 5 { break }
}
print(i)
What do you think i
is here? What should it be? I’ll give you a hint, they aren’t the same answer.
Yes, the above examples are narrow and specific. But that’s exactly the point. When we need to write for narrow and specific cases, that’s exactly when we need to get outside of the abstraction box that makes for simpler code.
It’s strange watching Swift evolve. Maybe I’m just dense or stuck in my old ways, but I can’t see how this change is aligned with one of Swift’s aspirations of being a systems-level language.