Swift - Functional Programming vs Performance
DISCLAIMER: This won't be a deep and boring article about computer science, algorithm optimization, and related topics, LOL, but just a bit of talk about something I found interesting as an iOS Developer, so I decided to run some experiments on my own.
Introduction
As many of you might know, I didn't have a developer career the whole time (just some years in the beginning, right after university, and now in the last 5 years), but I always liked to read and discuss algorithms and data structure topics although I'm not the most experienced guy to talk about it.
Since I returned to the development path after the 13-year gap, one of the most noticeable things I saw was: functional programming everywhere!
Developers using lambdas, maps, filters, etc., in most of the languages I have in touch: Swift, Kotlin, JavaScript, etc. And I can't deny that the code becomes clean and beautiful in a certain way.
Because it seemed to be a common practice, I had the (false) assumption that the compilers might be doing some optimization under the hood, because I always questioned myself that all of these iterations over iterations should have some price, and still everyone don't seem to care much about it.
Therefore, after a talk with a fellow Android developer in the company about a good solution to extract deeply nested lists from an API response and compute to a single list, we've come up with a functional solution that led to the same discussion about possible performance issues and that's why I decided to run some experiments.
Study Case
For this article, I won't discuss my company's problem above (for obvious reasons, lol) but I'll propose an equivalent problem (and even simpler). Let's suppose we're interested in a simple algorithm to find the sum of all positive numbers below a given number where they are multiple of 3 or 5.
Example: If the given number is 10, then the expected sum should be (3 + 5 + 6 + 9 = 23)
Because I'm an iOS developer now, let's write a first approach in Swift using traditional programming with for loops, conditionals, and mod operations:
func compute1(_ x: Int) -> Int {
var sum = 0
for i in 1..<x {
if i % 3 == 0 || i % 5 == 0 {
sum += i
}
}
return sum
}
Ugly, huh? But let's see if it works at least...
compute1(10)
// Console: 23
Good. Now let's try to "beautify" it:
领英推荐
func compute2(_ x: Int) -> Int {
return (1..<x).reduce(0, {
($1 % 3 == 0 || $1 % 5 == 0) ? $0 + $1 : $0 + 0
})
}
compute2(10)
// Console: 23
Good, still working. Here I'm using a fancy reduce to sum all numbers multiple of 3 or 5 from a range of 1 to the given number.
Let's improve it more. What about adding a filter to reduce the initial list to only multiples of 3 and 5 and then using a simple reduce to sum them all? Let's also replace the ugly mod operation (x % y == 0) with a fancy method from the standard library:
func compute3(_ x: Int) -> Int {
return (1..<x)
.filter { $0.isMultiple(of: 3) || $0.isMultiple(of: 5) }
.reduce(0, +)
}
compute3(10)
// Console: 23
Hell yeah!! Now we reached the state-of-the-art of cleanness and readability. Uncle Bob would be proud!!
Benchmarks
Now we need to compare the approaches. For it, I'm just thinking about measuring the duration of each call, multiple times, and extracting the average duration (to be fair enough, since executions vary according to the PC mood).
We can use a method like that:
func benchmark(_ repetitions: Int, _ work: () -> Void) {
let clock = ContinuousClock()
var sum: Duration = .zero
(1...repetitions).forEach { _ in
sum += clock.measure(work)
}
let average = sum / Int64(repetitions)
print("Avg: \(average)")
}
Let's try with 10 repetitions and 100.000 as the given number to increase the complexity and duration:
benchmark(10) { _ = compute1(100000) }
benchmark(10) { _ = compute2(100000) }
benchmark(10) { _ = compute3(100000) }
That's the console output:
Avg: 0.8118450542 seconds
Avg: 1.7683034834 seconds
Avg: 1.7809556166 seconds
As you can see both functional approaches performed more than 2x worse than the traditional approach. And the "cleaner" one was the big loser... ??
Final Thoughts
As I mentioned in the disclaimer at the beginning, this article wasn't meant to do a deep dive into algorithms, and I'm not here to disqualify or talk bad things about functional programming. But I would like you to consider these things when writing code and encourage you to study and understand why these things happen.
Unfortunately, we've been talking a lot about developers that will be replaced by IA, and I would say that the only way to avoid it is to become smarter. Don't restrict yourself to just mimic youtubers (that love to say "don't do this", stop doing that", etc, lol), and copy/paste their code. Try to understand how things work under the hood. Be a real problem solver.