The Return of: Can Rust speed up your Azure Functions?
More Rust on Azure Functions
Interaction between technology is great! While giving developers from either language the chance to tap into a potentially larger and - in other areas maybe? - useful technology. These interactions have to be explored to know their limits and where they excel.
Explore
A couple of weeks back I started using WASM on Azure’s FaaS (serverless/Functions as a Services) and unexpectedly the Rust version of my simple Monte Carlo estimation was considerably slower than its JavaScript counterpart. In a picture:
Clearly this needs more exploring! This time around the challenges will be harder and more practical. Let’s see how WASM and JavaScript perform.
On inter-language benchmarks
Languages and technologies are often vastly different in their implementation, so comparing them often makes no sense. Benchmarking is hard, which is why the numbers published here are not universal truths - they can only act as guidance on where to look for potential alternative solutions.
In my opinion the timings in this (and the previous) blog post indicate the current state of web assembly rather than provide ground truths for the use cases presented. For each solution I recommend doing your own benchmarking to see where your implementation stands.
Onwards
Since the last blog post’s outcome was unexpected, I had to do some more digging on WASM. It has been suggested that the issue for the massive slowdown is due to two things:
- Cross-barrier calling
Math.random()
from Rust into JavaScript - The proposed speed ups typically result from smaller files (therefore quicker downloads) and that WASM files are already pre-compiled
The second suggestion would effectively eliminate any advantages of WASM on Node (and FaaS platforms) since there is usually no download and no JIT compilation required.
Let’s check and see if we can make Rust + WASM faster on Node!
The State Of Randomness And Numbers
During my testing I came across several limitiations of WASM, the biggest one being the inability to use a good random generator. So far, the rand
crate in its latest release (0.5.2) has added WASM support - but it uses custom JavaScript, which isn’t supported by wasm_bindgen yet. This means that a cryptography-safe random number generator is not available in Rust for WASM, and for now it’s best to stick to PRNGs, for example rand::prng
.
Additionally, WASM to JavaScript does not use 64 bit integers unfortunately, which limits the parameter choice for the Fibonacci number considerably. 43 yields the highest signed 32 bit number.
Simulating Pi … Again
As an update to the previous version, I implemented a simple Wichmann Hill pseudo-random number generator, which should eliminate the problem of calling cross-boundary (from Rust into JS) and levels the playing field as well since JavaScript and Rust both can use the same type of generator. Here’s the implementation I chose:
const S1_MOD: f32 = 30269f32;
const S2_MOD: f32 = 30307f32;
const S3_MOD: f32 = 30323f32;
pub struct WichmannHillRng {
s1: f32,
s2: f32,
s3: f32
}
impl WichmannHillRng {
fn new(s1: f32, s2: f32, s3: f32) -> WichmannHillRng {
// TODO: check if 1< s1,s2,s3 > 30_000
WichmannHillRng {
s1: s1,
s2: s2,
s3: s3
}
}
pub fn seeded(seed: u32) -> WichmannHillRng {
let t = seed;
let s1 = (t % 29999) as f32;
let s2 = (t % 29347) as f32;
let s3 = (t % 29097) as f32;
WichmannHillRng::new(s1, s2, s3)
}
pub fn next_f32(&mut self) -> f32 {
self.s1 = (171f32 * self.s1) % S1_MOD;
self.s2 = (172f32 * self.s2) % S2_MOD;
self.s3 = (170f32 * self.s3) % S3_MOD;
(self.s1 / S1_MOD + self.s2 / S2_MOD + self.s3 / S3_MOD) % 1f32
}
}
The same code in JavaScript (please disregard the “architecture”):
let s1 = 23415;
let s2 = 23749;
let s3 = 1633;
const S1_MOD = 30269;
const S2_MOD = 30307;
const S3_MOD = 30323;
function WH() {
s1 = (171 * s1) % S1_MOD;
s2 = (172 * s2) % S2_MOD;
s3 = (170 * s3) % S3_MOD;
return (s1 / S1_MOD + s2 / S2_MOD + s3 / S3_MOD) % 1.0
}
Local Tests
Again the simulation is run with 10 000 000 iterations and the results are … devastating:
The same result in numbers:
rs | js | |
---|---|---|
count | 20 | 20 |
mean | 1.688500 | 0.388000 |
std | 0.046143 | 0.013992 |
min | 1.660000 | 0.380000 |
25% | 1.670000 | 0.380000 |
50% | 1.670000 | 0.380000 |
75% | 1.690000 | 0.390000 |
max | 1.850000 | 0.430000 |
Curiously the Rust implementation is very slow. This shows that calling across the language barrier is not at all expensive, or it wouldn’t be roughly 3x slower with a simple PRNG! Hence importing and invoking Math.random()
in Rust is the way to go for random numbers at the moment (as mentioned above).
Fibonacci!
Next up is a venture into recursion - a recursive Fibonacci number generator. This very simple algorithm has a terrible runtime complexity (O(2^n)), so please don’t use it in real life 😁. As stated before, the Fibonacci number of 43 is the largest signed 32 bit integer, which is all WASM can handle right now.
Here are both implementations, first Rust …
pub fn fib(n: u32) -> u32 {
match n {
1 | 2 => 1,
_ => fib(n - 1) + fib(n - 2)
}
}
… then JavaScript:
function fib(n) {
if (n === 1) return 1;
if (n === 2) return 1;
return fib(n - 1) + fib(n - 2);
}
Local Tests
Calculating the Fibonacci number of 43 isn’t the most challenging task, but it seems to take a lot longer on JavaScript! Rust and WASM seem to excel in this scenario!
The numbers for this chart are similarly impressive.
rs | js | |
---|---|---|
count | 20 | 20 |
mean | 1.16050 | 2.917000 |
std | 0.02982 | 0.072555 |
min | 1.10000 | 2.720000 |
25% | 1.14000 | 2.887500 |
50% | 1.15000 | 2.950000 |
75% | 1.19000 | 2.960000 |
max | 1.21000 | 3.010000 |
Moving to the cloud: Azure Functions
The repository of the code is actually wired up as a CI source for Azure Functions, so this code is deployed with every push 😀. I’ll leave the Functions deployed for a while, so try them out yourself at:
Remote tests are again executed as URL-based load test from VSTS. To avoid scaling artifacts of the functions, the parameters are kept at one concurrent user issuing requests for one minute. Let’s look at the performance to see whether it mimics what we have seen locally:
JavaScript
Rust & WebAssembly
Yes, they are fairly similar to local results. Their absolute execution times are of course different since the hardware and Node version are different:
- The JavaScript “pi” test is about 3x as fast (vs 4x locally)
- The Rust “fib” test is about 3x as fast (vs ~2.5x locally)
These are great results and definitely show that it’s useful to have WASM available in the cloud 😀.
Time for a practical thing
These experiments show that it’s not as straightforward as it might seem. WASM enables developers to use their favorite language wherever JavaScript works, yet it comes at a price. If you are thinking of adopting WASM in a Node application, it’s highly recommended to benchmark any performance critical parts!
Personally I will continue this journey and explore a more practical use case here, check back soon! Until then, I recommend checking out the Github repository!
Never miss any of my posts and follow me on Twitter, or even better, add my RSS feed to your reader!