Skip to content

Fixes for low hanging performance "bugs" (AI driven) - #358

Open
KristofferC wants to merge 8 commits into
masterfrom
kc/perf
Open

Fixes for low hanging performance "bugs" (AI driven)#358
KristofferC wants to merge 8 commits into
masterfrom
kc/perf

Conversation

@KristofferC

Copy link
Copy Markdown
Collaborator

This PR consists of a set of commits, each quite small, that each has a significant impact on either allocations or runtime. Each commits has a corresponding benchmark added and shows the change in performance. A table of the impact of each commit is shown below. Note that this PR was mostly generated via AI but is still opened in the hope that it nonetheless is useful.

   Benchmark                                       Time (before → after)    Speedup                Memory    Allocations
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  ━━━━━━━━━━━━━━━━━━━━━━━  ━━━━━━━━━  ━━━━━━━━━━━━━━━━━━━━  ━━━━━━━━━━━━━
   sortperm, 6 Float64 columns × 100k rows              12.87 → 12.34 ms      1.04×       2.87 → 1.62 MiB    34,473 → 11
  ──────────────────────────────────────────────  ───────────────────────  ─────────  ────────────────────  ─────────────
   sortperm, 6 Int columns × 100k rows                    2.36 → 2.21 ms      1.07×    2.06 MiB → 961 KiB    17,504 → 12
  ──────────────────────────────────────────────  ───────────────────────  ─────────  ────────────────────  ─────────────
   Append 1M rows × 4 columns                             9.56 → 8.03 ms      1.19×    115.14 → 30.56 MiB        120 → 8
  ──────────────────────────────────────────────  ───────────────────────  ─────────  ────────────────────  ─────────────
   Construct 128-column homogeneous StructArray          4.24 μs → 56 ns      75.5×          4.61 KiB → 0         13 → 0
  ──────────────────────────────────────────────  ───────────────────────  ─────────  ────────────────────  ─────────────
   Construct 128-column named StructArray                3.87 μs → 79 ns      48.7×          3.36 KiB → 0          5 → 0
  ──────────────────────────────────────────────  ───────────────────────  ─────────  ────────────────────  ─────────────
   Group 100k rows across 64 columns                  365.09 ms → 728 μs       501×        355.32 MiB → 0    698,520 → 0
  ──────────────────────────────────────────────  ───────────────────────  ─────────  ────────────────────  ─────────────
   Index a row from 128 columns                          2.51 μs → 55 ns      45.8×          4.16 KiB → 0        131 → 0

Recursive lexicographic refinement allocated a fresh merge-sort scratch vector for every subgroup. Thread one workspace through the recursion instead.

MWE: benchmark/sortperm_recursive.jl (100,000 rows, 6 Float64 columns, cardinality 16; Julia 1.12.6, minimum of 20 samples).

Before: 12.874 ms, 2.87 MiB, 34,473 allocations

After:  12.340 ms, 1.62 MiB, 11 allocations
Integer-key refinement allocated and filled a new histogram for every subgroup. Keep one histogram alongside the merge-sort scratch vector and resize/fill it in place.

MWE: benchmark/sortperm_recursive.jl (100,000 rows, 6 Int columns, cardinality 16; Julia 1.12.6, minimum of 40 samples; baseline is the preceding commit).

Before: 2.356 ms, 2.06 MiB, 17,504 allocations

After:  2.206 ms, 960.53 KiB, 12 allocations
The generic Tables fallback pushed rows one at a time without reserving component capacity. Apply sizehint! when the iterator reports a length, while retaining the existing row-wise conversion path.

MWE: benchmark/append_rows.jl (1,000,000 NamedTuple rows, 4 columns; Julia 1.12.6, minimum of 10 samples).

Before: 9.555 ms, 115.14 MiB, 120 allocations

After:  8.034 ms, 30.56 MiB, 8 allocations
Shape validation mapped axes over every component before reducing the results. For homogeneous concrete component tuples, validate in a type-stable loop and return early on a mismatch.

MWE: benchmark/wide_constructor.jl (128 Vector{Float64} columns; Julia 1.12.6, minimum BenchmarkTools estimate).

Before: 1.983 us, 1.23 KiB, 3 allocations

After:  56.233 ns, 0 bytes, 0 allocations
Wide homogeneous component tuples already encode their shared array type. Derive the row tuple type directly instead of mapping eltype and splatting a large temporary tuple; retain the fallback for abstract and heterogeneous components.

MWE: benchmark/wide_constructor.jl (inferred 128-column Vector{Float64} StructArray; Julia 1.12.6, minimum BenchmarkTools estimate; baseline is the preceding commit).

Before: 4.244 us, 4.61 KiB, 13 allocations

After:  56.233 ns, 0 bytes, 0 allocations
Recursive Tuple-tail comparison crosses Julia's specialization limit for wide StructArrays and boxes each comparison. Compare homogeneous concrete component tuples with a type-stable loop while retaining recursive dispatch for heterogeneous tuples.

MWE: benchmark/group_wide.jl (100,000 rows, 64 Int columns, cardinality 16; Julia 1.12.6, minimum of 10 samples).

Before: 365.092 ms, 355.32 MiB, 698,520 allocations

After:  728.416 us, 0 bytes, 0 allocations
Scalar getindex already materializes a tuple of component values. Tuple and NamedTuple row types can construct directly from it, avoiding the specialization cliff from a wide vararg splat while preserving their conversions.

MWE: benchmark/wide_getindex.jl (128 Float64 columns, 100 rows; Julia 1.12.6, minimum BenchmarkTools estimate).

Before: 2.509 us, 4.16 KiB, 131 allocations

After:  54.795 ns, 0 bytes, 0 allocations
NamedTuple component collections hit the same wide-tuple specialization limit during axes validation. Above that limit, use the encoded homogeneous value-tuple type to select the allocation-free loop; retain the unrolled path for narrow static arrays and the generic fallback for heterogeneous or abstract fields.

MWE: benchmark/wide_constructor.jl (inferred 128-column named StructArray; Julia 1.12.6, minimum BenchmarkTools estimate).

Before: 3.869 us, 3.36 KiB, 5 allocations

After:  79.459 ns, 0 bytes, 0 allocations
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