Debug anyone?
In the previous blog post (Map vs for in vs forEach challenge in Swift βοΈ and To if let or to not nil? π) I have tested some methods using unit tests and function measure()
from XCTest
framework. But are these results also legit for the production? Letβs take a look that loop
challenge and test it using different building configurations.
From tests to the code in debug mode
We can implement measure()
method by ourself using code like that:
func measure(attempts: Int, measuredClosure: (() -> ())) -> ([TimeInterval], TimeInterval) {
var durationsInSeconds = [TimeInterval]()
for _ in 1...attempts {
let startDate = Date()
measuredClosure()
durationsInSeconds.append(Date().timeIntervalSince(startDate))
}
let averageInSeconds = durationsInSeconds.reduce(0, +) / Double(durationsInSeconds.count)
return (durationsInSeconds, averageInSeconds)
}
It takes the number of attempts and code to measure, it returns the tuple which contains all the results and the average.
Letβs test for in
approach:
var numbers = Array(1...arrayLength)
let (_, averageEmptyForIn) = measure(attempts: measureAttempts) {
for _ in numbers {}
}
print("averageEmptyForIn: \(averageEmptyForIn)")
Now it is time to compile it and run.
$ swift build
$ .build/debug/Comparer
averageEmptyForIn: 1.48660808801651
OK. It works, we have the results now it is time to rest⦠but suddenly someone asks:
βHave you checked it on production?β
From the code in debug mode to release
Going from debug
mode to release
mode is simple by adding one more flag to the swift build
command. It is time to build it and execute.
$ swift build --configuration release
$ .build/release/Comparer
averageEmptyForIn: 0
What? 0
? What is going on?
Magic in the release mode
In the release
mode, the compiler uses optimization mechanism and the final performance of the application should increased. The compilation time can be longer but the final results should be better than in the debug
mode. The compiler noticed that this for in loop
is empty, it is pointless to execute it at all so it skips it completely during building and the results is 0
.
To measure this kind of stuff we need to put something more robust between braces. I will use a new random mechanism Int.random(in: 1...2)
included in Swift 4.2
. It generates a random number from inserted range so the output should be the number 1
or 2
.
let (_, averageForInSeconds) = measure(attempts: measureAttempts) {
for i in numbers {
_ = i + Int.random(in: 1...2)
}
}
Now this sentence can be added for every previous loop
approaches, compiled using specific configuration and launched. The repository with the improved code can be found here.
Test environment
- Device: MacBook Pro (15-inch, 2015, 2.2GHz Intel Core i7, 16GB RAM)
- OS: macOS High Sierra 10.13.6
- Swift: 4.2 (Xcode 10 Beta 5)
- Array length: 10,000,000
Test results
Tests
Variant | Average duration |
---|
map() | 5.215 sec |
for in | 5.921 sec |
forEach() | 6.083 sec |
Debug mode
Variant | Average duration |
---|
map() | 5.164 sec |
for in | 5.908 sec |
forEach() | 5.929 sec |
Release mode
Variant | Average duration |
---|
map() | 2.189 sec |
for in | 2.187 sec |
forEach() | 2.180 sec |
Conclusions
- The performance of the code compiled in the
debug
mode and the results of executed tests
are similar (both complied using the same configuration). - In the
release
mode there is no significant impact which loop
approach is used. The results are quite the same. - Code compiled in the
release
mode is executed faster than in debug
mode. It should be obvious but who knows, maybe there are some edge cases. π - Tests are complied in
debug
mode by default. The results of the tests can be used as indicator but the production performance could be different.