perf: replace stdlib sort.Slice with slices pkg to eliminate heap escapes#25616
perf: replace stdlib sort.Slice with slices pkg to eliminate heap escapes#25616fengttt wants to merge 9 commits into
Conversation
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
gouhongshen
left a comment
There was a problem hiding this comment.
Blocker: the two semantic-order-sensitive conversions pass non-strict comparators to slices.SortFunc. The API explicitly requires a strict weak ordering; returning a negative result for equal values makes cmp(x,x) < 0, so the claimed behavior preservation is outside the API contract and depends on the current sorting implementation. Since filter order is already demonstrated to affect query correctness, this cannot be merged as-is. Please either define an explicit, valid tie-breaker that captures the required semantic order, or leave these two sites on sort.Slice and address their pre-existing invalid comparators separately.
LeftHandCold
left a comment
There was a problem hiding this comment.
Two additional comparator-contract violations found while auditing the remaining conversions; these are the same root class as the existing requested changes.
…t.Slice Address review: a slices.SortFunc comparator must be a strict weak ordering. Four sites were not: - pkg/vm/engine/disttae/local_disttae_datasource.go: the block-order comparator returned "less" in both directions when both zone maps were uninitialized. Compare initialization state explicitly first (both uninitialized => equal, exactly one => a consistent sign), then compare min/max. Valid in both ASC and DESC branches. - pkg/vm/engine/tae/db/checkpoint/snapshot.go: nil entries were treated as equal to every entry (non-transitive incomparability). Give nil a deterministic position (sorted first, both-nil => 0); dereferences stay nil-safe. - pkg/vm/engine/tae/txn/txnimpl/table.go: the soft-deleted-object sort used a non-strict CreatedAt.LE. Use slices.SortStableFunc with a strict CreatedAt.Compare so equal timestamps keep their original relative order. - pkg/sql/plan/stats.go: the filter-cost sort's comparator is non-strict (cost1 <= cost2) and its exact tie ordering is load-bearing -- it selects which predicate a vector/IVF index probes with and which value a runtime bool error reports (see operator/is_not_operator, vector/vector_ivf_mode, and IVF issue matrixorigin#25639). No strict slices comparator reproduces that order (a stable strict sort changes it and returns wrong results), so this one site is intentionally kept on sort.Slice. Verified: go build ./..., go vet, gofmt clean; BVT operator, vector, optimizer, dml/{select,delete,insert}, join, dtype, disttae all 100%. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e; use stable filter sort Fixes matrixorigin#25639: an IVFFLAT `mode=pre` query with more than one distance predicate on the indexed column (e.g. `l2 < 1.1 AND l2 < 1.2`) could return wrong rows. getDistRangeFromFilters kept only the FIRST bound per side as the index range and left the rest as residual filters, which the mode=pre join then dropped -- so the result depended on filter order (7 rows instead of 3 when the looser bound came first). Now getDistRangeFromFilters folds every same-side bound into the tightest one (min for upper, max for lower; exclusive wins on an equal value), so the index enforces the intersection of all predicates regardless of order. A looser same-side bound is redundant and dropped; a non-literal bound still falls back to being kept as a residual filter. Verified: `l2 < 1.1 AND l2 < 1.2` and `l2 < 1.2 AND l2 < 1.1` both return the correct 3 rows. Fixes matrixorigin#25646: with the IVF scan no longer order-sensitive, the filter-cost sort in stats.go can be a well-defined ordering again. Switch it from sort.Slice to slices.SortStableFunc with a strict cmp.Compare: equal-cost filters keep their original (as-written) relative order -- a deterministic, valid strict weak ordering -- instead of sort.Slice's unspecified tie order. This completes the sort.Slice -> slices migration for this site. Golden updates (behavior is otherwise unchanged): - optimizer/in_domain, optimizer/associative: EXPLAIN now renders equal-cost filters in as-written order (display only). - operator/is_not_operator: `str2 IS TRUE AND str1 IS TRUE` (an invalid query) now cites str2's invalid value first; still an error either way. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
gouhongshen
left a comment
There was a problem hiding this comment.
Additional findings from a re-audit of the current head:
- The two new P1 findings are inline below: the join-order comparator is non-transitive, and the IVF distance-bound folding can remove the first non-literal predicate before runtime validation.
- The last commit also expands this PR beyond a mechanical sort migration: it changes IVF predicate semantics and SQL goldens. Please either split that behavior fix or document it as an independently verified scope.
- The new numeric-bound tests cover literal bounds and order reversal, but not first-invalid/second-valid bounds, NULL/column/parameter bounds, or end-to-end mode=pre/mode=post preservation.
Existing stats/txn/zone-map threads are intentionally not duplicated. I added a follow-up to the existing snapshot thread for the remaining nil dereference path.
There was a problem hiding this comment.
Pull request overview
This PR is a broad performance-focused refactor across MatrixOne that replaces sort.Slice / sort.SliceStable (and one slice-based sort.Sort) with the generic slices package to reduce heap escapes and reflection overhead, while also tightening determinism/robustness around vector-distance predicate handling and filter ordering.
Changes:
- Replaced many
sort.Slice/sort.SliceStablecall sites withslices.Sort/slices.SortFunc/slices.SortStableFunc(often viacmp.Compare) to avoid slice boxing and improve escape behavior. - Made planner filter ordering deterministic via stable, strict comparators, updating expected test outputs accordingly.
- Updated vector distance bound extraction to fold multiple same-side bounds into the tightest bound and added unit coverage for order-independence.
Reviewed changes
Copilot reviewed 70 out of 71 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/distributed/cases/vector/vector_ivf_mode.sql | Clarifies intended invariant for multiple distance bounds (intersection, order-independent). |
| test/distributed/cases/optimizer/in_domain.result | Updates expected EXPLAIN filter order due to new deterministic sorting. |
| test/distributed/cases/optimizer/associative.result | Updates expected filter ordering in plan output. |
| test/distributed/cases/operator/is_not_operator.result | Updates expected error text driven by deterministic filter evaluation order. |
| pkg/vm/engine/tae/txn/txnimpl/table.go | Switches to slices.SortStableFunc for object ordering without sort.Slice boxing. |
| pkg/vm/engine/tae/logtail/snapshot.go | Replaces several sort.Slice uses with slices.SortFunc for deterministic TS ordering. |
| pkg/vm/engine/tae/logtail/handle.go | Uses slices.Sort for key ordering. |
| pkg/vm/engine/tae/logtail/ckp_reader.go | Uses slices.SortFunc + cmp.Compare for descending counts. |
| pkg/vm/engine/tae/logstore/driver/batchstoredriver/file.go | Uses slices.SortFunc for version ordering of log files. |
| pkg/vm/engine/tae/db/merge/statOverlap.go | Uses slices.SortFunc for overlap ratio ranking. |
| pkg/vm/engine/tae/db/merge/statLayerZero.go | Uses slices.SortFunc for min zone-map ordering. |
| pkg/vm/engine/tae/db/checkpoint/snapshot.go | Replaces sort.Slice with strict slices.SortFunc (including deterministic nil handling). |
| pkg/vm/engine/tae/db/checkpoint/replay.go | Uses slices.SortFunc for checkpoint file ordering. |
| pkg/vm/engine/tae/compute/compute.go | Migrates generic SortAndDedup to slices.SortFunc. |
| pkg/vm/engine/tae/common/intervals.go | Replaces interval sorting with slices/cmp. |
| pkg/vm/engine/tae/catalog/schema.go | Sorts sort-key defs with slices.SortFunc + cmp. |
| pkg/vm/engine/readutil/tombstone_data.go | Sorts Rowids via slices.SortFunc rather than sort.Slice. |
| pkg/vm/engine/memoryengine/shard.go | Sorts shard infos with slices.SortFunc + cmp. |
| pkg/vm/engine/memoryengine/shard_hash.go | Replaces several sort.Slice calls with slices.SortFunc. |
| pkg/vm/engine/disttae/txn.go | Uses slices.Sort / slices.SortFunc for deterministic workspace ordering. |
| pkg/vm/engine/disttae/tracked_partitions.go | Uses slices.SortFunc to evict oldest snapshot deterministically. |
| pkg/vm/engine/disttae/logtailreplay/get_flush_ts.go | Uses slices.SortFunc for time ordering. |
| pkg/vm/engine/disttae/local_disttae_datasource.go | Introduces strict ordering for zone-map init state + slices.SortFunc. |
| pkg/vm/engine/disttae/cache/catalog.go | Replaces sort.Sort (slice named type) with slices.SortFunc. |
| pkg/vectorindex/cuvs/cdc.go | Uses slices/cmp for chunk and output ordering. |
| pkg/util/debug/goroutine/analyze.go | Sorts groups by size with slices.SortFunc. |
| pkg/txn/storage/memorystorage/mem_handler.go | Uses slices.SortFunc for column/def ordering. |
| pkg/txn/client/operator.go | Sorts tracker info with slices.SortFunc + cmp. |
| pkg/tools/checkpointtool/checkpoint_reader.go | Uses slices.SortFunc for entry/account/table ordering. |
| pkg/taskservice/task_runner.go | Uses slices.SortFunc for retry queue ordering. |
| pkg/taskservice/mem_task_storage.go | Uses slices.SortFunc/cmp for deterministic in-memory query output ordering. |
| pkg/sql/schedule/placement.go | Replaces worker ordering with slices.SortFunc. |
| pkg/sql/plan/tools/helper.go | Uses slices.SortFunc to canonicalize var ref lists deterministically. |
| pkg/sql/plan/stats.go | Makes filter-cost ordering stable + strict via slices.SortStableFunc. |
| pkg/sql/plan/join_order.go | Replaces subtree/dimension ordering with slices.SortFunc. |
| pkg/sql/plan/function/func_builtin_jq.go | Uses slices.SortFunc for deterministic map key ordering. |
| pkg/sql/plan/function/func_binary.go | Uses slices.SortFunc for geometry interval ordering. |
| pkg/sql/plan/explain/explain_node.go | Uses slices.Sort for deterministic reporting arrays. |
| pkg/sql/plan/apply_indices_vector.go | Folds multiple distance bounds and adds helpers for literal extraction/merging. |
| pkg/sql/plan/apply_indices_vector_test.go | Updates/extends tests to assert tightest-bound folding and order-independence. |
| pkg/sql/compile/compile.go | Replaces several stable sorts with slices.SortStableFunc. |
| pkg/sql/colexec/table_function/processlist.go | Sorts sessions using slices.SortFunc. |
| pkg/sql/colexec/lockop/lock_op.go | Sorts lock rows with slices.SortFunc. |
| pkg/sql/colexec/external/hive_partition.go | Uses slices.SortFunc for deterministic file/prefix ordering. |
| pkg/sql/colexec/external/hive_partition_cache.go | Uses slices.SortFunc for deterministic dir entry ordering. |
| pkg/shardservice/scheduler_balance.go | Uses slices.SortFunc for replica-count ordering. |
| pkg/shardservice/runtime.go | Uses slices.SortFunc for deterministic CN ordering. |
| pkg/hakeeper/operator/builder.go | Uses slices.Sort for deterministic peer lists. |
| pkg/hakeeper/checkers/tnservice/parse.go | Uses slices.Sort to stabilize shard ID order. |
| pkg/hakeeper/checkers/tnservice/check.go | Uses slices.SortFunc + cmp across several selection orderings. |
| pkg/hakeeper/checkers/logservice/parse.go | Uses slices.Sort for replica ID ordering. |
| pkg/hakeeper/checkers/logservice/filter.go | Uses slices.SortFunc for store selection. |
| pkg/hakeeper/checkers/logservice/check.go | Replaces non-strict comparator with slices.SortFunc + cmp. |
| pkg/hakeeper/bootstrap/bootstrap.go | Uses slices.SortFunc + cmp for tick-based ordering. |
| pkg/geo/hull.go | Uses slices.SortFunc + cmp for coordinate ordering. |
| pkg/fulltext/fixedbytepool.go | Uses slices.SortFunc for LRU ordering. |
| pkg/frontend/rewrite_rule.go | Uses slices.SortStableFunc with strict comparator for rule cache ordering. |
| pkg/frontend/mysql_cmd_executor.go | Uses slices.SortFunc for SHOW output ordering. |
| pkg/frontend/logservice.go | Replaces several sort.Slice orderings with strict slices.SortFunc + cmp. |
| pkg/frontend/data_branch_privilege.go | Uses slices.Sort for deterministic ID lists. |
| pkg/frontend/compiler_context.go | Uses slices.SortFunc for UDF match ordering. |
| pkg/fileservice/s3_fs.go | Uses slices.SortFunc for IO entry ordering. |
| pkg/fileservice/qcloud_sdk.go | Uses slices.SortFunc for multipart part ordering. |
| pkg/fileservice/memory_fs.go | Uses slices.SortFunc for IO entry ordering. |
| pkg/fileservice/local_fs.go | Uses slices.SortFunc for IO entry ordering. |
| pkg/fileservice/local_etl_fs.go | Uses slices.SortFunc for IO entry ordering. |
| pkg/fileservice/aws_sdk_v2.go | Uses slices.SortFunc for multipart part ordering. |
| pkg/clusterservice/cluster.go | Uses slices.SortFunc to keep TN stores ordered by tick. |
| pkg/backup/types.go | Uses slices.SortFunc + cmp for meta type ordering. |
| pkg/backup/tae.go | Replaces meta file ordering with slices.SortFunc. |
| cmd/mo-service/debug.go | Uses slices.SortFunc + cmp for deterministic debug output ordering. |
aptend
left a comment
There was a problem hiding this comment.
Requesting changes for the remaining correctness blockers on the current head:
-
The first distance bound is accepted without validating that it is a numeric literal. When the first matching upper or lower bound is a column, parameter, NULL, or another unsupported expression,
mergeUpperBound/mergeLowerBoundstores it inDistRangeand removes the predicate from the residual filter list. The runtime reader then failsgetLiteralFloat64and returns without installing the bound, so the query can execute without that predicate. Please validate an unbounded side before storing it, preserve unsupported bounds as residual filters, and add first-invalid/second-valid tests in both orders. -
The two join-order conversions use a non-transitive comparator.
compareStatsswitches between selectivity and out-count within a 0.01 tolerance. For example, A=(selectivity 0.000, outcnt 3), B=(0.009, 2), and C=(0.018, 1) form a comparison cycle. This is not a strict weak ordering and cannot be passed toslices.SortFunc. Please define a transitive ordering key first, or leave both join-order sites onsort.Sliceand track the underlying comparator separately. -
parameterIntervalsCoverSegmenthas another tolerance-based, non-transitive comparator.sameGeometryCoordinatetreats starts within 1e-9 as equal, then compares their ends. Starts A=0.5, B=0.5000000009, C=0.5000000018 with descending ends form a comparison cycle. A complete interval cover can consequently be ordered with an apparent gap, producing incorrectST_CONTAINS,ST_COVERS,ST_EQUALS, orST_OVERLAPSresults. Please sort by an exact total order on start/end and reserve the epsilon comparison for the subsequent gap/merge logic; add a near-epsilon regression test.
The CI suite is green, but it does not cover these comparator-contract and unsupported-bound cases. The previously reported stats, txn, zone-map, and nil-ordering comparator fixes look addressed.
… non-transitive join sort Address review on the vector/DistRange changes: - Validate the FIRST distance bound. mergeUpperBound/mergeLowerBound accepted the first matching bound unconditionally; a non-literal first bound (column, param, cast) was peeled off the filter list but later rejected by the reader, silently dropping the constraint. They now validate every bound (including the first) and return false for non-literals so the predicate is kept as a residual filter. Added a first-invalid + second-valid test in both orders. - Single owner for the literal->float64 decode. Extract the decoder (with a nil guard) to plan.GetLiteralFloat64 in pkg/pb/plan; the planner (DistRange folding) and readutil.getLiteralFloat64 now both delegate to it, so plan-time and runtime bound handling can't drift. - getDistRangeFromFilters returns nil (not an empty DistRange) when every matching predicate was non-literal. - join_order.go: revert both compareStats sorts to sort.Slice. compareStats is non-transitive (0.01 selectivity tolerance), so the two-call wrapper is not a valid slices.SortFunc strict weak ordering. Tracked in matrixorigin#25702. - checkpoint/snapshot.go: guard the entries[i].end dereference with a nil check; nil entries now sort first, so the maxGlobalEnd scan must not assume non-nil. Verified: go build ./..., go vet, gofmt clean; unit tests pass; BVT vector, optimizer, join, dml/{select,delete,insert} all 100%. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
compareStats grouped selectivities within a sliding |s1-s2| < 0.01 window, which is not transitive (a~b and b~c does not imply a~c), so it was not a valid strict weak ordering and its two join_order sort sites had to stay on sort.Slice. Bucket selectivity onto a fixed 0.01 grid (floor(sel/0.01)) and compare lexicographically by (bucket, outcnt). This is a genuine strict weak ordering, so compareStats now returns a three-way int and the two join_order sorts migrate to slices.SortFunc (the direct boolean use becomes `compareStats(...) < 0`). Behavior is unchanged for the test suite: the grid only differs from the sliding window near 0.01 boundaries, and a sweep of the EXPLAIN- and join-heavy dirs (optimizer, join, cte, subquery, recursive_cte, view, distinct, window, ...) shows no plan or result changes. Added TestCompareStatsIsStrictWeakOrdering (the review counter-example is now cycle-free, plus a brute-force transitivity/antisymmetry check over a selectivity/outcnt grid). Refs matrixorigin#25702. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
XuPeng-SH
left a comment
There was a problem hiding this comment.
I think this still needs changes before merge.
The earlier join-order and vector-distance-bound issues look materially improved at this head, but one remaining correctness blocker stands out:
parameterIntervalsCoverSegment now uses slices.SortFunc, but the comparator is still not a strict weak ordering because it treats starts within 1e-9 as equal and then compares ends.
Current code in pkg/sql/plan/function/func_binary.go:
- if sameGeometryCoordinate(a.start, b.start) { return cmp.Compare(a.end, b.end) }
- else return cmp.Compare(a.start, b.start)
That relation is non-transitive. Example: starts A=0, B=0.9e-9, C=1.8e-9. Then AB and BC, but A is not equivalent to C. With ends A=0.9, B=0.8, C=0.7, the comparator gives A>B, B>C, and C>A — a cycle. slices.SortFunc explicitly requires a strict weak ordering, so behavior is undefined here.
This is not just theoretical. lineSegmentCoveredByLineCollection builds these intervals and immediately assumes the sorted first element has the minimum start. If the cyclic triple above is ordered with the 1.8e-9 interval first, parameterIntervalsCoverSegment returns false at intervals[0].start > 1e-9 even though the intervals together cover the full segment. That can change line-based geometry predicate results in the LINESTRING/MULTILINESTRING paths that call it.
Suggested fix: sort by an exact total order on raw (start, end) and keep the epsilon logic only in the later merge/gap checks. Please also add a near-epsilon regression that proves complete coverage still returns true across this boundary case.
parameterIntervalsCoverSegment sorted its intervals with a comparator that treated starts within 1e-9 (sameGeometryCoordinate) as equal and then compared ends. That epsilon band is non-transitive (start A~B and B~C does not imply A~C), so it is not a valid slices.SortFunc strict weak ordering: for a cyclic near-epsilon triple the sort can leave a non-minimum-start interval at index 0, and the coverage check then wrongly bails at intervals[0].start > 1e-9 even when the intervals together cover the whole segment -- changing LINESTRING/ MULTILINESTRING coverage predicate results. Sort by an exact total order on (start, end) instead; the epsilon tolerance stays in the gap/coverage loop, where it is applied pairwise and does not require transitivity. Behavior is unchanged for well-separated intervals (all real data); only the pathological near-epsilon ordering is corrected. Added TestParameterIntervalsCoverSegmentNearEpsilonBoundary: the reviewer's cyclic-triple boundary case (which returns a wrong false under the old comparator, verified) now reports full coverage as true in every input order, and a genuine gap still returns false. Verified: go build ./..., go vet, gofmt clean; the new unit test and the geo BVT suite pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@XuPeng-SH thanks — fixed in 221ffe0. You're right, this was the same non-transitive-comparator class as the earlier ones, and it's a real correctness bug in the LINESTRING/MULTILINESTRING coverage path, not just theoretical. slices.SortFunc(intervals, func(a, b geometryParamInterval) int {
if c := cmp.Compare(a.start, b.start); c != 0 {
return c
}
return cmp.Compare(a.end, b.end)
})The epsilon ( Added Verified: unit test + the full |
XuPeng-SH
left a comment
There was a problem hiding this comment.
Re-reviewed the latest head (245eac) after the geometry fix and the clean merge from main. The previous blocking comments are resolved.
Correctness: the migrated comparators now satisfy strict weak ordering, with stable sorting where equal-key order is semantically observable. The vector-distance planner folds repeated bounds to their true intersection, preserves unsupported bounds as residual predicates, and shares literal decoding with the reader. The geometry interval sort now uses an exact total order and applies epsilon only in the coverage logic. Join-stat ordering is transitive.
Performance: the conversions retain in-place O(n log n) behavior while removing interface/reflection boxing; no new unbounded work or hot-path allocation was introduced. The full basic benchmark passed on the geometry-fix parent commit.
Unhappy paths: checked nil/uninitialized zone maps and checkpoints, unsupported/non-literal and one-sided vector bounds, equal keys, input permutations, and Q1-Q3 resource/wait/growth risks. No new resource ownership, waits, goroutines, or unbounded accumulation were introduced.
Validation: all Go packages touched by the PR pass locally; the focused planner and geometry regressions pass 20 consecutive runs. The complete CI suite (UT, SCA, three BVT variants, coverage, and basic benchmark) passed on parent 221ffe0. The current head is a conflict-free main merge, its rerun has no failures so far, and merge protection will still wait for it.
| slices.SortFunc(vals, func(a, b T) int { | ||
| if lessFn(&a, &b) { | ||
| return -1 | ||
| } | ||
| if lessFn(&b, &a) { | ||
| return 1 | ||
| } | ||
| return 0 |
What
Escape-analysis-driven pass over stdlib
sort.sort.Slice(s, less)boxes its slice argument intointerface{}, which forcess escapes to heap(a per-call allocation — confirmed with-gcflags=-m) and uses reflection for element swaps. The genericslices.Sort/slices.SortFunc/slices.SortStableFuncdo neither.Changes
Converted ~104 stdlib
sort.Slice/sort.SliceStablesites across 66 files:slices.Sortslices.SortFuncwithcmp.Compare(cmp.Compare, nevera-b, is overflow-safe)sort.SliceStable→slices.SortStableFunc(stability preserved)a,b) index the target arraylessFnpredicates → two-call 3-way comparatorAlso converted the one slice-based 1-arg
sort.Sort(disttae/cacheColumns, a[]catalog.Columnnamed type), which escapes for the same reason.Deliberately left unchanged
sort.Sorton pointer receivers (*groupSortKeys,*BatchWithVersion): a pointer fits in the interface word, so boxing does not allocate — no benefit.SkipCmd.Sortinlogentry.go: its comparator has a side effect (swaps a parallelpsnsarray during comparison), soslices.SortFunc's different call pattern would corrupt it. Tracked in Remove side-effecting sort comparator in SkipCmd.Sort (logentry.go) #25615.sort.Search/sort.Strings/sort.Float64s: not slice-boxing.Verification
go build ./...— cleango veton all 66 touched packages — cleangofmt— clean🤖 Generated with Claude Code
Refs #25639 (owner will close manually)
Refs #25646 (owner will close manually)
Refs #25702 (owner will close manually)