Nice! That’s a really cool solution, and probably scales well! It fails the first requirement though, the input should be piped in. Reading from a file is faster than reading from stdin in general (eg replace IO.binstream with File.read and it runs faster) and gives you much more control, like you show in your code.
You can speed up your version quite a bit if you switch to iolists, instead of printing line by line.
Hi @jola, did you try getting rid of the :line option and processing chunk by chunk? What I’ve seen so far (more details here: Streaming lines from an enum of chunks) is that this
It does like I said in the article, I didn’t spend too much time messing around with the chunk size, but that number gave decent performance improvement compared to reading line by line with that input on my machine.
Also, switching to reading the file directly would be faster, but the original article used stdin in all examples, so it would be not be a fair comparison.
Take a look at @evadne’s solution for one that optimizes reading over 4 workers if you’re curious how fast it can be reading directly from file.
That’s awesome! I think @jola got hers down to 7-ish seconds in her talk (if I’m remembering correctly)? But it would be neat to throw yours onto a 16-core machine and see how it does
I ran @JEG2 locally and it came up with 9.5s. Here’s the comparison:
➜ time elixir wp_parallel.exs < ./words.txt > /dev/null
elixir wp_parallel.exs < ./words.txt > /dev/null 27.14s user 1.79s system 302% cpu 9.550 total
➜ time elixir wp_singlel.exs < ./words.txt > /dev/null
elixir wp_single.exs < ./words.txt > /dev/null 10.28s user 2.91s system 103% cpu 12.739 total
You can see that the parallel version uses much more CPU, but it brings the time down by ~20%. It is a cool approach to read in parallel. I tried to read at once and then spawn out the processes, which was slower than I expected.
@darinwilson running with > /dev/null gets it down lower
➜ elixirconf time elixir lib/direction3/async_stream.ex < ../words.txt > /dev/null
elixir lib/direction3/async_stream.ex < ../words.txt > /dev/null 17.79s user 3.08s system 380% cpu 5.484 total
and then you can cheat a bit
➜ elixirconf time elixir --erl "+hms 500000000" lib/direction3/async_stream.ex < ../words.txt > /dev/null
elixir --erl "+hms 500000000" lib/direction3/async_stream.ex < ../words.txt > 16.26s user 2.82s system 420% cpu 4.543 total
For those keeping score at home, 4.5 is almost as fast as C (admittedly it uses 4x CPU)
➜ elixirconf time elixir lib/extra/jeg2.exs < ../words.txt > /dev/null
elixir lib/extra/jeg2.exs < ../words.txt > /dev/null 27.02s user 1.77s system 276% cpu 10.417 total
Using the +hms cheat brings it down to like 7 seconds.
I really like this solution. You don’t have to stitch together prefix+suffix and memory usage is about 1GB on my machine. Speed is totally reasonable
You weren’t sorting by strings, you were sorting by count (they second value in the tuple). The output was correct. At least in the version I got from your gist.
With List.keysort they get sorted in reverse order though (asc instead of desc). There doesn’t seem to be an option to choose order? But yeah, it’s faster nice find! Something like 200-400ms after a few runs.
And a message to everyone in the thread: I just want to point out that all my measurements are with the output printed to terminal, unless otherwise specified. Just to be clear. Running with > /dev/null or > outputfile is much faster.
To close the “hole” in the pipeline between the Enum.each and :ets.tab2list, what do you think about Enum.reduce with the ets table as the accumulator?
...
|> Enum.reduce(:ets.new(:words, []), fn word, table ->
:ets.update_counter(table, word, {2, 1}, {word, 0})
table
end)
|> :ets.tab2list()
...
Performance wise I get same results. But maybe we could argue that returning the table in the reduction is not aesthetically that pleasant