Current section

Files

Jump to
super_cache lib mix tasks benchmark_ets.ex
Raw

lib/mix/tasks/benchmark_ets.ex

defmodule Mix.Tasks.BenchmarkEts do
@moduledoc """
Run ETS query benchmarks comparing match vs select with compiled specs.
Usage:
mix benchmark.ets
"""
use Mix.Task
@table :benchmark_table
@num_records 10_000
@num_iterations 10_000
def run(_args) do
IO.puts("=" |> String.duplicate(70))
IO.puts("ETS Query Benchmark: match vs select (with compiled match specs)")
IO.puts("=" |> String.duplicate(70))
setup_table()
insert_test_data()
IO.puts("\nInserted #{@num_records} records into #{inspect(@table)}")
IO.puts("Running #{@num_iterations} iterations per test...\n")
# Run benchmarks
benchmark_match_vs_select()
benchmark_match_object_vs_select()
benchmark_tab2list_vs_select_all()
# Run with compiled specs
IO.puts("\n" <> "=" |> String.duplicate(70))
IO.puts("Benchmark: Compiled Match Specs (reused)")
IO.puts("=" |> String.duplicate(70))
benchmark_with_compiled_specs()
cleanup_table()
end
defp setup_table do
try do
:ets.delete(@table)
rescue
_ -> :ok
end
:ets.new(@table, [:set, :named_table, :public])
end
defp cleanup_table do
:ets.delete(@table)
end
defp insert_test_data do
for i <- 1..@num_records do
:ets.insert(@table, {:key, i, "value_#{i}", rem(i, 100)})
end
end
defp benchmark_match_vs_select do
IO.puts("-" |> String.duplicate(70))
IO.puts("Test 1: Find records where 4th element == 42")
IO.puts("-" |> String.duplicate(70))
pattern = {:key, :"$1", :"$2", 42}
match_spec = [{pattern, [], [:"$1", :"$2"]}]
# Before: Using :ets.match/2
{time_before, _} =
:timer.tc(fn ->
for _ <- 1..@num_iterations do
:ets.match(@table, pattern)
end
end)
# After: Using :ets.select/2 with match spec
{time_after, _} =
:timer.tc(fn ->
for _ <- 1..@num_iterations do
:ets.select(@table, match_spec)
end
end)
print_results(":ets.match/2", time_before, ":ets.select/2", time_after)
end
defp benchmark_match_object_vs_select do
IO.puts("\n" <> "-" |> String.duplicate(70))
IO.puts("Test 2: match_object - find records with 4th element == 42")
IO.puts("-" |> String.duplicate(70))
pattern = {:key, :_, :_, 42}
match_spec = [{pattern, [], [:"$_"]}]
# Before: Using :ets.match_object/2
{time_before, _} =
:timer.tc(fn ->
for _ <- 1..@num_iterations do
:ets.match_object(@table, pattern)
end
end)
# After: Using :ets.select/2 with match spec
{time_after, _} =
:timer.tc(fn ->
for _ <- 1..@num_iterations do
:ets.select(@table, match_spec)
end
end)
print_results(":ets.match_object/2", time_before, ":ets.select/2", time_after)
end
defp benchmark_tab2list_vs_select_all do
IO.puts("\n" <> "-" |> String.duplicate(70))
IO.puts("Test 3: Get all records (tab2list vs select)")
IO.puts("-" |> String.duplicate(70))
match_spec = [{:"$_", [], [:"$_"]}]
# Before: Using :ets.tab2list/1
{time_before, _} =
:timer.tc(fn ->
for _ <- 1..(@num_iterations |> div(10)) do
:ets.tab2list(@table)
end
end)
# After: Using :ets.select/2 with match spec
{time_after, _} =
:timer.tc(fn ->
for _ <- 1..(@num_iterations |> div(10)) do
:ets.select(@table, match_spec)
end
end)
print_results(":ets.tab2list/1", time_before, ":ets.select/2", time_after)
end
defp benchmark_with_compiled_specs do
IO.puts("Reusing compiled match specs for repeated queries...")
pattern1 = {:key, :"$1", :"$2", 42}
match_spec1 = [{pattern1, [], [:"$1", :"$2"]}]
compiled1 = :ets.match_spec_compile(match_spec1)
pattern2 = {:key, :_, :_, 42}
match_spec2 = [{pattern2, [], [:"$_"]}]
compiled2 = :ets.match_spec_compile(match_spec2)
match_spec3 = [{:"$_", [], [:"$_"]}]
compiled3 = :ets.match_spec_compile(match_spec3)
# Benchmark 1: match vs compiled select
{time_match, _} =
:timer.tc(fn ->
for _ <- 1..@num_iterations do
:ets.match(@table, pattern1)
end
end)
{time_select, _} =
:timer.tc(fn ->
for _ <- 1..@num_iterations do
:ets.select(@table, match_spec1)
end
end)
{time_compiled, _} =
:timer.tc(fn ->
for _ <- 1..@num_iterations do
:ets.select(@table, compiled1)
end
end)
IO.puts("\nTest 1: Find records where 4th element == 42")
print_results_3ways(":ets.match/2", time_match, ":ets.select/2", time_select, "compiled", time_compiled)
# Benchmark 2: match_object vs compiled select
{time_match_obj, _} =
:timer.tc(fn ->
for _ <- 1..@num_iterations do
:ets.match_object(@table, pattern2)
end
end)
{time_select2, _} =
:timer.tc(fn ->
for _ <- 1..@num_iterations do
:ets.select(@table, match_spec2)
end
end)
{time_compiled2, _} =
:timer.tc(fn ->
for _ <- 1..@num_iterations do
:ets.select(@table, compiled2)
end
end)
IO.puts("\nTest 2: match_object - find records with 4th element == 42")
print_results_3ways(":ets.match_object/2", time_match_obj, ":ets.select/2", time_select2, "compiled", time_compiled2)
# Benchmark 3: tab2list vs compiled select all
{time_tab2list, _} =
:timer.tc(fn ->
for _ <- 1..(@num_iterations |> div(10)) do
:ets.tab2list(@table)
end
end)
{time_select3, _} =
:timer.tc(fn ->
for _ <- 1..(@num_iterations |> div(10)) do
:ets.select(@table, match_spec3)
end
end)
{time_compiled3, _} =
:timer.tc(fn ->
for _ <- 1..(@num_iterations |> div(10)) do
:ets.select(@table, compiled3)
end
end)
IO.puts("\nTest 3: Get all records")
print_results_3ways(":ets.tab2list/1", time_tab2list, ":ets.select/2", time_select3, "compiled", time_compiled3)
end
defp print_results(before_label, before_time, after_label, after_time) do
IO.puts("\n#{before_label}: #{format_time(before_time)}")
IO.puts("#{after_label}: #{format_time(after_time)}")
improvement = (before_time - after_time) / before_time * 100
IO.puts("Improvement: #{Float.round(improvement, 2)}% faster with #{after_label}")
end
defp print_results_3ways(label1, time1, label2, time2, label3, time3) do
IO.puts("\n#{label1}: #{format_time(time1)}")
IO.puts("#{label2}: #{format_time(time2)}")
IO.puts("#{label3}: #{format_time(time3)}")
improvement2 = (time1 - time2) / time1 * 100
improvement3 = (time1 - time3) / time1 * 100
IO.puts("\nImprovement vs #{label2}: #{Float.round(improvement2, 2)}%")
IO.puts("Improvement vs #{label3}: #{Float.round(improvement3, 2)}%")
end
defp format_time(microseconds) do
cond do
microseconds >= 1_000_000 -> "#{Float.round(microseconds / 1_000_000, 2)}s"
microseconds >= 1_000 -> "#{Float.round(microseconds / 1_000, 2)}ms"
true -> "#{microseconds}µs"
end <> " (#{round(microseconds)} µs total)"
end
end