Skip to content

In EEX Compiler, flatten expr list only once, not on every iteration#15331

Merged
josevalim merged 3 commits intoelixir-lang:mainfrom
PJUllrich:main
May 1, 2026
Merged

In EEX Compiler, flatten expr list only once, not on every iteration#15331
josevalim merged 3 commits intoelixir-lang:mainfrom
PJUllrich:main

Conversation

@PJUllrich
Copy link
Copy Markdown
Contributor

Disclosure: I found and fixed the following performance improvement using Opus 4.7, but I made damn sure to understand what the problem is and how the fix works. I also wrote the description below myself to make sure I know what I'm proposing here.

Problem

This PR fixes a performance issue in /lib/eex/lib/eex/compiler.ex:444-452 -> wrap_expr/5 which computes count = current ++ placeholder ++ new_lines ++ chars for every middle block and the end block of an expression. The current is the aggregator of the expression and because ++ is used, the compiler walks every character in that charlist before it appends the placeholder, new_lines, and chars. So, the longer the current becomes, the longer the compiler needs to walk the charlist until it finally appends the new block to the aggregator. This is roughly O(N^2).

Fix

With the help of Opus, I created a fix which builds a nested list of expressions and then flattens the list into a single charlist once the end block is reached, so flattening the list only once instead of after every middle block.

I also fixed another small issue which was that the quoted expressions were stored as Keyword list but retrieved like a Map (i.e. "find the expression for a given key") in insert_quoted/2. I refactored quoted to be a map which took care of the quadratic performance with large numbers of case arms. You can see it at the N = 1600 case in the benchmarks where the average iteration takes 4.4x longer if N doubles (from 800 -> 1600).

Benchmarks

Here are the before/after benchmarks:

I built a very long case x do ... end expression with N case arms like 1, 2`, ... N as cases.

N Before After Speedup Before μs/N μs/N² After μs/N μs/N²
100 0.47 ms 0.27 ms 1.7× 4.71 0.0471 2.72 0.0272
200 1.51 ms 0.49 ms 2.6× 7.56 0.0378 2.44 0.0122
400 5.49 ms 1.06 ms 4.6× 13.72 0.0343 2.66 0.0066
800 23.3 ms 2.13 ms 8.1× 29.12 0.0364 2.67 0.0033
1600 102.8 ms 4.61 ms 22.3× 64.22 0.0401 2.88 0.0018

@PJUllrich
Copy link
Copy Markdown
Contributor Author

This is a before and after of how the expression block is aggregated:

Before with ++ concatenation

--- wrap_expr call (key=0) ---
  current (in):  ~c" if x do "
  chars   (in):  ~c" else "
  count   (out): ~c" if x do __EEX__(0); else "      ← walked 10 chars

--- wrap_expr call (key=1) ---
  current (in):  ~c" if x do __EEX__(0); else "      ← the 28-char flat list
  chars   (in):  ~c" end "
  count   (out): ~c" if x do __EEX__(0); else __EEX__(1); end "   ← walked 28 chars

--- handing to Code.string_to_quoted! ---
  wrapped: ~c" if x do __EEX__(0); else __EEX__(1); end "

After with nested iolist

--- wrap_expr call (key=0) ---
  current (in):  ~c" if x do "
  chars   (in):  ~c" else "
  count   (out): [~c" if x do ", [~c"__EEX__(", ~c"0", ~c");"], [], ~c" else "]

--- wrap_expr call (key=1) ---
  current (in):  [~c" if x do ", [~c"__EEX__(", ~c"0", ~c");"], [], ~c" else "]
  chars   (in):  ~c" end "
  count   (out): [
    [~c" if x do ", [~c"__EEX__(", ~c"0", ~c");"], [], ~c" else "],
    [~c"__EEX__(", ~c"1", ~c");"],
    [],
    ~c" end "
  ]

--- handing to Code.string_to_quoted! ---
  wrapped (iolist):    [
    [~c" if x do ", [~c"__EEX__(", ~c"0", ~c");"], [], ~c" else "],
    [~c"__EEX__(", ~c"1", ~c");"],
    [],
    ~c" end "
  ]
  wrapped (flattened): ~c" if x do __EEX__(0); else __EEX__(1); end "

Comment thread lib/eex/lib/eex/compiler.ex Outdated
Co-authored-by: José Valim <jose.valim@gmail.com>
@PJUllrich
Copy link
Copy Markdown
Contributor Author

Seems like an unrelated test failed in the CI

@josevalim josevalim merged commit a0e1604 into elixir-lang:main May 1, 2026
14 of 15 checks passed
@josevalim
Copy link
Copy Markdown
Member

💚 💙 💜 💛 ❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants