ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

Rust vs Swift - Speed comparison experiment that didn't go according to plan

By Vyacheslav Vorona, iOS engineer at UniFa.

You may have heard of Rust Language, said to be designed for "memory safety and speed". Being an iOS developer, I didn't pay much attention to Rust since we typically use Swift. It by design prevents unsafe behavior in terms of memory usage and is supposed to be, well... swift. However, some time ago I stumbled upon a blog post mentioning Rust as a way to:

  • Write cross-platform libraries that can be then linked to both iOS and Android applications (therefore, no need to work on the same stuff twice)
  • Delegate weighty jobs to the language that can deal with them faster

So, the second bullet point implies Rust has to be considerably "swifter" than Swift, right? I hope that makes you as curious as myself, as today we are going to compare the performance of two languages using the good old Munchausen numbers calculation and try to figure out who's the fastest here. Fasten your seatbelts (pun unintended).

Disclaimer:
I can't pretend I will do serious research on the topic within the scope of a somewhat short blog post. However, I'm trying to assess Rust's capabilities and figure out whether it is worth it to spend more time studying it or not. I hope you understand :)

Regarding the project setup

There is enough comprehensive guides on the Internet explaining how to build a Rust library and link it to an iOS project. I won't dive deep into it. However, most of those guides are a couple of years old and it seems like some steps are now causing more trouble than they used to (at least for me). So I thought I might share a tip here in case someone tries to reproduce my little project.

When you are done building your Rust library, you are supposed to link your .a file to the iOS project. All of the guides based on older versions of Xcode suggest you to go to Project->Your target->General->Linked Frameworks and Libraries and just drop your library there.

That however didn't work for me on Xcode 13.4. First of all, the whole Linked Frameworks and Libraries section got renamed to Frameworks, Libraries and Embedded Content. Second, dropping my library file there did not link it to the project.

What solved the problem in the end was adding the library to Project->Your target->Build Phases->Link Binary With Libraries.

Aside from this I have nothing to add in terms of project setup, so let's get to the fun stuff.

Munchausen Numbers

To compare the execution speed of two languages I decided to use Munchausen numbers calculation. Long story short, those are numbers that are equal to the sum of all of their digits raised to the power of themselves. For example:
3435 = 3^3 + 4^4 + 3^3 + 5^5
There is only a few of such numbers existing. However, the exact amount depends on how to treat 0^0.

  • If 0^0 is not allowed (as undefined), there are only 2 Munchausen numbers: 1 and 3435.
  • If it is assumed that 0^0 = 0, there are 4 of them: 0, 1, 3435, and 438579088.

For the sake of our project we will assume that 0^0 = 0.

Finding Munchausen numbers is a simple problem, but it takes a lot of calculations which makes it a decent way to assess the execution speed of a programming language.

Swift realization

Here is how we are searching for Munchausen numbers in Swift. Note that I am operating with Int32 to match the future Rust algorithm. A couple of notes on what is going on here: 1. Pre-caching the power for all of the digits; 0^0 is initially in the cache array
2. Searching for Munchausen numbers iterating through a long range containing all of them The calculation details:
3. Do until we go through all of the digits
4. Take the last digit of a number
5. Add the cached power of the digit to the overall sum
6. "Cut" the last digit

func swiftMunchausenNumbers() {
    // 1
    var cache: [Int32] = [0] 
    (1 ... 9).forEach { num in
        let pow = pow(Double(num), Double(num))
        cache.append(Int32(pow))
    }

    // 2
    for n in (0 ... 500000000) {
        if isMunchausenNumber(Int32(n), cache: cache) {
            print(n)
         }
     }
}

func isMunchausenNumber(_ number: Int32, cache: [Int32]) -> Bool {
    var currentNumber = number
    var sum: Int32 = 0
        
    while currentNumber > 0 {  // 3
        let digit = currentNumber % 10  // 4
        sum += cache[Int(digit)]   // 5
        if sum > number {
            return false
         }
         currentNumber /= 10  // 6
    }
        
    return number == sum
}

Rust realization

Here is the implementation of the algorithm in Rust. I am trying to keep the algorithms similar.

#[no_mangle]
pub extern fn rust_munchausen_numbers() {
    let mut cache = [0; 10];
    for n in 1..=9 {
        cache[n] = (n as i32).pow(n as u32);
    }

    for n in 0..500000000 {
        if is_munchausen_number(n, &cache) {
            println!("{}", n);
        }
    }
}

fn is_munchausen_number(number: i32, cache: &[i32; 10]) -> bool {
    let mut curren_number = number;
    let mut sum = 0;

    while curren_number > 0 {
        let digit = curren_number % 10;
        sum += cache[digit as usize];      
        if sum > number {
            return false;
        }
        curren_number /= 10;                   
    }

    number == sum
}

The code was compiled as a library and linked to a new Swift project. rust_munchausen_numbers method was exposed via the bridge header:

#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>

void rust_munchausen_numbers(void);

Execution speed measuring

To measure how long it takes for a piece of code to execute I'm using this small function in Swift:

func measureExecutionTime(of code: (() -> Void)) {
    let start = CFAbsoluteTimeGetCurrent()
    code()
    let diff = CFAbsoluteTimeGetCurrent() - start
    print("Took \(diff) seconds")
}

First, I'm launching the Swift algorithm and... waiting... waiting... making sure the program is even working and waiting a little bit more.
And finally...

Well, that took some time. But it is what we wanted to achieve, right?

Now let's try running the Rust algorithm and compare the results.

.

What?

.

Obviously, my only idea at that point is that something is wrong with the Swift algorithm. I try passing the cache array to isMunchausenNumber by reference, playing with the type casting for a little bit, and thoroughly checking the algorithm, but the execution time is surprisingly even worse than in the first attempt.

Finally, I give up and for a good measure run the Rust algorithm 100 times and get the average execution time: 4.3 seconds. Against Swift's ~450-500 seconds.

Conclusion

Rust indeed seems fast. Even if there is some mistake in my Swift algorithm that I couldn't detect, the x100 difference in execution times is too significant to blame only on a mistake.
I wish I could tell you the reason behind the strange results of my little experiment, but, unfortunately, I can't do it as of now. Anyway, the result is a result and I hope you at least found it amusing. 🍻
And I have a whole lot to learn about Rust now, I think. 🙂

Cheers,
Vyacheslav