That seems very surprising to me. Is this a common result? I've heard that the BEAM is not the best for "number crunching" code, is this one of those scenarios? Is this really just the raw performance of the languages, or a difference in algorithms? There's also no mention of the version of Elixir and the OTP. Did the JIT change anything?
Based on performance numbers I've seen, I'd expect a one-order-of-magnitude difference to be a more common case in general.
The BEAM VM is amazing at many things but it is still a VM and while the BeamAsm Just In Time compiler added in 2020 can offer performance improvement gains of 130%, there are areas where the BEAM still won't outperform native code.
Erlang excels at network programming and binary data processing. Computation intensive math and heavy string processing are both good NIF use cases.
An article on string performance [1],pre-JIT, describes initial Elixir benchmark of 140s vs C's 3.74s. They managed to make a number of tradeoffs and get the Elixir benchmark improved to 13s. Part of how they did that was to not use unicode (IO.binstream instead of IO.stream), that alone gained them ~4s.
[1] https://blog.jola.dev/elixir-string-processing-optimization
Back when I was writing Elixir, it's what I used to process Markdown and it was also substantially faster than the native Elixir Markdown library (Earmark).
In this case the code reuse was more important than pure native speed. We already had a Rust library that used pulldown-cmark [1] with some custom tweaks that we wanted to duplicate. Maybe this behavior could have been copied using cmark.ex too (we thought about doing this in pure Elixir, as mentioned in the post), but given how straightforward Rustler made integrating our existing code, this seems like the better choice.
After Rust, giving up a bit of performance to not have to maintain the C code underneath seems preferred. And often you don't even sacrifice performance.
Yeah no doubt about it, although in this case the C implementation has been a long running project that's under the official commonmark GitHub repo at https://github.com/commonmark/cmark.
But I think the most important thing here is an Elixir NIF already exists to use it. The blog post as is leaves readers having to implement ~100 lines of Elixir code to use the Rust version because the author of the blog post didn't include that code in the article, or open source it as a library for others to use.
From a reader's POV if your goal is to get a highly stable, fast and safe Markdown parser running in Elixir, the Elixir cmark library I linked in a parent comment solves that problem out of the box.