Functional programming often gets criticized as being slow, so I wanted to present an example where it is in fact several times faster than the equivalent imperative code1.
The problem is to count the lengths of Collatz sequences, which you may remember from here. In Rust, this is straightforward:
fn modular(a: i64, b: i64) -> i64 {
((a % b) + b) % b
}
pub fn collatz_length(mut i: i64) -> i64 {
let mut l = 1;
while i != 1 {
i = match modular(i, 2) {
0 => i / 2,
_ => 3 * i + 1,
};
l += 1;
}
l
}
This is a thoroughly imperative style - we use a mutable variable to track
state, and we use a while
loop rather than recursion. Our C code will be
similarly simple:
int collatz_c(int n) {
int l = 0;
while (n != 1) {
if (n % 2 == 0)
n = n / 2;
else
n = 3 * n + 1;
l++;
}
return l;
}
The implementation in ATS is notably less straightforward (you may find this section of the manual to be helpful):
#include "share/atspre_staload.hats"
fun collatz_length {n : nat} (n : int(n)) : int =
let
fnx loop {n : nat}{l : addr} (pf : !int @ l | n : int(n), res : ptr(l)) : void =
if n > 1 then
let
val () = !res := 1 + !res
in
if n mod 2 = 0 then
loop(pf | n / 2, res)
else
loop(pf | 3 * n + 1, res)
end
var res: int with pf = 1
val () = loop(pf | n, addr@res)
in
res
end
Here, we do something that is not possible to do in C - we safely stack-allocate a function argument. In addition, we use recursion, allowing us to model the actual problem2.
In practice, this approach leads to dramatically faster code - our ATS code is 1.5 to 2.8 times faster than the equivalent Rust.
Input | Language | Time |
---|---|---|
2223 | ATS | 132.6 ns |
2223 | C | 201.7 ns |
2223 | Rust | 202 ns |
10971 | ATS | 193.1 ns |
10971 | C | 298.7 ns |
10971 | Rust | 400 ns |
106239 | ATS | 253.5 ns |
106239 | C | 399.2 ns |
106239 | Rust | 738 ns |
While this particular example may be simple-minded, I think it is nonetheless a nice demonstration that functional programming can indeed be faster than imperative.
1: You can find the full benchmarking setup here.2: In fact, had we write a more conventional implementation using pattern matching and recursion, we'd get about the same performance as Rust without the painful compromise of imperative programming.