Skip to content

Avoid traversing untouched tails in Enum.reverse_slice/3 - #15938

Merged
josevalim merged 1 commit into
elixir-lang:mainfrom
preciz:perf/reverse_slice
Sep 24, 2026
Merged

josevalim merged 1 commit into
elixir-lang:mainfrom
preciz:perf/reverse_slice

Conversation

@preciz

@preciz preciz commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Assisted-by: Codex:GPT-6
Assisted-by: Antigravity:Gemini 3.8 Flash

In most cases it is faster but has tradeoffs. It uses less memory.

Walk the prefix and selected slice directly, returning the input for zero- and one-element reversals. Reuse the untouched suffix and remove whole-list reversal and counting.
Include boundary and single-enumeration tests.

Bench:

# Run with: ./bin/elixir bench.exs
Mix.install([:benchee])

path = "lib/elixir/lib/enum.ex"

for {ref, module} <- [{"HEAD^", OldEnum}, {"HEAD", NewEnum}] do
  {source, 0} = System.cmd("git", ["show", "#{ref}:#{path}"], cd: __DIR__)

  [_, enum_code] = String.split(source, "defmodule Enum do\n", parts: 2)
  [enum_code, _] = String.split(enum_code, "\nend\n\ndefimpl Enumerable", parts: 2)
  code = "defmodule #{inspect(module)} do\n" <> enum_code <> "\nend\n"

  Code.compile_string(code, path)
end

list_20 = Enum.to_list(1..20)
list_10k = Enum.to_list(1..10_000)

inputs = %{
  "small (len 20): count=0" => {list_20, 5, 0},
  "small (len 20): head slice (start 0, count 5)" => {list_20, 0, 5},
  "small (len 20): middle slice (start 5, count 10)" => {list_20, 5, 10},
  "small (len 20): tail slice (start 15, count 5)" => {list_20, 15, 5},
  "small (len 20): start out of bounds (start 50, count 5)" => {list_20, 50, 5},
  "large (len 10k): count=0" => {list_10k, 500, 0},
  "large (len 10k): head small slice (start 0, count 10)" => {list_10k, 0, 10},
  "large (len 10k): middle small slice (start 1000, count 50)" => {list_10k, 1000, 50},
  "large (len 10k): middle large slice (start 2000, count 6000)" => {list_10k, 2000, 6000},
  "large (len 10k): tail slice (start 9500, count 500)" => {list_10k, 9500, 500},
  "large (len 10k): full list (start 0, count 10000)" => {list_10k, 0, 10_000},
  "large (len 10k): start out of bounds (start 15000, count 100)" => {list_10k, 15_000, 100},
  "range (len 10k): middle slice (start 1000, count 50)" => {1..10_000, 1000, 50}
}

Benchee.run(
  %{
    "HEAD^" => fn {enum, start, count} -> OldEnum.reverse_slice(enum, start, count) end,
    "HEAD" => fn {enum, start, count} -> NewEnum.reverse_slice(enum, start, count) end
  },
  inputs: inputs,
  pre_check: :all_same,
  warmup: 1,
  time: 2,
  memory_time: 1
)

Results:

Input Scenario Old (HEAD^) IPS / Memory New (HEAD) IPS / Memory Speedup / Memory Reduction
large (len 10k): count=0 14.0 K ips / 223.89 KB 26.77 M ips / 0 KB 1,911x faster (0 B memory)
large (len 10k): head small slice (start 0, count 10) 8.95 K ips / 224.07 KB 7.48 M ips / 0.34 KB 835x faster (667x less memory)
large (len 10k): middle small slice (start 1k, count 50) 11.03 K ips / 224.70 KB 141.02 K ips / 32.86 KB 12.8x faster (6.8x less memory)
range (len 10k): middle slice (start 1k, count 50) 4.03 K ips / 313.30 KB 38.07 K ips / 189.11 KB 9.45x faster (1.66x less memory)
large (len 10k): middle large slice (start 2k, count 6k) 8.49 K ips / 305.16 KB 16.43 K ips / 224.02 KB 1.94x faster (1.36x less memory)
large (len 10k): full list (start 0, count 10k) 7.58 K ips / 273.91 KB 13.21 K ips / 156.27 KB 1.74x faster (1.75x less memory)
large (len 10k): tail slice (start 9.5k, count 500) 9.51 K ips / 231.73 KB 12.61 K ips / 223.98 KB 1.33x faster
small (len 20): count=0 11.22 M ips / 640 B 26.92 M ips / 0 B 2.40x faster (0 B memory)
small (len 20): head slice (start 0, count 5) 4.46 M ips / 744 B 10.13 M ips / 184 B 2.27x faster (4x less memory)
small (len 20): middle slice (start 5, count 10) 5.13 M ips / 824 B 6.68 M ips / 528 B 1.30x faster
large (len 10k): start out of bounds 17.69 K ips / 223.89 KB 14.80 K ips / 156.27 KB ~1.20x slower (less memory)
small (len 20): start out of bounds 10.82 M ips / 640 B 5.92 M ips / 344 B ~1.83x slower (+76 ns)
small (len 20): tail slice (start 15, count 5) 7.48 M ips / 744 B 3.97 M ips / 608 B ~1.88x slower (+118 ns)

Walk the prefix and selected slice directly, returning the input for zero- and one-element reversals. Reuse the untouched suffix and remove whole-list reversal and counting.

Include boundary and single-enumeration tests.

Assisted-by: Codex:GPT-6
@josevalim

Copy link
Copy Markdown
Member

Can you please do the benchmarks also using ranges as enumerables? It may be that we need to provide a special path for lists but keep the current implementation for others.

@preciz

preciz commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor Author

Bench ranges:

# Run with: ./bin/elixir bench_ranges.exs
Mix.install([:benchee])

path = "lib/elixir/lib/enum.ex"

for {ref, module} <- [{"HEAD^", OldEnum}, {"HEAD", NewEnum}] do
  {source, 0} = System.cmd("git", ["show", "#{ref}:#{path}"], cd: __DIR__)

  [_, enum_code] = String.split(source, "defmodule Enum do\n", parts: 2)
  [enum_code, _] = String.split(enum_code, "\nend\n\ndefimpl Enumerable", parts: 2)
  code = "defmodule #{inspect(module)} do\n" <> enum_code <> "\nend\n"

  Code.compile_string(code, path)
end

range_20 = 1..20
range_10k = 1..10_000

inputs = %{
  "small (len 20): count=0" => {range_20, 5, 0},
  "small (len 20): head slice (start 0, count 5)" => {range_20, 0, 5},
  "small (len 20): middle slice (start 5, count 10)" => {range_20, 5, 10},
  "small (len 20): tail slice (start 15, count 5)" => {range_20, 15, 5},
  "small (len 20): full range (start 0, count 20)" => {range_20, 0, 20},
  "small (len 20): start out of bounds (start 50, count 5)" => {range_20, 50, 5},
  "large (len 10k): count=0" => {range_10k, 500, 0},
  "large (len 10k): head small slice (start 0, count 10)" => {range_10k, 0, 10},
  "large (len 10k): middle small slice (start 1000, count 50)" => {range_10k, 1000, 50},
  "large (len 10k): middle large slice (start 2000, count 6000)" => {range_10k, 2000, 6000},
  "large (len 10k): tail slice (start 9500, count 500)" => {range_10k, 9500, 500},
  "large (len 10k): full range (start 0, count 10000)" => {range_10k, 0, 10_000},
  "large (len 10k): start out of bounds (start 15000, count 100)" => {range_10k, 15_000, 100}
}

Benchee.run(
  %{
    "HEAD^" => fn {enum, start, count} -> OldEnum.reverse_slice(enum, start, count) end,
    "HEAD" => fn {enum, start, count} -> NewEnum.reverse_slice(enum, start, count) end
  },
  inputs: inputs,
  pre_check: :all_same,
  warmup: 1,
  time: 2,
  memory_time: 1
)

Results:

Input Scenario Old (HEAD^) IPS / Memory New (HEAD) IPS / Memory Speedup / Memory Change
large (len 10k): count=0 7.41 K ips / 170.81 KB 44.75 K ips / 156.25 KB 6.04x faster (less memory)
large (len 10k): head small slice (start 0, count 10) 3.63 K ips / 312.68 KB 43.87 K ips / 156.59 KB 12.09x faster (2.00x less memory)
large (len 10k): middle small slice (start 1k, count 50) 3.91 K ips / 313.30 KB 36.58 K ips / 189.11 KB 9.35x faster (1.66x less memory)
large (len 10k): middle large slice (start 2k, count 6k) 5.33 K ips / 394.80 KB 9.42 K ips / 394.83 KB 1.77x faster (same memory)
large (len 10k): full range (start 0, count 10k) 5.07 K ips / 394.79 KB 6.27 K ips / 312.52 KB 1.24x faster (1.26x less memory)
large (len 10k): start out of bounds (start 15k, count 100) 7.26 K ips / 170.81 KB 9.86 K ips / 312.52 KB 1.36x faster (+141 KB memory)
large (len 10k): tail slice (start 9.5k, count 500) 8.17 K ips / 238.49 KB 8.70 K ips / 394.80 KB 1.07x faster (+156 KB memory)
small (len 20): count=0 3.74 M ips / 640 B 11.03 M ips / 320 B 2.95x faster (2.00x less memory)
small (len 20): head slice (start 0, count 5) 2.62 M ips / 744 B 8.40 M ips / 504 B 3.20x faster (1.48x less memory)
small (len 20): middle slice (start 5, count 10) 2.72 M ips / 824 B 5.01 M ips / 848 B 1.84x faster (~same memory)
small (len 20): tail slice (start 15, count 5) 3.26 M ips / 744 B 4.60 M ips / 928 B 1.41x faster (+184 B memory)
small (len 20): full range (start 0, count 20) 2.56 M ips / 984 B 4.43 M ips / 664 B 1.73x faster (1.48x less memory)
small (len 20): start out of bounds (start 50, count 5) 3.71 M ips / 640 B 5.11 M ips / 664 B 1.38x faster (~same memory)

@josevalim
josevalim merged commit 96afa7a into elixir-lang:main Sep 24, 2026
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