Skip to content

perf: optimize map_sort for multi-entry string maps (up to 3x faster) - #5901

Merged
viirya merged 2 commits into
apache:mainfrom
viirya:codex/map-sort-index-allocation
Sep 15, 2026
Merged

viirya merged 2 commits into
apache:mainfrom
viirya:codex/map-sort-index-allocation

Conversation

@viirya

@viirya viirya commented Sep 13, 2026

Copy link
Copy Markdown
Member

Which issue does this PR close?

Closes #5900. Part of #5818; follows the matched normalization/hash benchmarks in #5822 and singleton optimization in #5887.

Rationale for this change

Normalizing multi-entry map<string,int> batches slices the key array and allocates Arrow sorting
indices and prefix tuples for every row. Reuse the prefix-tuple buffer across rows to reduce this
allocation overhead while preserving the exact permutation.

What changes are included in this PR?

  • Specialize multi-entry Utf8 keys with Int32 values, reusing (u32 index, u32 prefix, u64 length)
    scratch storage. Keep Arrow's comparator and tuple layout, including equal-key permutations.
    Scratch capacity grows with the largest row. The existing global index append and single take remain.
  • Preserve perf: optimize map_sort singleton normalization (18x faster) #5887's singleton eligibility check, batch dispatch and direct index append. Insert the
    multi-entry path into its const-generic helper; other key/value types retain Arrow validation.
  • Fill rebased offsets in bulk for all-empty visible batches while retaining map reconstruction.
  • Add wide/null/Unicode int-value fixtures and four tests for exact duplicate-key permutations,
    mixed singleton/multi-entry slices, physical entries under null maps, unsupported keys and empty
    visible slices. Retain all upstream tests and the existing null/schema/sorted-flag semantics.

How are these changes tested?

  • All 16 map-sort tests passed (12 upstream tests retained unchanged, 4 added).
  • Workspace/all-target Clippy with -D warnings passed.
  • make format PROFILES=-Pspark-4.0 passed, using the existing profile that provides SemanticDB.
  • Release workspace/native-library build passed.
  • Fresh upstream and integrated release cohorts each completed all 122 cases. All 18 full-run
    slowdown flags with CI lower bound above +1% received independent paired measurements, as did
    two wide-map wins. Two fallback cases still above +2% received reverse-order pairs.

Matched 2–10-entry forward normalization improved about 66% (3x); matched 2–50-entry cases improved
28–39% in the full cohort and 33–39% in fresh pairs. No larger slowdown persisted across the
confirmations. The int-key 16-entry fallback was +3.50% in a fresh pair and +1.99% in reverse order;
small effects remain uncertain under observed process drift. The int-key 4-entry case fell from
+6.09% to +0.11% in reverse order. Initial singleton +8–9% flags did not reproduce: fresh matched
singleton pairs were -0.08% and +1.72%. These are microbenchmarks, not end-to-end Spark measurements.

Measurement cohort and limits

Base: 4abfd95114d61ad454f9ee269be1615f469f24e2, including merged singleton PR #5887 (ca223a8d4).
All 122 baseline measurements completed before the integrated production source was applied.
Both cohorts used byte-identical benchmark files, dependency manifests and lockfile.

Apple M4 Max, 64 GB, Rust 1.97.1, release/thin LTO/codegen-units=1, no RUSTFLAGS override.
Full cohorts: 50 samples, 1 s warmup, 2 s target measurement (Criterion extends long cases).
Independent pairs: 100 samples, 3 s warmup, 5 s target measurement. No heavy build or benchmark ran
concurrently. Narrow within-process CIs do not capture desktop/background and cross-process drift.
All unfavorable original measurements are retained alongside confirmations.

Inputs and correctness checks are untimed. Normalization output allocation/drop and combined
struct reconstruction/drop are timed. Hash-buffer allocation is untimed and seed reset is timed.
Hash-only forward/reversed cases use the same normalized input as repeated controls. Each batch
has 8,192 visible rows; physical entries under null maps count toward entry rates. No phase subtraction
is used. Hashing implementation and nested-hashing defaults are unchanged.

Complete integrated comparison (122 cases)

Time is ms per 8,192-row batch, with 95% CI; unsupported-key and buffer-reset cases are per call.
The change column is Criterion's cross-process mean estimate; confirmations follow below.

Case Baseline ms (95% CI) Integrated ms (95% CI) Change % (95% CI)
matched_maps/hash_buffer_seed_reset 0.000297 [0.000290, 0.000303] 0.000263 [0.000256, 0.000270] -12.62 [-15.17, -9.89]
matched_maps/hash_only/map/0/forward 0.004978 [0.004940, 0.005014] 0.004620 [0.004580, 0.004656] -7.50 [-8.55, -6.38]
matched_maps/hash_only/map/0/reversed 0.004934 [0.004895, 0.004971] 0.004646 [0.004607, 0.004691] -5.12 [-5.98, -4.21]
matched_maps/hash_only/map/1/forward 0.032509 [0.032211, 0.032795] 0.032108 [0.031790, 0.032468] -1.43 [-3.03, 0.17]
matched_maps/hash_only/map/1/reversed 0.030791 [0.030709, 0.030898] 0.030644 [0.030586, 0.030703] -2.11 [-3.12, -1.15]
matched_maps/hash_only/map/10/forward 0.352903 [0.347282, 0.359123] 0.341396 [0.337814, 0.345680] -5.95 [-7.30, -4.58]
matched_maps/hash_only/map/10/reversed 0.333073 [0.331511, 0.334519] 0.360368 [0.355854, 0.364416] 7.57 [6.15, 8.80]
matched_maps/hash_only/map/50/forward 2.165835 [2.150633, 2.181090] 2.248646 [2.216507, 2.280922] 3.10 [1.67, 4.71]
matched_maps/hash_only/map/50/reversed 2.166743 [2.154227, 2.179776] 2.235745 [2.205941, 2.273575] 5.81 [3.76, 7.92]
matched_maps/hash_only/struct_map_int/0/forward 0.012248 [0.012107, 0.012398] 0.011977 [0.011887, 0.012084] -0.26 [-1.61, 1.14]
matched_maps/hash_only/struct_map_int/0/reversed 0.012756 [0.012627, 0.012881] 0.012039 [0.011928, 0.012167] -6.78 [-7.75, -5.67]
matched_maps/hash_only/struct_map_int/1/forward 0.038521 [0.038220, 0.038885] 0.037884 [0.037690, 0.038159] -2.68 [-3.61, -1.70]
matched_maps/hash_only/struct_map_int/1/reversed 0.037590 [0.037378, 0.037797] 0.039951 [0.039543, 0.040325] 4.79 [3.61, 5.98]
matched_maps/hash_only/struct_map_int/10/forward 0.364487 [0.361603, 0.367084] 0.367201 [0.363984, 0.370435] -1.40 [-2.65, -0.07]
matched_maps/hash_only/struct_map_int/10/reversed 0.343979 [0.340997, 0.346947] 0.357271 [0.353344, 0.361585] 4.19 [3.00, 5.47]
matched_maps/hash_only/struct_map_int/50/forward 2.213356 [2.195088, 2.232738] 2.211575 [2.179998, 2.248343] -0.17 [-1.79, 1.33]
matched_maps/hash_only/struct_map_int/50/reversed 2.139081 [2.130019, 2.148116] 2.243743 [2.219535, 2.271584] 7.77 [6.19, 9.52]
matched_maps/normalize_hash/map/0/forward 0.011930 [0.011872, 0.011976] 0.007811 [0.007772, 0.007854] -34.53 [-34.97, -34.11]
matched_maps/normalize_hash/map/0/reversed 0.011863 [0.011806, 0.011928] 0.007999 [0.007914, 0.008111] -30.74 [-31.69, -29.79]
matched_maps/normalize_hash/map/1/forward 0.082434 [0.081518, 0.083587] 0.077613 [0.076782, 0.078749] -2.62 [-4.38, -0.83]
matched_maps/normalize_hash/map/1/reversed 0.097234 [0.096097, 0.098288] 0.074317 [0.074074, 0.074615] -23.75 [-24.44, -23.06]
matched_maps/normalize_hash/map/10/forward 2.082057 [2.070464, 2.093390] 0.750409 [0.747988, 0.752879] -63.81 [-64.06, -63.56]
matched_maps/normalize_hash/map/10/reversed 2.429893 [2.407186, 2.453356] 1.151531 [1.145534, 1.158021] -53.15 [-53.53, -52.74]
matched_maps/normalize_hash/map/50/forward 5.477007 [5.436108, 5.520821] 3.710618 [3.699270, 3.722581] -32.25 [-32.82, -31.68]
matched_maps/normalize_hash/map/50/reversed 6.370159 [6.324398, 6.414206] 4.374540 [4.363505, 4.385494] -31.33 [-31.83, -30.80]
matched_maps/normalize_hash/struct_map_int/0/forward 0.020538 [0.020326, 0.020813] 0.015627 [0.015430, 0.015867] -25.25 [-26.27, -24.23]
matched_maps/normalize_hash/struct_map_int/0/reversed 0.021126 [0.020736, 0.021448] 0.016097 [0.015938, 0.016238] -23.01 [-24.11, -21.86]
matched_maps/normalize_hash/struct_map_int/1/forward 0.088329 [0.088211, 0.088437] 0.082637 [0.082505, 0.082763] -6.48 [-6.68, -6.26]
matched_maps/normalize_hash/struct_map_int/1/reversed 0.101194 [0.099879, 0.102577] 0.081745 [0.081589, 0.081884] -20.14 [-20.98, -19.31]
matched_maps/normalize_hash/struct_map_int/10/forward 2.104292 [2.087008, 2.120388] 0.762873 [0.760500, 0.765230] -63.93 [-64.21, -63.66]
matched_maps/normalize_hash/struct_map_int/10/reversed 2.343765 [2.333059, 2.357974] 1.213060 [1.199996, 1.230424] -50.03 [-50.75, -49.33]
matched_maps/normalize_hash/struct_map_int/50/forward 5.573334 [5.520074, 5.625929] 3.727349 [3.714379, 3.742165] -33.12 [-33.80, -32.43]
matched_maps/normalize_hash/struct_map_int/50/reversed 6.094283 [6.059158, 6.132586] 4.398988 [4.384451, 4.413022] -27.82 [-28.32, -27.34]
matched_maps/normalize_only/map/0/forward 0.006960 [0.006917, 0.007002] 0.003103 [0.003052, 0.003154] -55.86 [-56.29, -55.45]
matched_maps/normalize_only/map/0/reversed 0.006939 [0.006907, 0.006970] 0.003285 [0.003255, 0.003321] -52.50 [-53.11, -51.90]
matched_maps/normalize_only/map/1/forward 0.047532 [0.047305, 0.047784] 0.051704 [0.050974, 0.052802] 8.58 [7.03, 10.66]
matched_maps/normalize_only/map/1/reversed 0.046676 [0.046159, 0.047362] 0.051078 [0.050758, 0.051435] 9.46 [8.08, 10.77]
matched_maps/normalize_only/map/10/forward 1.457805 [1.449019, 1.470317] 0.492988 [0.489871, 0.496710] -66.20 [-66.59, -65.69]
matched_maps/normalize_only/map/10/reversed 1.859864 [1.852003, 1.868389] 0.903327 [0.899307, 0.907138] -51.74 [-52.04, -51.44]
matched_maps/normalize_only/map/50/forward 3.070098 [3.018114, 3.127601] 1.835687 [1.820745, 1.852364] -38.61 [-39.61, -37.60]
matched_maps/normalize_only/map/50/reversed 3.588305 [3.580530, 3.596033] 2.556989 [2.532958, 2.579918] -28.33 [-28.84, -27.84]
multi_entry_int_maps/hash_only/map/mixed_dense_null 0.449289 [0.445058, 0.454586] 0.402165 [0.400495, 0.403981] -9.89 [-10.52, -9.29]
multi_entry_int_maps/hash_only/map/wide_dense_null 0.617394 [0.614634, 0.620249] 0.556214 [0.553946, 0.558295] -9.70 [-10.19, -9.20]
multi_entry_int_maps/hash_only/map/wide_long_unicode 71.502781 [71.167283, 71.850570] 64.300277 [64.174908, 64.432286] -10.07 [-10.53, -9.61]
multi_entry_int_maps/hash_only/map/wide_long_unicode_dense_null 36.097994 [35.900216, 36.308762] 32.683120 [32.545423, 32.845109] -9.46 [-10.12, -8.78]
multi_entry_int_maps/hash_only/map/wide_no_null 1.700726 [1.686073, 1.719461] 1.486960 [1.482161, 1.492874] -15.00 [-16.07, -13.91]
multi_entry_int_maps/hash_only/map/wide_sparse_null 1.727546 [1.708216, 1.747878] 1.511608 [1.504700, 1.518844] -14.33 [-15.25, -13.38]
multi_entry_int_maps/hash_only/struct_map_int/mixed_dense_null 0.452555 [0.449866, 0.455648] 0.409470 [0.407853, 0.411273] -9.53 [-10.37, -8.73]
multi_entry_int_maps/hash_only/struct_map_int/wide_dense_null 0.620534 [0.616507, 0.624194] 0.566632 [0.564718, 0.568522] -9.13 [-9.75, -8.48]
multi_entry_int_maps/hash_only/struct_map_int/wide_long_unicode 73.218033 [72.742804, 73.717389] 64.374067 [64.246127, 64.511935] -12.08 [-12.70, -11.47]
multi_entry_int_maps/hash_only/struct_map_int/wide_long_unicode_dense_null 35.743625 [35.499035, 35.991249] 32.728699 [32.555951, 32.932193] -8.43 [-9.27, -7.61]
multi_entry_int_maps/hash_only/struct_map_int/wide_no_null 1.686136 [1.670163, 1.702652] 1.496503 [1.490776, 1.504781] -9.63 [-10.48, -8.76]
multi_entry_int_maps/hash_only/struct_map_int/wide_sparse_null 1.724639 [1.709869, 1.739987] 1.516817 [1.512161, 1.520917] -11.60 [-12.12, -11.09]
multi_entry_int_maps/normalize_hash/map/mixed_dense_null 1.677325 [1.659271, 1.695524] 0.879415 [0.876691, 0.882169] -48.38 [-48.97, -47.78]
multi_entry_int_maps/normalize_hash/map/wide_dense_null 3.492575 [3.463381, 3.522336] 1.770534 [1.764865, 1.775920] -49.29 [-49.75, -48.83]
multi_entry_int_maps/normalize_hash/map/wide_long_unicode 98.993755 [97.956763, 100.109657] 85.089966 [84.857972, 85.343834] -14.05 [-15.03, -13.11]
multi_entry_int_maps/normalize_hash/map/wide_long_unicode_dense_null 62.323938 [61.782437, 62.892852] 53.181282 [52.967561, 53.425551] -14.67 [-15.52, -13.85]
multi_entry_int_maps/normalize_hash/map/wide_no_null 4.377918 [4.356941, 4.400787] 2.601935 [2.594394, 2.610129] -40.75 [-41.14, -40.38]
multi_entry_int_maps/normalize_hash/map/wide_sparse_null 4.341314 [4.327330, 4.355079] 2.719642 [2.711363, 2.728375] -37.30 [-37.57, -37.03]
multi_entry_int_maps/normalize_hash/struct_map_int/mixed_dense_null 1.591679 [1.577916, 1.607271] 0.886168 [0.882572, 0.889973] -44.67 [-45.20, -44.15]
multi_entry_int_maps/normalize_hash/struct_map_int/wide_dense_null 3.350707 [3.333561, 3.369101] 1.778045 [1.772774, 1.783062] -46.87 [-47.19, -46.54]
multi_entry_int_maps/normalize_hash/struct_map_int/wide_long_unicode 99.514669 [98.494585, 100.582908] 84.849294 [84.610544, 85.119287] -14.74 [-15.68, -13.81]
multi_entry_int_maps/normalize_hash/struct_map_int/wide_long_unicode_dense_null 62.983163 [62.488513, 63.466262] 53.196420 [53.022391, 53.392616] -15.54 [-16.25, -14.80]
multi_entry_int_maps/normalize_hash/struct_map_int/wide_no_null 4.241714 [4.225298, 4.257163] 2.605669 [2.598808, 2.612900] -38.36 [-38.64, -38.06]
multi_entry_int_maps/normalize_hash/struct_map_int/wide_sparse_null 4.326263 [4.307835, 4.343826] 2.755664 [2.742788, 2.768494] -36.54 [-36.86, -36.19]
multi_entry_int_maps/normalize_only/map/mixed_dense_null 1.049101 [1.040979, 1.058181] 0.499449 [0.495715, 0.503455] -52.68 [-53.16, -52.20]
multi_entry_int_maps/normalize_only/map/wide_dense_null 2.553617 [2.516866, 2.588511] 1.243265 [1.226862, 1.257867] -51.60 [-52.10, -51.08]
multi_entry_int_maps/normalize_only/map/wide_long_unicode 22.638445 [22.522588, 22.776090] 21.227017 [21.140163, 21.322381] -6.23 [-6.92, -5.59]
multi_entry_int_maps/normalize_only/map/wide_long_unicode_dense_null 22.743612 [22.675809, 22.808203] 21.271406 [21.188210, 21.359706] -6.47 [-6.94, -6.00]
multi_entry_int_maps/normalize_only/map/wide_no_null 2.353425 [2.340957, 2.366710] 1.125002 [1.116061, 1.136612] -51.46 [-51.85, -51.11]
multi_entry_int_maps/normalize_only/map/wide_sparse_null 2.451406 [2.431075, 2.468949] 1.216252 [1.213643, 1.219300] -50.40 [-50.68, -50.10]
regression_maps/hash_only/map/mixed_dense_null 0.573734 [0.571174, 0.576098] 0.515212 [0.513514, 0.517152] -9.99 [-10.50, -9.45]
regression_maps/hash_only/map/mixed_long_unicode_dense_null 39.475333 [39.143922, 39.776078] 32.243070 [32.172971, 32.317813] -18.32 [-18.96, -17.61]
regression_maps/hash_only/map/mixed_no_null 0.955752 [0.951690, 0.959291] 0.861644 [0.858801, 0.864760] -8.97 [-9.46, -8.47]
regression_maps/hash_only/map/mixed_sparse_null 0.985686 [0.977372, 0.994366] 0.881138 [0.878176, 0.883949] -10.12 [-11.09, -9.17]
regression_maps/hash_only/map/singleton_dense_null 0.012874 [0.012810, 0.012933] 0.012009 [0.011962, 0.012059] -7.20 [-7.97, -6.48]
regression_maps/hash_only/map/singleton_long_unicode 5.413250 [5.360072, 5.466480] 4.777156 [4.749153, 4.808462] -11.75 [-12.75, -10.70]
regression_maps/hash_only/map/singleton_no_null 0.034010 [0.033710, 0.034319] 0.031366 [0.031325, 0.031407] -7.47 [-8.05, -6.91]
regression_maps/hash_only/map/singleton_sparse_null 0.042623 [0.042278, 0.042941] 0.038719 [0.038635, 0.038820] -7.62 [-8.26, -7.00]
regression_maps/hash_only/struct_map_int/mixed_dense_null 0.620130 [0.612510, 0.627184] 0.525686 [0.523824, 0.527695] -13.82 [-15.11, -12.53]
regression_maps/hash_only/struct_map_int/mixed_long_unicode_dense_null 38.143917 [37.726545, 38.572804] 32.247516 [32.174597, 32.324397] -15.46 [-16.42, -14.50]
regression_maps/hash_only/struct_map_int/mixed_no_null 0.961241 [0.949367, 0.975726] 0.871356 [0.869152, 0.873629] -10.16 [-10.91, -9.47]
regression_maps/hash_only/struct_map_int/mixed_sparse_null 1.002809 [0.995061, 1.011914] 0.888110 [0.883873, 0.893059] -13.26 [-14.24, -12.28]
regression_maps/hash_only/struct_map_int/singleton_dense_null 0.021009 [0.020927, 0.021112] 0.019654 [0.019601, 0.019726] -6.74 [-7.34, -6.05]
regression_maps/hash_only/struct_map_int/singleton_long_unicode 5.608737 [5.546328, 5.672366] 4.744444 [4.718851, 4.775973] -15.41 [-16.49, -14.30]
regression_maps/hash_only/struct_map_int/singleton_no_null 0.042110 [0.041828, 0.042441] 0.037817 [0.037783, 0.037849] -10.15 [-10.58, -9.73]
regression_maps/hash_only/struct_map_int/singleton_sparse_null 0.050949 [0.050603, 0.051328] 0.044919 [0.044813, 0.045050] -11.15 [-11.72, -10.64]
regression_maps/normalize_hash/map/mixed_dense_null 1.932876 [1.913055, 1.953200] 1.757228 [1.751421, 1.763408] -10.82 [-11.78, -9.83]
regression_maps/normalize_hash/map/mixed_long_unicode_dense_null 46.546517 [46.249115, 46.851328] 41.980622 [41.879802, 42.081515] -9.81 [-10.44, -9.19]
regression_maps/normalize_hash/map/mixed_no_null 2.394949 [2.377064, 2.413906] 2.086912 [2.078818, 2.096521] -12.62 [-13.45, -11.71]
regression_maps/normalize_hash/map/mixed_sparse_null 2.752169 [2.717999, 2.788445] 2.406924 [2.398357, 2.416604] -12.14 [-13.01, -11.28]
regression_maps/normalize_hash/map/singleton_dense_null 0.084054 [0.083345, 0.084878] 0.076710 [0.076291, 0.077178] -9.23 [-10.47, -7.91]
regression_maps/normalize_hash/map/singleton_long_unicode 5.707849 [5.679360, 5.739430] 5.162268 [5.131169, 5.197831] -9.56 [-10.29, -8.77]
regression_maps/normalize_hash/map/singleton_no_null 0.098735 [0.097811, 0.099723] 0.093007 [0.092184, 0.093867] -6.39 [-7.46, -5.38]
regression_maps/normalize_hash/map/singleton_sparse_null 0.137767 [0.136914, 0.138736] 0.127877 [0.127098, 0.128788] -7.30 [-7.89, -6.66]
regression_maps/normalize_hash/struct_map_int/mixed_dense_null 1.936508 [1.919340, 1.955266] 1.774254 [1.763450, 1.785962] -8.48 [-9.69, -7.27]
regression_maps/normalize_hash/struct_map_int/mixed_long_unicode_dense_null 47.735977 [47.309989, 48.159968] 41.824211 [41.712403, 41.938327] -12.38 [-13.20, -11.57]
regression_maps/normalize_hash/struct_map_int/mixed_no_null 2.393451 [2.370252, 2.415322] 2.097543 [2.089294, 2.106787] -13.07 [-13.94, -12.20]
regression_maps/normalize_hash/struct_map_int/mixed_sparse_null 2.663930 [2.653059, 2.675174] 2.441886 [2.417599, 2.468089] -8.77 [-9.29, -8.25]
regression_maps/normalize_hash/struct_map_int/singleton_dense_null 0.093762 [0.092784, 0.094827] 0.084679 [0.084273, 0.085129] -10.34 [-11.37, -9.28]
regression_maps/normalize_hash/struct_map_int/singleton_long_unicode 5.848648 [5.796031, 5.900309] 5.130805 [5.112896, 5.149592] -12.27 [-13.11, -11.40]
regression_maps/normalize_hash/struct_map_int/singleton_no_null 0.107473 [0.106733, 0.108489] 0.098741 [0.098308, 0.099330] -9.20 [-10.45, -7.81]
regression_maps/normalize_hash/struct_map_int/singleton_sparse_null 0.153384 [0.151849, 0.154817] 0.134456 [0.134169, 0.134739] -10.76 [-11.74, -9.79]
regression_maps/normalize_only/map/mixed_dense_null 1.305613 [1.298508, 1.314473] 1.364932 [1.346962, 1.381698] 1.24 [-0.01, 2.62]
regression_maps/normalize_only/map/mixed_long_unicode_dense_null 10.100908 [10.057039, 10.149230] 10.152993 [10.067440, 10.245531] 0.52 [-0.41, 1.51]
regression_maps/normalize_only/map/mixed_no_null 1.238506 [1.233877, 1.244083] 1.298261 [1.287412, 1.308777] 4.77 [3.68, 5.84]
regression_maps/normalize_only/map/mixed_sparse_null 1.670553 [1.632795, 1.702511] 1.619063 [1.603978, 1.633042] -1.04 [-2.56, 0.39]
regression_maps/normalize_only/map/singleton_dense_null 0.065474 [0.065122, 0.065870] 0.068979 [0.068489, 0.069474] 5.41 [4.68, 6.14]
regression_maps/normalize_only/map/singleton_long_unicode 0.395832 [0.393017, 0.398785] 0.415664 [0.411426, 0.420134] 6.29 [5.03, 7.58]
regression_maps/normalize_only/map/singleton_no_null 0.061801 [0.061540, 0.062139] 0.067296 [0.066293, 0.068235] 6.76 [5.52, 7.97]
regression_maps/normalize_only/map/singleton_sparse_null 0.093908 [0.092231, 0.095821] 0.097075 [0.096292, 0.097936] 6.11 [4.79, 7.32]
spark_map_sort/int_keys/0 0.006962 [0.006902, 0.007018] 0.003254 [0.003215, 0.003289] -53.25 [-53.78, -52.73]
spark_map_sort/int_keys/1 0.017960 [0.017881, 0.018045] 0.019514 [0.019332, 0.019680] 8.85 [8.08, 9.60]
spark_map_sort/int_keys/16 1.416743 [1.413385, 1.420491] 1.538763 [1.525474, 1.552531] 8.02 [7.03, 8.95]
spark_map_sort/int_keys/4 0.933755 [0.931875, 0.935690] 1.010855 [1.005474, 1.016571] 9.34 [8.70, 9.98]
spark_map_sort/int_keys/64 1.868838 [1.864112, 1.873809] 2.045926 [2.001160, 2.093124] 10.00 [8.59, 11.47]
spark_map_sort/string_keys/0 0.006997 [0.006971, 0.007029] 0.003286 [0.003268, 0.003302] -53.16 [-53.53, -52.78]
spark_map_sort/string_keys/1 0.035552 [0.035431, 0.035720] 0.040330 [0.039887, 0.040847] 12.93 [11.97, 13.89]
spark_map_sort/string_keys/16 3.999255 [3.988036, 4.012051] 3.193286 [3.172456, 3.213719] -20.15 [-20.70, -19.60]
spark_map_sort/string_keys/4 1.126407 [1.120661, 1.133037] 0.306869 [0.302937, 0.311142] -73.38 [-73.72, -73.04]
spark_map_sort/string_keys/64 4.220635 [4.200750, 4.243675] 3.287736 [3.268866, 3.306038] -22.10 [-22.74, -21.54]
spark_map_sort/unsupported_singleton_struct_key 0.000450 [0.000448, 0.000452] 0.000450 [0.000447, 0.000454] 0.22 [-0.68, 1.17]
Fresh independent pairs and reverse-order confirmations
Case Fresh baseline ms (95% CI) Fresh integrated ms (95% CI) A/B change % B/A change %
matched_maps/hash_only/map/10/reversed 0.307597 [0.306905, 0.308324] 0.308063 [0.307327, 0.308836] 0.15
matched_maps/hash_only/map/50/forward 2.043116 [2.027317, 2.061498] 2.023694 [2.015670, 2.032417] -0.95
matched_maps/hash_only/map/50/reversed 2.031379 [2.021587, 2.042563] 2.033373 [2.025952, 2.041082] 0.10
matched_maps/hash_only/struct_map_int/1/reversed 0.035797 [0.035662, 0.035952] 0.035960 [0.035927, 0.035994] 0.46
matched_maps/hash_only/struct_map_int/10/reversed 0.322155 [0.321483, 0.322876] 0.323318 [0.322561, 0.324042] 0.36
matched_maps/hash_only/struct_map_int/50/reversed 2.060097 [2.054241, 2.066084] 2.059854 [2.051329, 2.069160] -0.01
matched_maps/normalize_only/map/1/forward 0.044923 [0.044822, 0.045029] 0.044885 [0.044768, 0.045009] -0.08
matched_maps/normalize_only/map/1/reversed 0.045150 [0.045023, 0.045276] 0.045925 [0.045612, 0.046272] 1.72
matched_maps/normalize_only/map/50/forward 2.837147 [2.834211, 2.840324] 1.718847 [1.713643, 1.724722] -39.42
matched_maps/normalize_only/map/50/reversed 3.585526 [3.575871, 3.598663] 2.419840 [2.417011, 2.422764] -32.51
regression_maps/normalize_only/map/mixed_no_null 1.202431 [1.194928, 1.210687] 1.190024 [1.180861, 1.200472] -1.03
regression_maps/normalize_only/map/singleton_dense_null 0.064859 [0.064683, 0.065039] 0.064748 [0.064412, 0.065114] -0.17
regression_maps/normalize_only/map/singleton_long_unicode 0.372372 [0.371965, 0.372786] 0.373767 [0.373339, 0.374249] 0.37
regression_maps/normalize_only/map/singleton_no_null 0.061337 [0.061022, 0.061699] 0.061878 [0.061588, 0.062208] 0.88
regression_maps/normalize_only/map/singleton_sparse_null 0.092175 [0.091949, 0.092419] 0.092645 [0.092393, 0.092924] 0.51
spark_map_sort/int_keys/1 0.017857 [0.017817, 0.017899] 0.017066 [0.016999, 0.017147] -4.43
spark_map_sort/int_keys/16 1.393109 [1.390020, 1.396776] 1.441855 [1.439159, 1.444649] 3.50 1.99
spark_map_sort/int_keys/4 0.939643 [0.936180, 0.943118] 0.996864 [0.994855, 0.999169] 6.09 0.11
spark_map_sort/int_keys/64 1.859120 [1.853594, 1.865412] 1.741639 [1.736799, 1.747314] -6.32
spark_map_sort/string_keys/1 0.035479 [0.035395, 0.035553] 0.035450 [0.035257, 0.035684] -0.08
Benchmark executable identities (SHA-256)
  • baseline map_sort: d93a75c7df21c0b57fb994664c6024b8aa07bea85939f08d7b0e6279689c6e8f
  • baseline hash: 05739bd6b3ef97c63ed814943cd994e205212abb9a95c8484c977ebdc77e3207
  • integrated map_sort: 47a0a73bbedfd8c6339c0e3626d1d0af6ffb9e6fdee679c8c26896905147f7a3
  • integrated hash: ccc68e4d95296c241e222a7c5a552c8a57c30b5365fccbe72ff097a5400e7dd3

@github-actions github-actions Bot added enhancement New feature or request performance area:expressions Expression evaluation labels Sep 13, 2026

@rich7420 rich7420 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@viirya thanks for the patch

@mbutrovich
mbutrovich self-requested a review September 14, 2026 15:37

@sunchao sunchao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness

Summary and compatibility

Reviewed 8282008e388ab05327ab4f7d8dbba94ae034ddab against 4abfd95114d61ad454f9ee269be1615f469f24e2. The prior implementation already handled singleton maps efficiently, but multi-entry rows still sliced their key arrays and invoked Arrow sorting separately. This change reuses string-sort scratch storage for Utf8 keys with Int32 values and bulk-fills offsets for empty visible batches.

The maintained Spark 4.0 implementation sorts keys in ascending order while moving their values with them; its optimizer inserts this normalization for map grouping and repartitioning. Default string ordering is binary UTF-8 ordering. The new comparator agrees with that ordering and reproduces Arrow 59.3's prefix/length comparator and tuple layout. Keeping the tuple layout also preserves Arrow's current unstable permutation for duplicate keys; this does not add a stable-sort guarantee.

The safety and reconstruction boundaries hold: valid MapArray offsets bound every unchecked key access, the four-byte load is guarded by string length, and the nonnegative i32 offsets keep generated indices representable. The global permutation takes whole key/value entries together. Sliced offsets are rebased, map validity and schema metadata are retained, and physical entries below null maps follow the existing sorting/error behavior. Empty batches, all-null batches and the declared-sorted shortcut retain their existing behavior. Nonmatching key/value types continue through Arrow; unsupported nested keys still produce its errors.

The Spark-side type gates continue to reject collated string sorting, and the strict floating-point restriction is unchanged. The maintained Spark 3.5 branch has no MapSort expression; Comet's registration and map-shuffle normalization remain Spark 4.x paths. Maintained 3.4 and 4.1 source branches were unavailable, so the direct source comparison is qualified accordingly.

Validation

All 12 previous unit tests are byte-identical; four additions cover sliced/null/Unicode maps, exact duplicate-key permutations across 48 size/distribution combinations, unsupported keys, and empty visible slices. Rust CI passed all 16 map-sort tests and 1,448 tests overall, with five skipped. Spark 4.1 shuffle CI passed 491 tests, including map keys below an offset, entry-order independence, partition assignment versus Spark, and nested nulls.

Those jobs checked out a8e9f9bc, whose complete source tree equals this PR head. The newer merge preview is distinct; I am not treating the older run as validation of every later base change. At the review cutoff, the head had 53 successful and 10 skipped checks. I performed source and CI verification, without a local native build or benchmark. No verified P1/P2 correctness issue remains in this change.

Performance

The scratch reuse removes per-row key slices and Arrow's temporary index, tuple and output allocations from the selected shape. Scratch is reused across rows and grows with the largest row; the existing batch permutation and single entry take remain. Other shapes pay the added batch type check and fast-path branch while retaining their existing work. Eligible singletons still append directly.

The author reports 122 matched cases against a base that already includes #5887. I checked the inventory: all 18 original slowdown flags have fresh pairs, with reverse-order checks for the two remaining larger fallback flags. Reported 2–10-entry forward normalization is about 3x faster; 2–50-entry fresh pairs improve 33–39%. Initial singleton slowdowns largely disappear in fresh runs. The int-key 16-entry fallback remains +3.50% and +1.99% in the two orders; small effects remain uncertain, and unchanged hash-only controls show appreciable process drift in the original cohort.

The fixtures check exact entries, validity, offsets and schema outside timing. Normalization allocation/drop and combined struct reconstruction are timed; hash-buffer allocation is outside timing and seed reset is inside. Dependency manifests and lockfile are unchanged between the assigned base and head. The timing results and executable hashes are author-reported; I did not independently validate the benchmark binaries or raw samples. This supports the targeted allocation optimization, with the stated microbenchmark limits, rather than an end-to-end Spark speedup claim.

Design

The implementation fits the existing batch-permutation design: one private helper handles the measured string/int shape, while reconstruction and fallback stay shared. Preserving Arrow's comparator avoids introducing a second ordering policy. The empty-visible-batch shortcut still goes through take and rebasing, which is necessary when a slice retains an unused entry prefix. The change leaves hashing, expression registration and feature defaults intact.

Abstraction & complexity

The helper earns its cost by reusing the same scratch allocation across rows. Its explicit tuple and safety comments explain behavior that a simpler index-only unstable sort could change for equal keys. The existing const-generic singleton dispatch remains; there is no additional framework or configuration. The differential Arrow test and independent fixture permutations provide useful coverage for maintaining this specialized code. No additional change is requested before merge.

@viirya
viirya added this pull request to the merge queue Sep 15, 2026
@viirya

viirya commented Sep 15, 2026

Copy link
Copy Markdown
Member Author

Thanks for reviewing!

Merged via the queue into apache:main with commit fad6230 Sep 15, 2026
63 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:expressions Expression evaluation enhancement New feature or request performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reduce per-row sorting allocations in multi-entry map_sort normalization

3 participants