Skip to content

Optimize table index lookup - #31

Open
preciz wants to merge 1 commit into
elixir-mint:mainfrom
preciz:optimize-table-index-lookup
Open

Optimize table index lookup#31
preciz wants to merge 1 commit into
elixir-mint:mainfrom
preciz:optimize-table-index-lookup

Conversation

@preciz

@preciz preciz commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Assisted-by: Gemini 3.7 Flash
Assisted-by: Codex CLI:GPT 5.6 Sol

Replace generated static clauses and Enum.at/2 with tuple-backed static lookup and an inlined dynamic traversal, removing the redundant empty-table clause. Cover every static index and dynamic depths through 64.

Bench:

Mix.install([:benchee])

defmodule Bench.Data do
  def static, do: for(index <- 1..61, do: {"static-#{index}", "value-#{index}"})
  def dynamic, do: for(index <- 1..68, do: {"dynamic-#{index}", "value-#{index}"})
end

defmodule Bench.Main do
  defstruct entries: [], length: 0

  @static_table Bench.Data.static()
  @static_table_size length(@static_table)
  @dynamic_table_start @static_table_size + 1

  def new, do: %__MODULE__{entries: Bench.Data.dynamic(), length: 68}

  def lookup_by_index(table, index)

  for {header, index} <- Enum.with_index(@static_table, 1) do
    def lookup_by_index(%__MODULE__{}, unquote(index)), do: {:ok, unquote(header)}
  end

  def lookup_by_index(%__MODULE__{length: 0}, _index), do: :error

  def lookup_by_index(%__MODULE__{entries: entries, length: length}, index)
      when index >= @dynamic_table_start and index <= @dynamic_table_start + length - 1 do
    {:ok, Enum.at(entries, index - @dynamic_table_start)}
  end

  def lookup_by_index(%__MODULE__{}, _index), do: :error
end

defmodule Bench.Optimized do
  defstruct entries: [], length: 0

  @static_table Bench.Data.static()
  @static_table_size length(@static_table)
  @static_table_by_index @static_table |> Enum.map(&{:ok, &1}) |> List.to_tuple()
  @dynamic_table_start @static_table_size + 1

  def new, do: %__MODULE__{entries: Bench.Data.dynamic(), length: 68}

  def lookup_by_index(table, index)

  def lookup_by_index(%__MODULE__{}, index) when index in 1..@static_table_size do
    elem(@static_table_by_index, index - 1)
  end

  def lookup_by_index(%__MODULE__{entries: entries, length: length}, index)
      when index >= @dynamic_table_start and index <= @dynamic_table_start + length - 1 do
    {:ok, get_dynamic_entry(entries, index - @dynamic_table_start)}
  end

  def lookup_by_index(%__MODULE__{}, _index), do: :error

  @compile {:inline, get_dynamic_entry: 2}
  defp get_dynamic_entry([entry | _], 0), do: entry
  defp get_dynamic_entry([_ | rest], n), do: get_dynamic_entry(rest, n - 1)
end

defmodule Bench.Inputs do
  @request_static [1, 2, 4, 6, 16, 17, 19, 32, 51, 58]
  @response_static [8, 21, 24, 28, 31, 33, 34, 36, 44, 54, 55, 59, 60]

  def build do
    :rand.seed(:exsss, {17, 61, 4096})

    %{
      "request-like" =>
        input(@request_static, 4770, [
          {1..1, 317},
          {2..4, 1207},
          {5..8, 1287},
          {9..16, 1373},
          {17..32, 720},
          {33..39, 326}
        ]),
      "response-like" =>
        input(@response_static, 4390, [
          {1..1, 43},
          {2..4, 370},
          {5..8, 631},
          {9..16, 880},
          {17..32, 1440},
          {33..64, 2233},
          {65..68, 13}
        ])
    }
  end

  defp input(static_indices, static_count, dynamic_buckets) do
    static = static_indices |> Stream.cycle() |> Enum.take(static_count)

    dynamic =
      Enum.flat_map(dynamic_buckets, fn {depths, count} ->
        depths |> Stream.cycle() |> Enum.take(count) |> Enum.map(&(&1 + 61))
      end)

    %{
      indices: Enum.shuffle(static ++ dynamic),
      main: Bench.Main.new(),
      optimized: Bench.Optimized.new()
    }
  end
end

defmodule Bench.Runner do
  def main(%{indices: indices, main: table}), do: main(indices, table, 0)
  def optimized(%{indices: indices, optimized: table}), do: optimized(indices, table, 0)

  defp main([], _table, sum), do: sum

  defp main([index | rest], table, sum) do
    {:ok, {name, _}} = Bench.Main.lookup_by_index(table, index)
    main(rest, table, sum + byte_size(name))
  end

  defp optimized([], _table, sum), do: sum

  defp optimized([index | rest], table, sum) do
    {:ok, {name, _}} = Bench.Optimized.lookup_by_index(table, index)
    optimized(rest, table, sum + byte_size(name))
  end
end

Benchee.run(
  %{"main" => &Bench.Runner.main/1, "optimized" => &Bench.Runner.optimized/1},
  inputs: Bench.Inputs.build(),
  pre_check: :all_same,
  warmup: 2,
  time: 5,
  memory_time: 2,
  reduction_time: 2
)

Results (noisy system);

Operating System: Linux
CPU Information: AMD Ryzen 7 8845HS w
Number of Available Cores: 16
Available memory: 54.72 GB
Elixir 1.20.4
Erlang 29.0.5
JIT enabled: true

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 5 s
memory time: 2 s
reduction time: 2 s
parallel: 1
inputs: request-like, response-like
Estimated total run time: 44 s
Excluding outliers: false

##### With input request-like #####
Name                ips        average  deviation         median         99th %
optimized        3.83 K      260.93 μs    ±17.60%      255.17 μs      345.13 μs
main             2.05 K      488.61 μs    ±18.46%      460.59 μs      793.64 μs

Comparison: 
optimized        3.83 K
main             2.05 K - 1.87x slower +227.68 μs

Memory usage statistics:

Name         Memory usage
optimized       122.58 KB
main            204.30 KB - 1.67x memory usage +81.72 KB

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

Reduction count statistics:

Name      Reduction count
optimized         64.21 K
main             153.68 K - 2.39x reduction count +89.47 K

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

##### With input response-like #####
Name                ips        average  deviation         median         99th %
optimized        2.56 K      390.96 μs    ±14.18%      381.35 μs      506.82 μs
main             1.64 K      610.75 μs    ±15.40%      580.07 μs      927.38 μs

Comparison: 
optimized        2.56 K
main             1.64 K - 1.56x slower +219.79 μs

Memory usage statistics:

Name         Memory usage
optimized       131.48 KB
main            219.14 KB - 1.67x memory usage +87.66 KB

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

Reduction count statistics:

Name      Reduction count
optimized        114.70 K
main             258.88 K - 2.26x reduction count +144.17 K

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

Replace generated static clauses and Enum.at/2 with tuple-backed static lookup and an inlined dynamic traversal, removing the redundant empty-table clause. Cover every static index and dynamic depths through 64.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant