Skip to content

Avoid double traversal when restoring CR diff lines - #15885

Merged
josevalim merged 1 commit into
elixir-lang:mainfrom
claytongentry:optimize-cr-diff-reversal
Sep 11, 2026
Merged

Avoid double traversal when restoring CR diff lines#15885
josevalim merged 1 commit into
elixir-lang:mainfrom
claytongentry:optimize-cr-diff-reversal

Conversation

@claytongentry

Copy link
Copy Markdown
Contributor

Assisted-by: Codex:GPT-6

Replace Enum.reverse/1 followed by Enum.concat/2 with Enum.reverse/2 in the CR diff handling base cases, avoiding an extra traversal and copy of the reversed list.

Benchmark is noisy for small input timings and shows no or negligible improvement, but shows reduced average execution time by ~8% at 100 lines and ~11-12% at 1000 lines. 1000 line case also shows ~5% less memory and ~5% fewer reductions.

Bench:

Mix.install([{:benchee, "== 1.5.1"}])

defmodule OldFormat do
  @moduledoc "Previous CR diff restoration using reverse followed by concat."

  @cr "↵"

  def insert_cr_symbols(diffs, false), do: diffs
  def insert_cr_symbols(diffs, true), do: do_insert_cr_symbols(diffs, [])

  defp do_insert_cr_symbols([], acc), do: Enum.reverse(acc)

  defp do_insert_cr_symbols([{:del, del}, {:ins, ins} | rest], acc) do
    {del, ins} = do_insert_cr_symbols(del, ins, {[], []})
    do_insert_cr_symbols(rest, [{:ins, ins}, {:del, del} | acc])
  end

  defp do_insert_cr_symbols([diff | rest], acc) do
    do_insert_cr_symbols(rest, [diff | acc])
  end

  defp do_insert_cr_symbols([left | left_rest], [right | right_rest], {left_acc, right_acc}) do
    {left, right} = insert_cr_symbol(left, right)
    do_insert_cr_symbols(left_rest, right_rest, {[left | left_acc], [right | right_acc]})
  end

  defp do_insert_cr_symbols([], right, {left_acc, right_acc}) do
    left = Enum.reverse(left_acc)
    right = right_acc |> Enum.reverse() |> Enum.concat(right)
    {left, right}
  end

  defp do_insert_cr_symbols(left, [], {left_acc, right_acc}) do
    left = left_acc |> Enum.reverse() |> Enum.concat(left)
    right = Enum.reverse(right_acc)
    {left, right}
  end

  defp insert_cr_symbol(left, right) do
    case {String.ends_with?(left, "\r"), String.ends_with?(right, "\r")} do
      {bool, bool} -> {left, right}
      {true, false} -> {String.replace(left, "\r", @cr), right}
      {false, true} -> {left, String.replace(right, "\r", @cr)}
    end
  end
end

defmodule NewFormat do
  @moduledoc "Optimized CR diff restoration using reverse with a tail."

  @cr "↵"

  def insert_cr_symbols(diffs, false), do: diffs
  def insert_cr_symbols(diffs, true), do: do_insert_cr_symbols(diffs, [])

  defp do_insert_cr_symbols([], acc), do: Enum.reverse(acc)

  defp do_insert_cr_symbols([{:del, del}, {:ins, ins} | rest], acc) do
    {del, ins} = do_insert_cr_symbols(del, ins, {[], []})
    do_insert_cr_symbols(rest, [{:ins, ins}, {:del, del} | acc])
  end

  defp do_insert_cr_symbols([diff | rest], acc) do
    do_insert_cr_symbols(rest, [diff | acc])
  end

  defp do_insert_cr_symbols([left | left_rest], [right | right_rest], {left_acc, right_acc}) do
    {left, right} = insert_cr_symbol(left, right)
    do_insert_cr_symbols(left_rest, right_rest, {[left | left_acc], [right | right_acc]})
  end

  defp do_insert_cr_symbols([], right, {left_acc, right_acc}) do
    {Enum.reverse(left_acc), Enum.reverse(right_acc, right)}
  end

  defp do_insert_cr_symbols(left, [], {left_acc, right_acc}) do
    {Enum.reverse(left_acc, left), Enum.reverse(right_acc)}
  end

  defp insert_cr_symbol(left, right) do
    case {String.ends_with?(left, "\r"), String.ends_with?(right, "\r")} do
      {bool, bool} -> {left, right}
      {true, false} -> {String.replace(left, "\r", @cr), right}
      {false, true} -> {left, String.replace(right, "\r", @cr)}
    end
  end
end

inputs =
  Map.new([{1, 0}, {10, 3}, {100, 3}, {1000, 3}, {1000, -3}], fn {depth, extra} ->
    label = "CR diff, #{depth} lines, #{extra} extra insertions"
    diff = [del: List.duplicate("old\r", depth), ins: List.duplicate("new\r", depth + extra)]
    {label, diff}
  end)

Benchee.run(
  %{
    "Old (reverse + concat)" => &OldFormat.insert_cr_symbols(&1, true),
    "New (reverse with tail)" => &NewFormat.insert_cr_symbols(&1, true)
  },
  inputs: inputs,
  pre_check: :all_same,
  warmup: 0.5,
  time: 1.0,
  memory_time: 0.5,
  reduction_time: 0.5
)

Results

##### With input CR diff, 1 lines, 0 extra insertions #####
Name                              ips        average  deviation         median         99th %
Old (reverse + concat)        13.07 M       76.53 ns  ±4779.30%          83 ns         125 ns
New (reverse with tail)       11.52 M       86.82 ns  ±5103.53%          83 ns         125 ns

Comparison:
Old (reverse + concat)        13.07 M
New (reverse with tail)       11.52 M - 1.13x slower +10.29 ns

Memory usage statistics:

Name                       Memory usage
Old (reverse + concat)            216 B
New (reverse with tail)           232 B - 1.07x memory usage +16 B

**All measurements for memory usage were the same**

Reduction count statistics:

Name                    Reduction count
Old (reverse + concat)               21
New (reverse with tail)              21 - 1.00x reduction count +0

**All measurements for reduction count were the same**

##### With input CR diff, 10 lines, 3 extra insertions #####
Name                              ips        average  deviation         median         99th %
New (reverse with tail)        2.75 M      363.97 ns  ±1403.88%         333 ns         500 ns
Old (reverse + concat)         2.53 M      395.79 ns   ±971.13%         334 ns         542 ns

Comparison:
New (reverse with tail)        2.75 M
Old (reverse + concat)         2.53 M - 1.09x slower +31.82 ns

Memory usage statistics:

Name                       Memory usage
New (reverse with tail)         1.23 KB
Old (reverse + concat)          1.38 KB - 1.13x memory usage +0.156 KB

**All measurements for memory usage were the same**

Reduction count statistics:

Name                    Reduction count
New (reverse with tail)              89
Old (reverse + concat)               90 - 1.01x reduction count +1

**All measurements for reduction count were the same**

##### With input CR diff, 100 lines, 3 extra insertions #####
Name                              ips        average  deviation         median         99th %
New (reverse with tail)      323.96 K        3.09 μs    ±34.85%           3 μs        5.29 μs
Old (reverse + concat)       298.58 K        3.35 μs    ±69.82%        3.08 μs        5.83 μs

Comparison:
New (reverse with tail)      323.96 K
Old (reverse + concat)       298.58 K - 1.09x slower +0.26 μs

Memory usage statistics:

Name                       Memory usage
New (reverse with tail)         9.95 KB
Old (reverse + concat)         11.51 KB - 1.16x memory usage +1.56 KB

**All measurements for memory usage were the same**

Reduction count statistics:

Name                    Reduction count
New (reverse with tail)             797
Old (reverse + concat)              799 - 1.00x reduction count +2

**All measurements for reduction count were the same**

##### With input CR diff, 1000 lines, -3 extra insertions #####
Name                              ips        average  deviation         median         99th %
New (reverse with tail)       29.01 K       34.47 μs     ±9.56%       34.04 μs       44.29 μs
Old (reverse + concat)        25.89 K       38.63 μs    ±12.23%       38.29 μs       52.06 μs

Comparison:
New (reverse with tail)       29.01 K
Old (reverse + concat)        25.89 K - 1.12x slower +4.16 μs

Memory usage statistics:

Name                       Memory usage
New (reverse with tail)       109.18 KB
Old (reverse + concat)        115.10 KB - 1.05x memory usage +5.92 KB

**All measurements for memory usage were the same**

Reduction count statistics:

Name                    Reduction count
New (reverse with tail)          8.36 K
Old (reverse + concat)           8.78 K - 1.05x reduction count +0.42 K

**All measurements for reduction count were the same**

##### With input CR diff, 1000 lines, 3 extra insertions #####
Name                              ips        average  deviation         median         99th %
New (reverse with tail)       28.65 K       34.90 μs    ±11.70%       34.46 μs       45.52 μs
Old (reverse + concat)        25.11 K       39.82 μs    ±12.59%          39 μs       53.42 μs

Comparison:
New (reverse with tail)       28.65 K
Old (reverse + concat)        25.11 K - 1.14x slower +4.92 μs

Memory usage statistics:

Name                       Memory usage
New (reverse with tail)       109.51 KB
Old (reverse + concat)        115.10 KB - 1.05x memory usage +5.59 KB

**All measurements for memory usage were the same**

Reduction count statistics:

Name                    Reduction count
New (reverse with tail)          8.39 K
Old (reverse + concat)           8.81 K - 1.05x reduction count +0.42 K

**All measurements for reduction count were the same**

Assisted-by: Codex:GPT-6
Signed-off-by: Clayton Gentry <clayton@podstock.io>
@claytongentry
claytongentry force-pushed the optimize-cr-diff-reversal branch from 605531e to 05ecf3e Compare September 11, 2026 12:23
@josevalim
josevalim merged commit 9d67a45 into elixir-lang:main Sep 11, 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