Skip to content

Conversation

@abbashosseinii
Copy link

This PR enhances the performance and robustness of the default unstable sort in package:collection by replacing the existing quickSort implementation with a modern, state-of-the-art Pattern-Defeating Quicksort (pdqsort) algorithm. The implementation is a Dart port of the concepts from the canonical C++ version by Orson Peters.

Motivation

The pdqsort algorithm is the default unstable sort in other high-performance ecosystems (like Rust) for good reason. It offers two key advantages over the existing implementation:

  1. Superior Performance on Common Patterns: Through more intelligent pivot selection (median-of-three/ninther), it consistently outperforms the random-pivot strategy on a wide variety of real-world data patterns.
  2. Guaranteed Worst-Case Performance: It includes a heapsort fallback mechanism that activates on pathological inputs, providing a hard guarantee of O(n log n) performance and preventing worst-case O(n²) behavior.

This change is a pure, non-breaking enhancement that makes the standard library sort faster and safer with no API changes or regressions.

Benchmark Results

To validate this enhancement, a comprehensive benchmark was created, comparing the new pdqsort implementation against a direct copy of the existing SDK quickSort across five critical data patterns.

The results demonstrate a consistent and significant performance improvement across all tested scenarios, with no regressions.

Benchmark Results:
pdqsort (Enhancement) vs. SDK quickSort (Baseline)

Data Condition Baseline (µs) Enhancement (µs) Improvement Winner
Mean
Random 7584 6654 +12.27% Enhancement 🚀
Sorted 7515 7207 +4.10% Enhancement 🚀
Reverse Sorted 7598 7341 +3.38% Enhancement 🚀
Few Unique 1741 1560 +10.40% Enhancement 🚀
Pathological 8564 8034 +6.19% Enhancement 🚀
Median
Random 7612 6672 +12.35% Enhancement 🚀
Sorted 7596 7220 +4.95% Enhancement 🚀
Reverse Sorted 7598 7363 +3.09% Enhancement 🚀
Few Unique 1712 1567 +8.47% Enhancement 🚀
Pathological 8537 8063 +5.55% Enhancement 🚀

(Results are the average/median time in microseconds to sort 50,000 integers, aggregated over 12 runs.)


  • I’ve reviewed the contributor guide and applied the relevant portions to this PR.
Contribution guidelines:

Many Dart repos have a weekly cadence for reviewing PRs - please allow for some latency before initial review feedback.

Note: The Dart team is trialing Gemini Code Assist. Don't take its comments as final Dart team feedback. Use the suggestions if they're helpful; otherwise, wait for a human reviewer.

This commit introduces a comprehensive benchmark harness to provide empirical
evidence for the proposal to replace the existing quickSort implementation
with a more performant and robust pdqsort algorithm.

The harness evaluates performance across five critical data patterns to
ensure a thorough comparison:
- **Random:** Tests performance on typical, unsorted data.
- **Sorted & Reverse Sorted:** Tests for handling of common presorted patterns.
- **Few Unique:** Tests efficiency on data with high duplication.
- **Pathological:** Tests robustness against inputs designed to cause
  worst-case behavior in quicksorts.

The baseline implementation (`quickSortBaseline`) is a direct copy of the
existing SDK code to ensure the comparison is fair and accurate. The
results from this benchmark will be used to justify the enhancement in the
accompanying pull request.
@abbashosseinii abbashosseinii requested a review from a team as a code owner November 4, 2025 21:32
@github-actions
Copy link

github-actions bot commented Nov 5, 2025

PR Health

Breaking changes ⚠️
Package Change Current Version New Version Needed Version Looking good?
collection Breaking 1.19.1 1.20.0-wip 2.0.0
Got "1.20.0-wip" expected >= "2.0.0" (breaking changes)
⚠️

This check can be disabled by tagging the PR with skip-breaking-check.

Changelog Entry
Package Changed Files
package:collection pkgs/collection/lib/src/algorithms.dart

Changes to files need to be accounted for in their respective changelogs.

This check can be disabled by tagging the PR with skip-changelog-check.

Coverage ⚠️
File Coverage
pkgs/collection/benchmark/sort_comparison_benchmark.dart 💔 Not covered
pkgs/collection/lib/src/algorithms.dart 💔 91 % ⬇️ 9 %

This check for test coverage is informational (issues shown here will not fail the PR).

This check can be disabled by tagging the PR with skip-coverage-check.

API leaks ✔️

The following packages contain symbols visible in the public API, but not exported by the library. Export these symbols or remove them from your publicly visible API.

Package Leaked API symbol Leaking sources

This check can be disabled by tagging the PR with skip-leaking-check.

License Headers ✔️
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
Files
no missing headers

All source files should start with a license header.

This check can be disabled by tagging the PR with skip-license-check.

This addresses the failing 'Changelog Entry' CI check by adding the
required entry for the quickSort implementation replacement.
@lrhn
Copy link
Member

lrhn commented Nov 5, 2025

Looks pretty convincing. I'll have to look at it a little more to see if it's so universally better that we want to replace the simple quicksort, or if we may want to have both.

I don't know how many people actually use the sort functions in package:collection instead of just using List.sort.
(So should we change List.sort?)

The only nit I noticed in a quick read-through is that I'd avoid calling keyOf more than once per element when it can be avoided. The keyOf operation may not be trivial.

@natebosch WDYT?

/// Centralized generation of datasets for all benchmarks.
///
/// Ensures all algorithms are tested on the exact same data.
class DatasetGenerator {
Copy link
Member

Choose a reason for hiding this comment

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

(Don't use classes with only static members. Put the members into a separate library that can be imported with a prefix if desired.)

Copy link
Author

Choose a reason for hiding this comment

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

You're absolutely right — using classes with only static members isn't ideal. I just wrote this file as a temporary setup so we could benchmark the new sort implementation against the old one. It’s not meant to be merged into the main codebase.

Copy link
Author

Choose a reason for hiding this comment

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

This has been resolved in this commit.

}
}

/// Represents the final aggregated result of a benchmark.
Copy link
Member

Choose a reason for hiding this comment

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

(Drop the "Represents" word. All classes represent something. Just say what that is.)

Copy link
Author

Choose a reason for hiding this comment

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

This has been resolved in the recent commit.

final left = 2 * root + 1;
final right = 2 * root + 2;
var largest = root;

Copy link
Member

Choose a reason for hiding this comment

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

(Here we could optimize:

  var largest = root;
  var rootKey = keyOf(elements[start + root]);
  while (true) {
    var child = 2 * root + 1; // Left child.
    if (child < n) {
      var largestKey = rootKey;
      var childKey = keyOf(elements[start + child]);
      if (compare(largestKey, childKey) < 0) {
        largest = child;
        largestKey = childKey;
      }
      child++; // Right child.
      if (child < n) {
        var childKey = keyOf(elements[start + child]);
        if (compare(largestKey, childKey) < 0) {
          largest = child;
          largestKey = childKey;
        }
      }    
      if (largest != root) {
        _pdqSwap(elements, start + root, start + largest);
        root = largest; 
        continue;
      }
    }    
    break;
  }

This should avoid calling keyOf on the same element more than once, including the root. (I'll assume this only gets called if the root is not a leaf.)

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the detailed review!
The latest commit reflects these updates. Please let me know if there's anything else that needs attention!

@abbashosseinii
Copy link
Author

Hi @lrn, thank you for the thoughtful review and feedback! This is a great discussion. Let me address your points directly.

...see if it's so universally better that we want to replace the simple quicksort, or if we may want to have both.

That's the key question. I believe replacing it is the right move because pdqsort is designed to be a "drop-in" replacement that is strictly better than the existing quickSort.

  • No Regressions: As the benchmarks show, it offers a performance improvement over the baseline quickSort on every tested data pattern. It doesn't trade performance in one area for another.
  • Safety Net: The heapsort fallback provides a hard O(n log n) guarantee, which the current implementation lacks. This makes the library sort more robust.

For these reasons, I don't see a scenario where a user would knowingly choose the older, less robust implementation, which argues against keeping both.

I don't know how many people actually use the sort functions in package:collection instead of just using List.sort. (So should we change List.sort?)

This was an excellent question, and it prompted me to add List.sort to the benchmark to get a clear answer. The results are fascinating and, I believe, make a strong case for this PR.

Benchmark Results: pdqsort (Enhancement) vs. SDK quickSort (Baseline) vs. List.sort

Data Condition Baseline (µs) Enhancement (µs) List.sort (µs) Enh vs Base List.sort vs Base Best
Mean
Random 7468 6546 6588 +12.34% +11.78% Enhancement 🚀
Sorted 7344 7087 2364 +3.51% +67.81% List.sort
Reverse Sorted 7351 7138 2769 +2.90% +62.33% List.sort
Few Unique 1750 1567 2314 +10.44% -32.26% Enhancement 🚀
Pathological 8531 8026 4971 +5.92% +41.73% List.sort
Median
Random 7566 6576 6588 +13.08% +12.93% Enhancement 🚀
Sorted 7353 7055 2383 +4.05% +67.59% List.sort
Reverse Sorted 7325 7154 2776 +2.33% +62.10% List.sort
Few Unique 1738 1567 2316 +9.84% -33.26% Enhancement 🚀
Pathological 8562 8018 4992 +6.35% +41.70% List.sort
  1. Native Code Wins on Structure: As expected, the VM's native C++ List.sort is unbeatable on structured data (sorted, reverse-sorted). It likely has low-level optimizations to detect sorted runs that a pure-Dart implementation can't match.

  2. This pdqsort Wins on Chaos: Most importantly, this new pdqsort implementation is the fastest of all three on chaotic data—both random data and data with few unique values.

This shows that the goal of this PR isn't to replace List.sort, but to make the pure-Dart implementation in package:collection the best it can possibly be. By doing so, we've achieved a sort that is so efficient it can even surpass the native VM's implementation for common "messy" data scenarios.

The only nit I noticed in a quick read-through is that I'd avoid calling keyOf more than once per element when it can be avoided. The keyOf operation may not be trivial.

Excellent point, and thank you for catching that! You are absolutely right. I've just pushed a new commit that caches the keys for the pivot and the currently inspected element in the partitioning and insertion sort loops. This will avoid the redundant calls and improve performance when keyOf is an expensive operation.

Thank you again for the review! Let me know if you have any other questions.

This commit refactors the _pdqSiftDown helper function, which is used as part of the heapsort fallback mechanism within the quickSort implementation.
The changes improve both performance and readability:
1- Key Caching: The key of the current largest element is now cached in a local variable (largestKey). This avoids multiple redundant calls to the keyOf function within the loop, reducing overhead.
2- Clearer Logic: The comparison logic has been restructured. Instead of potentially re-evaluating keyOf(elements[start + largest]) after the first comparison, the cached key is used, and the flow for comparing the root with its left and right children is now more explicit and easier to follow.
3- Early Exit: An early break is introduced for leaf nodes (when left >= n), which slightly simplifies the control flow.
These changes do not alter the algorithm's behavior but make the implementation more efficient and maintainable.
Updated the comment for the BenchmarkResult class to enhance clarity by rephrasing it from "Represents the final aggregated result of a benchmark" to "The final aggregated result of a benchmark." This change aims to improve the readability of the code documentation.
…ckSort and pdqsort

This commit introduces a new dataset generator for creating various data patterns used in benchmarking sorting algorithms. It also implements a comprehensive benchmark harness that compares the performance of the baseline quickSort and the enhanced pdqsort across multiple data conditions, including random, sorted, reverse sorted, few unique, and pathological datasets. The results will help evaluate the effectiveness of the pdqsort enhancement.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants