fireproofsocks

fireproofsocks

I’ve been working on an Elixir project that has required a lot of scripting. I usually reach for Elixir because I like it more (and in this case, I could reuse code). However, I’ve noticed that the performance is sometimes poor. I have tried doing the same task in Python, and at in my initial tests, Python is much faster.

Here’s the repo (specifically the scripts/ directory):

https://github.com/fireproofsocks/ex_vs_py

To reproduce the behavior (after install and mix deps.get):

  1. mix run scripts/make_files.exs : this preps the directory with sample files – takes maybe 30 seconds.
  2. mix run scripts/vet_files.exs to run the Elixir version of parsing/vetting the files. Example output: Duration: 2424 ms
  3. Compare with python scripts/vet_files.py with example output Duration: 608 ms

I haven’t spent a whole lot of time trying to refactor the Elixir (or the Python) code, but this setup is a fairly accurate recreation of one of the tasks we needed to figure out, and when you’re dealing with lots and lots of files, even little inefficiencies add up.

I’m wondering if the community here can share any insights or knowledge about Elixir’s performance for scripts such as this. Thanks in advance!

Showing Posts 1 to 10

BradS2S

BradS2S

I’d be curious what would happen if you piped the file into file.read instead of stream. It looks like Python reads the file completely into memory before processing the lines.

Also you might want to check out genstage which is optimized for creating data pipelines.

*disclaimer: I’m less experienced than the average user on this forum.

cloudytoday

cloudytoday

Interesting, thanks for sharing. In my case, which is mostly about lists crunching, Elixir is many tens of times faster than Python. With some optimizations the advantage becomes many hundreds of times faster even with 5 times larger amounts of data. For example, an imperative algorithm on a list of ~130k dicts in Python would take me about 20 minutes. Trying to rewrite it functionally and seeing the function just get stuck and never return is what made me switch to Elixir :grinning:. In Elixir, with an imperative algorithm, I’ve been able to get done with a list of ~500k structs in ~200ms.

Your .exs script is ~1000ms for me, while .py one is ~500ms. Elixir 14.3/OTP 25, Python 3.10.7.

RTLS

RTLS

Here is a PR that gets the Elixir performance closer to the python performance, though still slower.

https://github.com/fireproofsocks/ex_vs_py/pull/1

I saw from eprof that after Jason, most time was spent in genservers and cleaning up processes. This is due to the File module opening a new process for every opened file.

➜  ex_vs_py git:(main) ✗ mix profile.eprof scripts/vet_files.exs
Warmup...

Duration: 2009 ms
Duration: 1604 ms

Profile results of #PID<0.212.0>
#                                                          CALLS     %  TIME µS/CALL
Total                                                    3460446 100.0 43300    0.13

:gen_server.call/3                                         64901  2.21  9567    0.15
:erlang.monitor/2                                          64902  2.60 11244    0.17
:file.check_args/1                                        184703  2.70 11683    0.06
File.exists?/2                                             54901  2.91 12604    0.23
Enum."-each/2-lists^foreach/1-0-"/2                        64901  2.93 12698    0.20
:file.read_file_info/2                                     54901  3.31 14329    0.26
:erlang.demonitor/2                                        64902  4.34 18778    0.29
:file.call/2                                               64901  5.90 25540    0.39
:gen.do_call/4                                             64901 10.78 46673    0.72
Jason.Decoder.string/6                                   1322723 21.88 94739    0.07

My PR replaces a lot of these calls with :prim_file which is a nif and does not spawn a process for every file. That also allows the Task.async_stream to help performance; including the async_stream while using the File module just leads to the File genserver being a bottleneck.

Next you might try improving the json performance, perhaps with Eljiffy.

D4no0

D4no0

I suspect the stream might be the culprit here, if your file is never that big avoid using stream or read bigger chunks at once.

LostKobrakai

LostKobrakai

While it’s useful to look at the perf of the erlang code you’d probably also want to evaluate how much of the time is starting up the beam vm. Not sure which otp applications are started by default with mix, but I recently read that they contribute to a good chunk of the startup time for things running on the beam.

hst337

hst337

Yeah, but in this benchmark all applications are started before the script is executed

hst337

hst337

I’ve modified both scripts to return a list of results and Elixir beats python here.

start_time = :erlang.monotonic_time(:millisecond)
index_file = "tmp/files/index.txt"

results =
  index_file
  |> File.stream!()
  |> Task.async_stream(fn line ->
    path = String.trim(line)
    {:ok, contents} = :prim_file.read_file(path)
    %{"paths" => txt_paths} = :jiffy.decode(contents, [:return_maps])
  end, max_concurrency: 16, ordered: false)
  |> Stream.flat_map(fn {:ok, results} -> results end)
  |> Task.async_stream(fn file ->
    match? {:ok, _}, :prim_file.read_file_info(file)
  end, max_concurrency: 8, ordered: false)
  |> Enum.map(fn {:ok, result} -> result end)

end_time = :erlang.monotonic_time(:millisecond)
duration = end_time - start_time
IO.puts("Duration: #{duration} ms")
IO.inspect Enum.all? results

vs

from os.path import exists
import json
from time import time

index_file = "tmp/files/index.txt"

existing = []

def vet_files():
    with open(index_file, 'r') as myfile:
        for line in myfile:
            open_json_file(line.rstrip())
            # dict_obj = json.loads(person_data)


def open_json_file(json_file):
    with open(json_file, 'r') as myfile:
        for line in myfile:
            data = json.loads(line)
            files_exist(data['paths'])

def files_exist(paths):
    for p in paths:
        existing.append(exists(p))

if __name__ == "__main__":
    start_time = int(time() * 1000)
    vet_files()
    end_time = int(time() * 1000)
    print(f'Duration: {end_time - start_time} ms')
    print(all(existing))

And the results are 291ms elixir vs 314ms python.

josevalim

josevalim

Creator of Elixir

Some general remarks for guidance:

  1. Keep in mind the Erlang VM makes specific trade-offs in relation to high-performance, such as process preemption. It is better to have a predictable system that goes slightly less fast than a fast ones that is unpredictable (or crashes)

  2. Streams have lower memory usage at the cost of higher CPU usage. If your goal is to go as fast as you can, not using streams may be better (such as File.read! |> String.split("\n", trim: true))

  3. You should see benefits by adding Task.async_stream and similar so you can leverage multi-core

  4. I would assume that most of the time is taken by JSON parsing so remember Jason is a pure Elixir package. I assume that the json parsing in Python is most likely done in C. So you may have better results by using something like jiffy (and a more apples to apples comparison)

20
Post #8
hst337

hst337

Most of time is spent on accessing files. I don’t know, but I thought that Erlang team has switched to epoll for prim_file on linux

michalmuskala

michalmuskala

Default file IO in Erlang is fairly slow. I’d recommend using it with the [:raw] option - it bypasses several layers of abstraction that introduce quite a fair bit of overhead.

Where Next? Top

Trending in Discussions Top

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2977 91898 914
New
AstonJ
The obligatory hello world thread! Who are you and where are you from? :stuck_out_tongue:
4616 55835 594
New
byu
@chrismccord : I just saw the Extract AGENTS.md from Phoenix.new into phx.new generator commit to the phoenix project. My initial shotgu...
New
arcanemachine
I was working on an Ecto migration and I needed a timestamp. So, for the nth time, I looked up the different data types for timestamps, a...
New
alexslade
Fly’s CEO posted this recently - Turn And Face The Strange · The Fly Blog It says that Fly is going all-in on sprites, which is a worry ...
New
Herve37
We’re evaluating API mocking tools for OpenAPI-based projects and would love to hear what other teams are using. We’re particularly inte...
New
matt-savvy
Is there a word for the ~> symbol used in Version strings? Do you also just call it a Squiggle Arrow™ ?!
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews