srowley
List traversal vs. recursive binary pattern-matching
I was working on the Luhn algorithm exercise on Exercism and took two approaches to the problem. Then I benchmarked them. I am curious to hear about which approach you would assume performed better.
As background (simplifying a bit), the algorithm involves:
- Starting with a string of presumably digits and spaces
- Strip the spaces
- Perform a calculation on each number (which calculation depends on position in the stripped string)
- Sum up the transformed numbers
- Take the remainder of the sum divided by 10
My first approach was to strip the spaces, convert the number to a charlist then a list of integers, and traverse the list of digits to do the transformation, then Enum.sum/1 and divide.
My second approach was to strip the spaces, and recursively process the string by binary pattern-matching on the first character, performing the applicable calculation and accumulating a sum, then divide.
Which of these approaches would you expect to perform better in a simple Benchee-style test?
I am curious to hear what people think; if anyone is interested enough to respond with their reasoned take I’ll post the results (and the code so you can tell me what I did wrong either in the slower case or the benchmarking).
Most Liked
Eiji
I believe that second one:
However your description is not enough clear for me …
- Is stripping spaces the first thing you do?
- How do you then fetch index?
- Do you use something like
String.split/2?
In my opinion not only index should be incremented on recursive pattern-matching, but also the calculation could be done in exactly same step.
This is what I have written in few minutes …
defmodule Example do
def sample(string), do: string |> parse(0, [[]]) |> Enum.sum() |> Kernel.rem(10)
defp parse("", _index, [[] | acc]), do: acc
defp parse("", index, [last | tail]), do: [get_value(last, index) | tail]
defp parse(<<" ", rest::binary>>, 0, [[]]), do: parse(rest, 0, [[]])
defp parse(<<" ", rest::binary>>, index, [last | tail]) do
parse(rest, index + 1, [[], get_value(last, index) | tail])
end
defp parse(<<digit::utf8, rest::binary>>, index, [last | tail]) when digit in ?0..?9 do
parse(rest, index, [[digit | last] | tail])
end
defp get_value(list, index), do: list |> Enum.reverse() |> List.to_integer() |> calculate(index)
defp calculate(integer, index), do: integer * index
end
In this code we have such 3 steps:
- Parsing - just simple recursive iteration
At end ofstringand/or at anywhitespace(except whitespaces at start) we need to reverse list (because of previously used[head | tail]optimization), convert it to integer and do some calculation based on the incremented (by anywhitespacesexcept those at start)index. - After that we are calling
Enum.sum/1 - And finally calculatiing
remainderof division by10
srowley
Thanks for participating! I agree. There are some nuances to the algorithm but my second approach was reasonably close to what you offered. And here are the results (where original_luhn is the list version and string_luhn is the recursive pattern matching version:
Operating System: Linux
CPU Information: Intel(R) Core(TM) i5-9300H CPU @ 2.40GHz
Number of Available Cores: 8
Available memory: 15.79 GB
Elixir 1.10.3
Erlang 22.3.4.1
Benchmark suite executing with the following configuration:
warmup: 2 s
time: 5 s
memory time: 0 ns
parallel: 1
inputs: none specified
Estimated total run time: 14 s
Benchmarking original_luhn...
Benchmarking string_luhn...
Name ips average deviation median 99th %
original_luhn 440.61 K 2.27 μs ±925.00% 2.10 μs 3.20 μs
string_luhn 325.14 K 3.08 μs ±1930.38% 2.80 μs 3.70 μs
Comparison:
original_luhn 440.61 K
string_luhn 325.14 K - 1.36x slower +0.81 μs
I found these results to be pretty surprising. I am wondering if the explanation is that I am unaware of something expensive in the string version, something optimized in the list version, or if I am benchmarking/interpreting the benchmark wrong.
Here is a link to the repo with both versions and a full explanation of the algorithm.
Popular in Discussions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #javascript
- #code-sync
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








