josevalim

josevalim

Creator of Elixir

NimbleParsec - a simple and fast parser combinator for Elixir

Yes, yet another parser combinator library!

Most of the parser combinators in the ecosystem are either compile-time, often using AST traversal and macros, which hurts composition, or are runtime based, which means it is slow when parsing. But more importantly, I haven’t found no library compiles parser combinator down to binary matches that rely on the VM optimizations.

So over the last 48h I built a yet another parsec combinator library for Elixir called NimbleParsec. The combinator composition happens fully at runtime which is then compiled down to binary matching. It works similar to quoted expressions in Elixir. The combinators build an AST which is then compiled down to binary match clauses. See the link above for an example and the code it compiles down to.

I have ran @OvermindDL1 benchmark scripts and I got these results:

$ mix bench
Erlang/OTP 20 [erts-9.0] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
Elixir 1.7.0-dev
Benchmark suite executing with the following configuration:
warmup: 2.0s
time: 3.0s
parallel: 1
inputs: parse_datetime, parse_int_10
Estimated total run time: 30.0s


Benchmarking with input parse_datetime:
Benchmarking combine...
Benchmarking ex_spirit...
Benchmarking nimble...
Warning: The function you are trying to benchmark is super fast, making measures more unreliable! See: https://github.com/PragTob/benchee/wiki/Benchee-Warnings#fast-execution-warning


Benchmarking with input parse_int_10:
Benchmarking combine...
Benchmarking ex_spirit...
Benchmarking nimble...
Warning: The function you are trying to benchmark is super fast, making measures more unreliable! See: https://github.com/PragTob/benchee/wiki/Benchee-Warnings#fast-execution-warning


##### With input parse_datetime #####
Name                ips        average  deviation         median
nimble        1425.75 K        0.70 μs   ±437.03%        0.70 μs
ex_spirit      177.70 K        5.63 μs   ±115.86%        5.00 μs
combine         95.83 K       10.44 μs    ±93.60%        9.00 μs

Comparison:
nimble        1425.75 K
ex_spirit      177.70 K - 8.02x slower
combine         95.83 K - 14.88x slower

##### With input parse_int_10 #####
Name                ips        average  deviation         median
nimble         949.95 K        1.05 μs   ±216.77%        1.00 μs
ex_spirit      463.71 K        2.16 μs   ±950.25%        2.00 μs
combine        338.62 K        2.95 μs   ±760.53%        2.00 μs

Comparison:
nimble         949.95 K
ex_spirit      463.71 K - 2.05x slower
combine        338.62 K - 2.81x slower

The above shows that for parsing datetimes, nimble is 8x faster than ex_spirit and 14x faster than combine. For the integer case, nimble is only twice faster, but it is worth noting Nimble’s integer parser is written on top of existing combinators while the integer parser for both ex_spirit and combine are written by hand. So nimble is beating hand-written code there.

I did not measure memory usage but that should also decrease wth nimble thanks to binary matching.

I have also benchmarked compilation times by compiling the same datetime parser 30 times. combine takes 1s, which makes sense as it is runtime based. nimble takes 2s and ex_spirit takes 6s.

While nimble is extremely new, I think most of the primitives are there, so you should be able to build almost anything. Improvements, PRs and feedback are very welcome, thanks!

159 19229 141

Most Liked

tmbb

tmbb

I’m the author of Makeup (a library for syntax highlighting of source code) and ExDocMakeup (a markdown processor that can be used with ExDoc that uses Makeup for syntax highlighting of code examples in the docs). It was a discussion around Makeup that initially prompted the development of NimbleParsec. Up until the most recent version, Makeup used to depend on the ExSpirit library by @OvermindDL1, which is much more flexible than NimbleParsec (as the discussion above shows), but much, much slower.

Using Benchee, I’ve prepared some benchmarks pitting the current version of Makeup (0.4.0) against the old version. I’ve gotten aproximately 10x speedups in both compilation time and runtime.

Here’s how to interpret the benchmarks below:

  • Formatter performance: the part that takes up a list of tokens and converts it into an HTML fragment
  • Lexer: the part that reads the raw source code and produces a list of tokens
  • Lexer compilation time: this was done by invoking the Elixir compiler at runtime on the relevant file. This is a good proxy measure of the “real” compilation process (it’s pretty cool that’s so easy to compile code at runtime in Elixir - never us this in production, though!)

The most important number is the “Lexer + Formatter”, because that’s what the user will do most of the time. The benchmarks use a fake elixir file with about 250 lines written by the authors of the python library Pygments (which is similar to makeup) to demonstrate most of the syntax rules.

ExSpirit (old version):

Name                                                              ips        average  deviation         median         99th %
Formatter performance                                          124.40        8.04 ms    ±97.25%          15 ms          16 ms
Reading file from disk + Lexer + Formatter (end to end)          7.29      137.24 ms     ±7.99%         140 ms         172 ms
Lexer performance                                                7.18      139.33 ms     ±9.72%         140 ms         157 ms
Lexer + Formatter                                                6.83      146.43 ms    ±10.56%         141 ms         187 ms
Lexer compilation time                                         0.0355       28156 ms     ±0.00%       28156 ms       28156 ms

NimbleParsec (new version):

Name                                                              ips        average  deviation         median         99th %
Formatter performance                                          620.02        1.61 ms    ±32.96%        1.60 ms        3.20 ms
Lexer performance                                              108.31        9.23 ms     ±4.86%        9.40 ms        9.40 ms
Lexer + Formatter                                               94.72       10.56 ms    ±69.37%          15 ms          16 ms
Reading file from disk + Lexer + Formatter (end to end)         92.32       10.83 ms    ±66.61%          15 ms          16 ms
Lexer compilation time                                           0.26     3812.50 ms     ±0.01%     3812.50 ms        3813 ms

The secret to the Formatter’s performance improvements was to use iolists whenever possible instead of binaries after a suggestion by @josevalim I’d never have thought that the difference would be so great. It turns out that concatenating strings on the BEAM is very slow and requires lots of copying. @josevalim has been extremely helpful in suggesting improvements to the lexer, answering questions about NimbleParsec and giving useful tips on how to increase performance of my library.

Performance improvements in the lexer were mainly due to the use of NimbleParsec (and to an obsessive amount of microbenchmaking). The performance of my lexer is now pretty much the same as the performance of the Elixir formatter, which means there are probably very little gains to be had. Makeup is quite similar to the Elixir formatter in that it parses files and prints a beautified version of the output. It was also written by people which are probably smarter than me and which have some actual knowledge of the internals of the BEAM.

This newest version of Makeup itself is not yet ready for prime time. You can help by running it on examples of real elixir code, inspecting the output and submitting an issue with examples tha look wrong. Such examples can be included in the test suite, which sadly doesn’t cover all syntax rules yet (and may never cover all rule combinations, that’s why it’s important to run it on real code to spot issues).

All of this will be easier after I’ve updated the docs with more useful information.

From my experience in the last few days, NimbleParsec seems to be ready to be used for real projects. It’s the ideal option for those who need to parse context-free languages, and with some postprocessing steps it can be used to help parse some context-sensitive ones. I don’t believe I’ve found any bug while using it for Makeup.

Thanks to @josevalim for writing this great library and for all the help and to @OvermindDL1 who’s written ExSpirit, without which Makeup would have never been written. Although quite slow, ExSpirit is way faster than almost anything out there and is still the only Elixir parser library that supports parsing context-sensitive languages.

If you’ll only take away one thing from this post, take this: string concatenation is slow. Iolists are the secret sauce that will make your programs go fast. Don’t replace your code without benchmarking it, though. Performance on the BEAM is weird, don’t trust your instincts from other languages.

mischov

mischov

defmodule NimbleParsec do
  @moduledoc "README.md"
             |> File.read!()
             |> String.split("<!-- MDOC !-->")
             |> Enum.fetch!(1)

Where has this been my whole life?

OvermindDL1

OvermindDL1

+++

Yeah definitely should be outputting IOlists, even with ExSpirit. ^.^;

I’m really curious about when it will be able to parse context-sensitive languages, then it can fully replace ExSpirit and I can deprecate it (less stuff for me to manage/update, the better!). :slight_smile:

Heh thanks, it was birthed because I needed it and nothing else out supported the features I needed. ^.^

I really want NimbleParsec to replace it, needs a few more features to be able to do that though. :slight_smile:

Where Next?

Popular in Announcing Top

OvermindDL1
Been making an MLElixir thing (not released yet…) for fun in spare time in the past day. I’m just trying to see how much I can get an ML...
132 13966 106
New
asiniy
Hey there! I wrote a download elixir package which does exactly what its name about - an easy way to download files. I saw solutions ab...
New
cjen07
parameterized pipe in elixir: |n&gt; edit: negative index in |n&gt; and mixed usage with |&gt; are supported example: use ParamPipe ...
New
hpopp
After just over two years in development, this latest version of Pigeon is what I finally consider done in regards to my original vision ...
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 29377 241
New
archan937
It is a well-know topic within the Elixir community: “To mock or not to mock? :)” Every alchemist probably has his / her own opinion con...
New
RobertDober
Earmark is a pure-Elixir Markdown converter. It is intended to be used as a library (just call Earmark.as_html), but can also be used as...
239 12560 134
New
tmbb
PhoenixWS - Websockets over Phoenix Channels Source code on Github here: GitHub - tmbb/phoenix_ws: Websockets implemented over Phoenix Ch...
New
New
fuelen
Hey folks! Want to present a toolkit for writing command-line user interfaces. It provides a convenient interface for colorizing text...
New

Other popular topics Top

lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30877 112
New
sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New

We're in Beta

About us Mission Statement