-
Notifications
You must be signed in to change notification settings - Fork 20
Use constant-time Variant type lookup in RowBinary decode #370
Description
Problem
Variant decoding picks the nested type with Enum.at/2 for every decoded value:
{:variant, variant_types} -> case bin do <<255, bin::bytes>> -> decode_rows(types_rest, bin, [nil | row], rows, types) <<variant_type_index::8, bin::bytes>> -> variant_type = Enum.at(variant_types, variant_type_index) decode_rows([variant_type | types_rest], bin, row, rows, types) end
This is a small cost compared with Variant encoding's exception cliff, but it is repeated for every non-null variant value and is easy to avoid.
Benchmark
Environment:
commit: 5c9244a
macOS, Apple M2, 8 GB RAM
Elixir 1.19.5, Erlang/OTP 28.3, JIT enabled
Microbenchmark code:
list = [:string, :u64, {:array, :u64}, :boolean, :f64] tuple = List.to_tuple(list) indices = Enum.map(1..5_000_000, &rem(&1, tuple_size(tuple))) Benchee.run( %{ "Enum.at variant type" => fn -> Enum.reduce(indices, nil, fn idx, _ -> Enum.at(list, idx) end) end, "tuple element variant type" => fn -> Enum.reduce(indices, nil, fn idx, _ -> :erlang.element(idx + 1, tuple) end) end }, warmup: 1, time: 2 )
Results:
Name ips average
tuple element variant type 0.25 4.06 s
Enum.at variant type 0.23 4.42 s
Tuple lookup: 1.09x faster over 5M lookups
This benchmark was run from mix run -e, so the absolute numbers include evaluated-function overhead. The relative result is still enough to classify this as a low-risk cleanup, not the main RowBinary bottleneck.
Suggested direction
Store decoding-only variant types as a tuple in decoding_type/1, then use :erlang.element(variant_type_index + 1, variant_types_tuple) during decode.
Implementation should preserve current behavior for invalid variant indexes or replace it with a clearer ArgumentError.
Tests to add:
- existing variant RowBinary decode tests still pass;
- invalid variant type indexes fail clearly;
nilvariant marker255remains unchanged.