for, forEach, and map (oh my)

We interrupt the recent political ramblings and assorted existential crises to bring you a post about loops in Swift.

I was tempted to use forEach today but then I started wondering what sort of overhead closures add. So I cobbled together something resembling a test. Staring with an array of ints:

var loopIndices = [Int](0..<20000)>

I used these to create characters which are appended to a string. The “classic” for loop looks like:

for ch in self.loopIndices { self.loopStr.append(UnicodeScalar(ch)?.escaped(asASCII: false) ?? "X") }

The forEach version looks like:

self.loopIndices.forEach { self.loopStr.append(UnicodeScalar($0)?.escaped(asASCII: false) ?? "X") }

I ran this test 100 times on my AppleTV and the total time for a regular for loop was 1.8076 seconds. The time in the forEach loop? 3.6851 seconds. So basically twice as long. For good measure I made a map version as well that looks like:

self.loopStr ={ UnicodeScalar($0)?.escaped(asASCII: false) ?? "X" }).joined()

The map has to join as well so it’s not surprising that it’s basically the sum of the other two tests coming in at 5.3892 seconds. Let me summarize in this handy table:

Type Duration Comments
Basic for loop 1.8076 seconds
forEach loop 3.6851 seconds Ew
map 5.3892 seconds Super ew

I thought, since the forEach closures are not @escaping, that Swift might be able to do some trickery since it shouldn’t need to capture values, worry about memory management, etc. So on a hunch I tried the same test with release settings (basically -O -whole-module-optimization instead of -Onone) and when we do that we get:

Type Duration Comments
Basic for loop 1.5675 seconds
forEach loop 1.6468 seconds Better! Only 5% slower, but then…
map 1.5958 seconds What sorcery is this?!

This got me wondering what map looks like without the added join so instead of appending to a String we have:

var loopStrings : [String]

And running this with optimization on yields:

Type Duration Comments
Basic for loop 0.4478 seconds
forEach loop 0.5814 seconds Overhead more visible now. 30% slower
map 0.3168 seconds Maybe it make better guesses about memory allocation?

In general appending to an Array is faster than appending Strings. No surprise there, but look how much better map is. Okay, so maybe we eliminate the need to muck about with memory in our tests and see what happens. We'll take the same array of Ints but add them up or something. Hmm, but map isn't great for that sort of thing, we'll reduce instead. We'll also double the number of items in our array of ints, but this is going to be really fast and our profiling numbers may be down in the noise floor. Whatever, the original question was how much overhead do closures add? So our map is now:

self.loopTotal = self.loopIndices.reduce(0) { $0 + $1 }

And the other loops have been modified to produce the same results. In this case our times are:

Type Duration Comments
Basic for loop 0.0070 seconds
forEach loop 0.0103 seconds Not quite 50% slower
reduce 0.0093 seconds Still better than forEach

With most of the actual work stripped away we can see the closure overhead more clearly. Although it's not a constant value or it would have been at least the difference in time from the previous example (0.1336 seconds). In this case, the difference works out to an extra 3300┬ÁS over 4 million iterations. Woo. So if forEach results in code that you believe is clearer you're probably not going to notice the performance hit. But it will be there so it's probably something you want to avoid if you're looking to conserve every cycle.