Skip to content

Specialize list.sort() comparisons for homogeneous lists - #8464

Merged
youknowone merged 6 commits into
RustPython:mainfrom
kangdora:powersort-type-specialize
Aug 8, 2026
Merged

Specialize list.sort() comparisons for homogeneous lists#8464
youknowone merged 6 commits into
RustPython:mainfrom
kangdora:powersort-type-specialize

Conversation

@kangdora

@kangdora kangdora commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements CPython's pre-sort check for list.sort(), as promised in the follow-ups of #8421.

Before sorting, the keys are scanned once (O(n)); when they are homogeneous in exact type, the O(n log n) comparisons skip rich_compare_bool's dispatch (recursion guard, subclass/reflected-op resolution, slot lookup, NotImplemented retry) and use a comparator specialized for that type. This mirrors the unsafe_latin_compare / unsafe_long_compare / unsafe_float_compare / unsafe_object_compare / unsafe_tuple_compare family in CPython's Objects/listobject.c.

What changed

  • crates/vm/src/builtins/list.rs: pre_sort_check scans the keys (the key function results when key= is given, matching CPython's lo.keys scan) and picks a tier: Str, Int, Float, Object (cached richcompare slot), Tuple (with a nested tier for first elements), or Generic. sorting.rs is untouched — specialization only decides which is_lt closure the existing powersort receives.
  • Each tier dispatches through its own timsort call, so the comparator monomorphizes into the merge loop — no per-comparison indirect call, unlike CPython's function-pointer approach.
  • The Object tier guards each comparison with a pointer check against the cached slot and falls back to rich_compare_bool on mismatch or NotImplemented, so a stale cache can only cost speed, never correctness.
  • The Tuple tier follows CPython exactly: elements are walked with == (identity shortcut included), only index 0 may use the specialized comparator (the scan verified nothing about later elements), later differences fall back to generic <, and nested tuples are not recursed into. The element-tier enum has no tuple variant, so that rule is enforced by the type system.
  • Tests in extra_tests/snippets/builtin_list.py: non-ASCII code point order, huge ints, bool/subclass rejection, mixed-type TypeError, NaN ordering, bytes, and five tuple cases (empty, nested, mixed first elements, ties at and past index 0).

Differences from CPython, and why they hold

  • No latin-1 restriction for str. CPython stores strings as UCS-1/2/4, so memcmp only matches code point order for 1-byte strings. RustPython stores WTF-8, and generalized UTF-8 byte order equals code point order across the whole range (lone surrogates included), so every homogeneous str list qualifies.
  • No machine-word restriction for int. CPython needs single-digit longs to shortcut its digit-array walk; comparing BigInts through Ord is already dispatch-free, so all-int lists qualify regardless of magnitude.
  • bool is excluded by the exact-type check (as in CPython), and subclasses never qualify since they may override __lt__.

Results

Release build, best of 5, seed 42, vs CPython 3.14. "generic" is the same build with the scan defeated by one subclass element, i.e. the previous behavior on otherwise identical data.

1M elements:

workload generic specialized CPython 3.14
1M random ints 0.72s 0.53s (×1.35) 0.30s
1M random floats 0.69s 0.45s (×1.54) 0.18s
1M random strs 1.07s 0.83s (×1.29) 0.35s
1M sorted ints 0.044s 0.035s

300k elements, and the speedup CPython itself gets from the same trick on the same data as a cross-check that the port is faithful:

workload speedup here speedup in CPython
random bytes (object tier) ×1.05 ×1.06
random (int, int) tuples ×1.23 ×1.17

(For str/int/float the CPython-side ratios are ×1.33/×1.07/×1.53 — same pattern.) The gap vs CPython on random ints narrows from 2.4× to 1.7×.

Follow-ups

The remaining gap is dominated by two things outside this PR's scope: merges are clone-based (T: Clone in sorting.rs), so every element move pays an atomic refcount operation where CPython memmoves bare pointers — I plan to address this with move-based merges as a follow-up; and PyStr keeps its bytes in a separate allocation (CPython inlines them since PEP 393), which is an object-representation question to be filed separately.

Notes

Despite mirroring CPython's unsafe_* comparator family, the port is entirely safe Rust: the invariants those comparators trust are still re-checked by cheap typed downcasts, so a violated invariant degrades to a panic or a fallback, never to undefined behavior.

The Object tier compares function pointers, which rustc flags (unpredictable_function_pointer_comparisons). Both failure modes are harmless here: a duplicated function makes the guard fail into the fallback path (slower, still correct), and merged functions have identical bodies.

Summary by CodeRabbit

  • New Features

    • Improved list.sort() and sorted() handling for strings, numbers, tuples, custom objects, and other key types.
    • Sorting preserves reverse-order behavior while using optimized comparisons where possible.
    • Enhanced fallback behavior for custom comparisons and unsupported comparison results.
  • Bug Fixes

    • Improved consistency for mixed numeric values, integer subclasses, booleans, NaN values, bytes, and invalid mixed-type comparisons.
    • Expanded support for Unicode strings and lexicographic tuple sorting.

Scan the sort keys once before sorting; when every element is exactly str, int, or float, compare wtf8 bytes / BigInt / f64 directly instead of going through rich_compare_bool dispatch, mirroring CPython's pre-sort check in listsort.c (unsafe_latin_compare and friends). Subclasses and mixed-type lists keep the generic __lt__ path.
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 53ad4266-48f3-4b9e-835d-2a96df371f92

📥 Commits

Reviewing files that changed from the base of the PR and between 2a02904 and d8e5307.

📒 Files selected for processing (2)
  • crates/vm/src/builtins/list.rs
  • extra_tests/snippets/builtin_list.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • extra_tests/snippets/builtin_list.py

📝 Walkthrough

Walkthrough

List sorting now classifies keys and selects specialized comparators for homogeneous types, tuples, custom objects, and generic values. Keyed and unkeyed sorting use the new path. Tests cover Unicode, numeric, tuple, bytes, NaN, subclass, and invalid comparisons.

Changes

List sort specialization

Layer / File(s) Summary
Key classification and specialized comparison
crates/vm/src/builtins/list.rs
The sorter classifies keys and uses specialized comparisons for strings, integers, floats, custom objects, tuples, and generic values. It handles rich-comparison fallback and reverse ordering.
Sort integration and behavior coverage
crates/vm/src/builtins/list.rs, extra_tests/snippets/builtin_list.py
Keyed and unkeyed sorting now use timsort_specialized. Tests cover Unicode strings, large integers, booleans, integer subclasses, mixed numerics, bytes, tuples, NaN, and invalid comparisons.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant do_sort
  participant timsort_specialized
  participant rich_compare_bool
  Caller->>do_sort: sort keyed or unkeyed values
  do_sort->>timsort_specialized: provide values and extracted keys
  timsort_specialized->>timsort_specialized: classify keys and compare values
  timsort_specialized->>rich_compare_bool: fall back for generic comparisons
  timsort_specialized-->>do_sort: return sorted values
  do_sort-->>Caller: return sorted result
Loading

Possibly related PRs

Suggested reviewers: shaharnaveh

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: specialized comparisons for homogeneous list.sort() inputs.
Linked Issues check ✅ Passed The changes implement pre-scan specialization, key-vector handling, exact-type comparators, generic fallbacks, and relevant tests for issue #8450.
Out of Scope Changes check ✅ Passed The implementation and tests remain focused on type-specialized list.sort() comparisons and related fallback behavior.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
extra_tests/snippets/builtin_list.py (2)

254-254: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert element types, not only values, for the int subclass case.

IntSub(1) == 1 is true, so this assertion passes even if the sort loses the subclass instances. Add a type check to prove the generic fallback keeps the original objects.

♻️ Proposed test change
-assert sorted([IntSub(2), 3, IntSub(1)]) == [1, 2, 3]
+_sub_sorted = sorted([IntSub(2), 3, IntSub(1)])
+assert _sub_sorted == [1, 2, 3]
+assert [type(x) for x in _sub_sorted] == [IntSub, IntSub, int]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extra_tests/snippets/builtin_list.py` at line 254, Update the assertion
covering sorted IntSub values to also verify that the resulting elements retain
their original IntSub/int types, not merely equal the expected numeric values.
Keep the existing ordering check and use explicit type assertions to confirm the
generic fallback preserves subclass instances.

245-247: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for reverse=True, key=, and a custom __lt__ tier.

The assertions are correct. Three new code paths in crates/vm/src/builtins/list.rs have no coverage here.

  • reverse=True: timsort_specialized swaps the comparator operands in every tier. No test exercises that branch. A reverse test also pins stability for equal elements.
  • key=: do_sort classifies the key vector, not the values. No test exercises the keyed specialized path.
  • PreSort::Object with a Python-level __lt__: line 259 covers bytes, which uses a built-in slot. A user class that returns NotImplemented from __lt__ exercises the fallback in object_lt.
💚 Proposed additional tests
assert sorted([3, 1, 2], reverse=True) == [3, 2, 1]
assert sorted(["b", "a", "c"], reverse=True) == ["c", "b", "a"]
assert sorted([(2, 9), (1, 5), (2, 1)], reverse=True) == [(2, 9), (2, 1), (1, 5)]

# reverse=True must stay stable for equal keys.
pairs = [(1, "a"), (0, "b"), (1, "c")]
assert sorted(pairs, key=lambda p: p[0], reverse=True) == [(1, "a"), (1, "c"), (0, "b")]

# key= classifies the key vector, not the values.
assert sorted(["bbb", "a", "cc"], key=len) == ["a", "cc", "bbb"]
assert sorted([3, 1, 2], key=float) == [1, 2, 3]


class Cmp:
    def __init__(self, v):
        self.v = v

    def __lt__(self, other):
        if not isinstance(other, Cmp):
            return NotImplemented
        return self.v < other.v

    def __eq__(self, other):
        return isinstance(other, Cmp) and self.v == other.v


assert [c.v for c in sorted([Cmp(2), Cmp(1), Cmp(3)])] == [1, 2, 3]

Run pytest -v from extra_tests after adding these, using a debug-mode cargo run for snippet execution.

As per coding guidelines: "When changes touch snippet tests, run pytest -v from extra_tests; use debug-mode cargo run for snippet execution."

Also applies to: 250-264

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extra_tests/snippets/builtin_list.py` around lines 245 - 247, Extend the
builtin list sorting tests around the existing sorted assertions to cover
reverse=True, keyed sorting, and a custom class with Python-level __lt__
returning NotImplemented for unrelated types. Include reverse stability for
equal keys and assertions that exercise both value and key-vector classification
paths, then run pytest -v from extra_tests with debug-mode cargo run snippet
execution.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/vm/src/builtins/list.rs`:
- Around line 813-862: Refactor the six `PreSort` arms around `timsort` to share
the reverse-aware key operand swap and sorting invocation. Add a generic helper
such as `run_timsort` that accepts `items`, `reverse`, `key`, and the
tier-specific comparator, then have each arm supply only its comparator
(`str_lt`, `int_lt`, `float_lt`, `object_lt`, `tuple_lt`, or
`rich_compare_bool`) while preserving monomorphized comparator types.

---

Nitpick comments:
In `@extra_tests/snippets/builtin_list.py`:
- Line 254: Update the assertion covering sorted IntSub values to also verify
that the resulting elements retain their original IntSub/int types, not merely
equal the expected numeric values. Keep the existing ordering check and use
explicit type assertions to confirm the generic fallback preserves subclass
instances.
- Around line 245-247: Extend the builtin list sorting tests around the existing
sorted assertions to cover reverse=True, keyed sorting, and a custom class with
Python-level __lt__ returning NotImplemented for unrelated types. Include
reverse stability for equal keys and assertions that exercise both value and
key-vector classification paths, then run pytest -v from extra_tests with
debug-mode cargo run snippet execution.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: ae3f2b61-acdf-41a6-9e64-440d517dd850

📥 Commits

Reviewing files that changed from the base of the PR and between bb75624 and 2a02904.

📒 Files selected for processing (2)
  • crates/vm/src/builtins/list.rs
  • extra_tests/snippets/builtin_list.py

Comment thread crates/vm/src/builtins/list.rs
@moreal moreal added the z-ca-2026 Tag to track Contribution Academy 2026 label Aug 8, 2026

@youknowone youknowone 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.

Interesting, Thank you!

@youknowone
youknowone merged commit 4530ecd into RustPython:main Aug 8, 2026
27 checks passed
kyokuping pushed a commit to kyokuping/RustPython that referenced this pull request Aug 9, 2026
…8464)

* Use type-specialized comparators for homogeneous list sorts

Scan the sort keys once before sorting; when every element is exactly str, int, or float, compare wtf8 bytes / BigInt / f64 directly instead of going through rich_compare_bool dispatch, mirroring CPython's pre-sort check in listsort.c (unsafe_latin_compare and friends). Subclasses and mixed-type lists keep the generic __lt__ path.

* Add edge-case tests for specialized list sorts

* Cache the richcompare slot for homogeneous list sorts

* Specialize tuple sorts on their first elements

* Apply ruff formatting to the new list sort tests

* Deduplicate the reverse swap across sort dispatch arms
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

z-ca-2026 Tag to track Contribution Academy 2026

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use type-specialized comparators in list.sort() like CPython's listsort.c

3 participants