From ebe5abaea300c1ec43d5d73608af4f7cce79af65 Mon Sep 17 00:00:00 2001 From: Abbas Date: Wed, 5 Nov 2025 00:30:50 +0330 Subject: [PATCH 01/27] feat(collection): Replace quickSort with pdqsort for performance and robustness --- pkgs/collection/lib/src/algorithms.dart | 228 ++++++++++++++++++------ 1 file changed, 175 insertions(+), 53 deletions(-) diff --git a/pkgs/collection/lib/src/algorithms.dart b/pkgs/collection/lib/src/algorithms.dart index 88d1c4f8..d8ef5739 100644 --- a/pkgs/collection/lib/src/algorithms.dart +++ b/pkgs/collection/lib/src/algorithms.dart @@ -5,7 +5,7 @@ /// A selection of data manipulation algorithms. library; -import 'dart:math' show Random; +import 'dart:math' show Random, ln2, log; import 'utils.dart'; @@ -482,29 +482,42 @@ void _merge( ); } -/// Sort [elements] using a quick-sort algorithm. +// --------------------------------------------------------------------------- +// QuickSort based on Pattern-defeating Quicksort (pdqsort). +// --------------------------------------------------------------------------- + +/// Sorts a list between [start] (inclusive) and [end] (exclusive). /// -/// The elements are compared using [compare] on the elements. -/// If [start] and [end] are provided, only that range is sorted. +/// The sorting algorithm is a Pattern-defeating Quicksort (pdqsort), a +/// hybrid of Quicksort, Heapsort, and Insertion Sort. +/// It is not stable, but is typically very fast. /// -/// Uses insertion sort for smaller sublists. +/// This implementation is highly efficient for common data patterns +/// (such as sorted, reverse-sorted, or with few unique values) and has a +/// guaranteed worst-case time complexity of O(n*log(n)). +/// +/// For a stable sort, use [mergeSort]. void quickSort( List elements, int Function(E a, E b) compare, [ int start = 0, int? end, ]) { - end = RangeError.checkValidRange(start, end, elements.length); - _quickSort(elements, identity, compare, Random(), start, end); + quickSortBy(elements, identity, compare, start, end); } -/// Sort [list] using a quick-sort algorithm. +/// Sorts a list between [start] (inclusive) and [end] (exclusive) by key. /// -/// The elements are compared using [compare] on the value provided by [keyOf] -/// on the element. -/// If [start] and [end] are provided, only that range is sorted. +/// The sorting algorithm is a Pattern-defeating Quicksort (pdqsort), a +/// hybrid of Quicksort, Heapsort, and Insertion Sort. +/// It is not stable, but is typically very fast. /// -/// Uses insertion sort for smaller sublists. +/// This implementation is highly efficient for common data patterns +/// (such as sorted, reverse-sorted, or with few unique values) and has a +/// guaranteed worst-case time complexity of O(n*log(n)). +/// +/// Elements are ordered by the [compare] function applied to the result of +/// the [keyOf] function. For a stable sort, use [mergeSortBy]. void quickSortBy( List list, K Function(E element) keyOf, @@ -513,53 +526,162 @@ void quickSortBy( int? end, ]) { end = RangeError.checkValidRange(start, end, list.length); - _quickSort(list, keyOf, compare, Random(), start, end); + final length = end - start; + if (length < 2) return; + _pdqSortByImpl(list, keyOf, compare, start, end, _log2(length)); } -void _quickSort( - List list, - K Function(E element) keyOf, - int Function(K a, K b) compare, - Random random, - int start, - int end, -) { - const minQuickSortLength = 24; - var length = end - start; - while (length >= minQuickSortLength) { - var pivotIndex = random.nextInt(length) + start; - var pivot = list[pivotIndex]; - var pivotKey = keyOf(pivot); - var endSmaller = start; - var startGreater = end; - var startPivots = end - 1; - list[pivotIndex] = list[startPivots]; - list[startPivots] = pivot; - while (endSmaller < startPivots) { - var current = list[endSmaller]; - var relation = compare(keyOf(current), pivotKey); - if (relation < 0) { - endSmaller++; +/// Minimum list size below which pdqsort uses insertion sort. +const int _pdqInsertionSortThreshold = 24; + +/// Computes the base-2 logarithm of [n]. +int _log2(int n) => n == 0 ? 0 : (log(n) / ln2).floor(); + +/// Swaps the elements at positions [i] and [j] in [elements]. +void _pdqSwap(List elements, int i, int j) { + final temp = elements[i]; + elements[i] = elements[j]; + elements[j] = temp; +} + +/// A simple, non-binary insertion sort for the base case of pdqsort. +void _pdqInsertionSort(List elements, K Function(E) keyOf, + int Function(K, K) compare, int start, int end) { + for (var i = start + 1; i < end; i++) { + final current = elements[i]; + final key = keyOf(current); + var j = i - 1; + while (j >= start && compare(keyOf(elements[j]), key) > 0) { + elements[j + 1] = elements[j]; + j--; + } + elements[j + 1] = current; + } +} + +/// Heapsort implementation for the fallback case of pdqsort. +void _pdqHeapSort(List elements, K Function(E) keyOf, + int Function(K, K) compare, int start, int end) { + final n = end - start; + for (var i = n ~/ 2 - 1; i >= 0; i--) { + _pdqSiftDown(elements, keyOf, compare, i, n, start); + } + for (var i = n - 1; i > 0; i--) { + _pdqSwap(elements, start, start + i); + _pdqSiftDown(elements, keyOf, compare, 0, i, start); + } +} + +/// Sift-down operation for the heapsort fallback. +void _pdqSiftDown(List elements, K Function(E) keyOf, + int Function(K, K) compare, int i, int n, int start) { + var root = i; + while (true) { + final left = 2 * root + 1; + final right = 2 * root + 2; + var largest = root; + + if (left < n && + compare(keyOf(elements[start + largest]), + keyOf(elements[start + left])) < + 0) { + largest = left; + } + if (right < n && + compare(keyOf(elements[start + largest]), + keyOf(elements[start + right])) < + 0) { + largest = right; + } + if (largest == root) { + break; + } + _pdqSwap(elements, start + root, start + largest); + root = largest; + } +} + +/// Sorts three elements at indices [a], [b], and [c]. +void _pdqSort3(List elements, K Function(E) keyOf, + int Function(K, K) compare, int a, int b, int c) { + if (compare(keyOf(elements[a]), keyOf(elements[b])) > 0) { + _pdqSwap(elements, a, b); + } + if (compare(keyOf(elements[b]), keyOf(elements[c])) > 0) { + _pdqSwap(elements, b, c); + if (compare(keyOf(elements[a]), keyOf(elements[b])) > 0) { + _pdqSwap(elements, a, b); + } + } +} + +/// The core implementation of Pattern-defeating Quicksort. +/// +/// [badAllowed] tracks how many bad pivot selections are allowed before +/// falling back to heap sort. +void _pdqSortByImpl(List elements, K Function(E) keyOf, + int Function(K, K) compare, int start, int end, int badAllowed) { + while (true) { + final size = end - start; + if (size < _pdqInsertionSortThreshold) { + _pdqInsertionSort(elements, keyOf, compare, start, end); + return; + } + + if (badAllowed == 0) { + _pdqHeapSort(elements, keyOf, compare, start, end); + return; + } + + final mid = start + size ~/ 2; + if (size > 80) { + // Ninther pivot selection for large arrays. + final s = size ~/ 8; + _pdqSort3(elements, keyOf, compare, start, start + s, start + 2 * s); + _pdqSort3(elements, keyOf, compare, mid - s, mid, mid + s); + _pdqSort3( + elements, keyOf, compare, end - 1 - 2 * s, end - 1 - s, end - 1); + _pdqSort3(elements, keyOf, compare, start + s, mid, end - 1 - s); + } else { + // Median-of-three for smaller arrays. + _pdqSort3(elements, keyOf, compare, start, mid, end - 1); + } + + // 3-Way Partitioning (Dutch National Flag). + _pdqSwap(elements, start, mid); + final pivotKey = keyOf(elements[start]); + + var less = start; + var equal = start; + var greater = end; + + while (equal < greater) { + var comparison = compare(keyOf(elements[equal]), pivotKey); + if (comparison < 0) { + _pdqSwap(elements, less++, equal++); + } else if (comparison > 0) { + greater--; + _pdqSwap(elements, equal, greater); } else { - startPivots--; - var currentTarget = startPivots; - list[endSmaller] = list[startPivots]; - if (relation > 0) { - startGreater--; - currentTarget = startGreater; - list[startPivots] = list[startGreater]; - } - list[currentTarget] = current; + equal++; } } - if (endSmaller - start < end - startGreater) { - _quickSort(list, keyOf, compare, random, start, endSmaller); - start = startGreater; + + final leftSize = less - start; + final rightSize = end - greater; + + // Detect highly unbalanced partitions and decrement badAllowed. + if (leftSize < size ~/ 8 || rightSize < size ~/ 8) { + badAllowed--; + } + + // Recurse on the smaller partition first to keep stack depth low. + if (leftSize < rightSize) { + _pdqSortByImpl(elements, keyOf, compare, start, less, badAllowed); + start = greater; // Tail-call optimization on the larger partition } else { - _quickSort(list, keyOf, compare, random, startGreater, end); - end = endSmaller; + _pdqSortByImpl(elements, keyOf, compare, greater, end, badAllowed); + end = less; // Tail-call optimization on the larger partition } - length = end - start; } - _movingInsertionSort(list, keyOf, compare, start, end, list, start); } From ea66e1652570a8b0fe5eb9ee265e0ad5b90db645 Mon Sep 17 00:00:00 2001 From: Abbas Date: Wed, 5 Nov 2025 00:46:49 +0330 Subject: [PATCH 02/27] test(collection): add benchmark for pdqsort vs. baseline quickSort 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. --- .../benchmark/sort_comparison_benchmark.dart | 419 ++++++++++++++++++ 1 file changed, 419 insertions(+) create mode 100644 pkgs/collection/benchmark/sort_comparison_benchmark.dart diff --git a/pkgs/collection/benchmark/sort_comparison_benchmark.dart b/pkgs/collection/benchmark/sort_comparison_benchmark.dart new file mode 100644 index 00000000..7bfd7577 --- /dev/null +++ b/pkgs/collection/benchmark/sort_comparison_benchmark.dart @@ -0,0 +1,419 @@ +// 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. + +import 'dart:math'; + +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:collection/src/algorithms.dart' show quickSort; +import 'package:collection/src/utils.dart'; + +// Sink variable to prevent the compiler from optimizing away benchmark code. +int sink = 0; + +/// Centralized generation of datasets for all benchmarks. +/// +/// Ensures all algorithms are tested on the exact same data. +class DatasetGenerator { + static const size = 50000; + static const count = 128; // Number of lists to cycle through. + + static final List> random = _generateRandom(); + static final List> sorted = _generateSorted(); + static final List> reverse = _generateReverse(); + static final List> fewUnique = _generateFewUnique(); + static final List> pathological = _generatePathological(); + + static List> _generateRandom() { + final r = Random(12345); + return List.generate( + count, (_) => List.generate(size, (_) => r.nextInt(2000))); + } + + static List> _generateSorted() { + final base = List.generate(size, (i) => i); + return List.generate(count, (_) => List.from(base)); + } + + static List> _generateReverse() { + final base = List.generate(size, (i) => size - 1 - i); + return List.generate(count, (_) => List.from(base)); + } + + static List> _generateFewUnique() { + final r = Random(67890); + return List.generate( + count, (_) => List.generate(size, (_) => r.nextInt(7))); + } + + static List> _generatePathological() { + final base = List.generate(size, (i) => i); + // Creates a "V-shape" or "organ pipe" array that can be challenging + // for quicksort implementations by promoting unbalanced partitions. + final pathological = [ + for (int i = 0; i < size; i++) + if (i.isEven) base[i], + for (int i = size - 1; i > 0; i--) + if (i.isOdd) base[i], + ]; + return List.generate(count, (_) => List.from(pathological)); + } +} + +/// Represents the final aggregated result of a benchmark. +class BenchmarkResult { + final double mean; + final int median; + BenchmarkResult(this.mean, this.median); +} + +/// A base class for our sort benchmarks to reduce boilerplate. +/// Note: We extend `BenchmarkBase` for its structure but will use our own +/// timing. +abstract class SortBenchmarkBase extends BenchmarkBase { + final List> datasets; + int _iteration = 0; + int _checksum = 0; + + SortBenchmarkBase(super.name, this.datasets); + + List getNextList() { + // Cloning the list is crucial so each run sorts an unsorted list. + return List.from(datasets[_iteration++ % datasets.length]); + } + + void updateChecksum(List list) { + // A simple checksum to ensure the list is used and not optimized away. + sink ^= list.first ^ list.last ^ list[list.length >> 1] ^ _checksum++; + } + + /// The core operation to be benchmarked. + void performSort(); + + @override + void run() => performSort(); +} + +// --- Benchmark Classes --- + +// Baseline (Old SDK quickSort) +class QuickSortBaselineRandomBenchmark extends SortBenchmarkBase { + QuickSortBaselineRandomBenchmark() + : super('Baseline.Random', DatasetGenerator.random); + @override + void performSort() { + final list = getNextList(); + quickSortBaseline(list, (a, b) => a.compareTo(b)); + updateChecksum(list); + } +} + +class QuickSortBaselineSortedBenchmark extends SortBenchmarkBase { + QuickSortBaselineSortedBenchmark() + : super('Baseline.Sorted', DatasetGenerator.sorted); + @override + void performSort() { + final list = getNextList(); + quickSortBaseline(list, (a, b) => a.compareTo(b)); + updateChecksum(list); + } +} + +class QuickSortBaselineReverseBenchmark extends SortBenchmarkBase { + QuickSortBaselineReverseBenchmark() + : super('Baseline.Reverse', DatasetGenerator.reverse); + @override + void performSort() { + final list = getNextList(); + quickSortBaseline(list, (a, b) => a.compareTo(b)); + updateChecksum(list); + } +} + +class QuickSortBaselineFewUniqueBenchmark extends SortBenchmarkBase { + QuickSortBaselineFewUniqueBenchmark() + : super('Baseline.FewUnique', DatasetGenerator.fewUnique); + @override + void performSort() { + final list = getNextList(); + quickSortBaseline(list, (a, b) => a.compareTo(b)); + updateChecksum(list); + } +} + +class QuickSortBaselinePathologicalBenchmark extends SortBenchmarkBase { + QuickSortBaselinePathologicalBenchmark() + : super('Baseline.Pathological', DatasetGenerator.pathological); + @override + void performSort() { + final list = getNextList(); + quickSortBaseline(list, (a, b) => a.compareTo(b)); + updateChecksum(list); + } +} + +// Enhancement (New pdqsort) +class PdqSortEnhancementRandomBenchmark extends SortBenchmarkBase { + PdqSortEnhancementRandomBenchmark() + : super('Enhancement.Random', DatasetGenerator.random); + @override + void performSort() { + final list = getNextList(); + quickSort(list, (a, b) => a.compareTo(b)); + updateChecksum(list); + } +} + +class PdqSortEnhancementSortedBenchmark extends SortBenchmarkBase { + PdqSortEnhancementSortedBenchmark() + : super('Enhancement.Sorted', DatasetGenerator.sorted); + @override + void performSort() { + final list = getNextList(); + quickSort(list, (a, b) => a.compareTo(b)); + updateChecksum(list); + } +} + +class PdqSortEnhancementReverseBenchmark extends SortBenchmarkBase { + PdqSortEnhancementReverseBenchmark() + : super('Enhancement.Reverse', DatasetGenerator.reverse); + @override + void performSort() { + final list = getNextList(); + quickSort(list, (a, b) => a.compareTo(b)); + updateChecksum(list); + } +} + +class PdqSortEnhancementFewUniqueBenchmark extends SortBenchmarkBase { + PdqSortEnhancementFewUniqueBenchmark() + : super('Enhancement.FewUnique', DatasetGenerator.fewUnique); + @override + void performSort() { + final list = getNextList(); + quickSort(list, (a, b) => a.compareTo(b)); + updateChecksum(list); + } +} + +class PdqSortEnhancementPathologicalBenchmark extends SortBenchmarkBase { + PdqSortEnhancementPathologicalBenchmark() + : super('Enhancement.Pathological', DatasetGenerator.pathological); + @override + void performSort() { + final list = getNextList(); + quickSort(list, (a, b) => a.compareTo(b)); + updateChecksum(list); + } +} + +// --- Main Execution Logic --- + +void main() { + const samples = 12; + + final benchmarks = [ + ( + 'Random', + QuickSortBaselineRandomBenchmark(), + PdqSortEnhancementRandomBenchmark() + ), + ( + 'Sorted', + QuickSortBaselineSortedBenchmark(), + PdqSortEnhancementSortedBenchmark() + ), + ( + 'Reverse Sorted', + QuickSortBaselineReverseBenchmark(), + PdqSortEnhancementReverseBenchmark() + ), + ( + 'Few Unique', + QuickSortBaselineFewUniqueBenchmark(), + PdqSortEnhancementFewUniqueBenchmark() + ), + ( + 'Pathological', + QuickSortBaselinePathologicalBenchmark(), + PdqSortEnhancementPathologicalBenchmark() + ), + ]; + + final results = {}; + + print('Running benchmarks ($samples samples each)...'); + for (final (condition, baseline, enhancement) in benchmarks) { + final baselineResult = _runBenchmark(baseline, samples); + final enhancementResult = _runBenchmark(enhancement, samples); + results[condition] = (baselineResult, enhancementResult); + } + + _printResultsAsMarkdownTable(results); +} + +BenchmarkResult _runBenchmark(SortBenchmarkBase benchmark, int samples) { + final times = []; + // Warmup run (not timed). + benchmark.run(); + for (var i = 0; i < samples; i++) { + final stopwatch = Stopwatch()..start(); + benchmark.run(); + stopwatch.stop(); + times.add(stopwatch.elapsedMicroseconds); + } + times.sort(); + final mean = times.reduce((a, b) => a + b) / samples; + final median = times[samples >> 1]; + return BenchmarkResult(mean, median); +} + +void _printResultsAsMarkdownTable( + Map results) { + final separator = '=' * 80; + print('\n$separator'); + print( + 'Benchmark Results: pdqsort (Enhancement) vs. SDK quickSort (Baseline)'); + print(separator); + print( + '''| Data Condition | Baseline (ยตs) | Enhancement (ยตs) | Improvement | Winner |'''); + print( + '''| :------------------ | :------------ | :--------------- | :---------- | :------------ |'''); + print( + '''| *Mean* | | | | |'''); + + for (final entry in results.entries) { + final condition = entry.key; + final (baseline, enhancement) = entry.value; + + final improvement = + (baseline.mean - enhancement.mean) / baseline.mean * 100; + final winner = improvement > 0 ? 'Enhancement ๐Ÿš€' : 'Baseline'; + final improvementString = + '${improvement > 0 ? '+' : ''}${improvement.toStringAsFixed(2)}%'; + + final baselineMean = baseline.mean.round(); + final enhancementMean = enhancement.mean.round(); + + print( + '''| ${condition.padRight(19)} | ${baselineMean.toString().padLeft(13)} | ${enhancementMean.toString().padLeft(16)} | ${improvementString.padLeft(11)} | $winner |'''); + } + + print( + '''| *Median* | | | | |'''); + + for (final entry in results.entries) { + final condition = entry.key; + final (baseline, enhancement) = entry.value; + + final improvement = + (baseline.median - enhancement.median) / baseline.median * 100; + final winner = improvement > 0 ? 'Enhancement ๐Ÿš€' : 'Baseline'; + final improvementString = + '${improvement > 0 ? '+' : ''}${improvement.toStringAsFixed(2)}%'; + + // No rounding needed for median as it's an int. + final baselineMedian = baseline.median; + final enhancementMedian = enhancement.median; + + print( + '''| ${condition.padRight(19)} | ${baselineMedian.toString().padLeft(13)} | ${enhancementMedian.toString().padLeft(16)} | ${improvementString.padLeft(11)} | $winner |'''); + } + + print(separator); +} + +// =========================================================================== +// BASELINE IMPLEMENTATION +// A direct copy of the original SDK quickSort, renamed to avoid conflicts. +// =========================================================================== + +void quickSortBaseline( + List elements, + int Function(E a, E b) compare, [ + int start = 0, + int? end, +]) { + end = RangeError.checkValidRange(start, end, elements.length); + _quickSortBaseline(elements, identity, compare, Random(), start, end); +} + +void _quickSortBaseline( + List list, + K Function(E element) keyOf, + int Function(K a, K b) compare, + Random random, + int start, + int end, +) { + const minQuickSortLength = 32; + var length = end - start; + while (length >= minQuickSortLength) { + var pivotIndex = random.nextInt(length) + start; + var pivot = list[pivotIndex]; + var pivotKey = keyOf(pivot); + var endSmaller = start; + var startGreater = end; + var startPivots = end - 1; + list[pivotIndex] = list[startPivots]; + list[startPivots] = pivot; + while (endSmaller < startPivots) { + var current = list[endSmaller]; + var relation = compare(keyOf(current), pivotKey); + if (relation < 0) { + endSmaller++; + } else { + startPivots--; + var currentTarget = startPivots; + list[endSmaller] = list[startPivots]; + if (relation > 0) { + startGreater--; + currentTarget = startGreater; + list[startPivots] = list[startGreater]; + } + list[currentTarget] = current; + } + } + if (endSmaller - start < end - startGreater) { + _quickSortBaseline(list, keyOf, compare, random, start, endSmaller); + start = startGreater; + } else { + _quickSortBaseline(list, keyOf, compare, random, startGreater, end); + end = endSmaller; + } + length = end - start; + } + _movingInsertionSortBaseline( + list, keyOf, compare, start, end, list, start); +} + +void _movingInsertionSortBaseline( + List list, + K Function(E element) keyOf, + int Function(K, K) compare, + int start, + int end, + List target, + int targetOffset, +) { + var length = end - start; + if (length == 0) return; + target[targetOffset] = list[start]; + for (var i = 1; i < length; i++) { + var element = list[start + i]; + var elementKey = keyOf(element); + var min = targetOffset; + var max = targetOffset + i; + while (min < max) { + var mid = min + ((max - min) >> 1); + if (compare(elementKey, keyOf(target[mid])) < 0) { + max = mid; + } else { + min = mid + 1; + } + } + target.setRange(min + 1, targetOffset + i + 1, target, min); + target[min] = element; + } +} From 307ec4831d8d94a22f779b1013cb1fe3a999ca63 Mon Sep 17 00:00:00 2001 From: Abbas Date: Wed, 5 Nov 2025 08:56:50 +0330 Subject: [PATCH 03/27] docs(collection): add changelog for quickSort pdqsort enhancement This addresses the failing 'Changelog Entry' CI check by adding the required entry for the quickSort implementation replacement. --- pkgs/collection/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkgs/collection/CHANGELOG.md b/pkgs/collection/CHANGELOG.md index 743fddf2..3063b309 100644 --- a/pkgs/collection/CHANGELOG.md +++ b/pkgs/collection/CHANGELOG.md @@ -9,6 +9,8 @@ - Add `PriorityQueue.of` constructor and optimize adding many elements. - Address diagnostics from `strict_top_level_inference`. - Run `dart format` with the new style. +- Replace `quickSort` implementation with a more performant and robust + Pattern-defeating Quicksort (pdqsort) algorithm. ## 1.19.1 From 009090e641860c6e3edeff27c60eeb77d9f42836 Mon Sep 17 00:00:00 2001 From: Abbas Date: Wed, 5 Nov 2025 20:42:28 +0330 Subject: [PATCH 04/27] refactor(core): Optimize and clarify _pdqSiftDown in heapsort 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. --- pkgs/collection/lib/src/algorithms.dart | 37 +++++++++++++++---------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/pkgs/collection/lib/src/algorithms.dart b/pkgs/collection/lib/src/algorithms.dart index d8ef5739..652bdd4e 100644 --- a/pkgs/collection/lib/src/algorithms.dart +++ b/pkgs/collection/lib/src/algorithms.dart @@ -578,24 +578,31 @@ void _pdqSiftDown(List elements, K Function(E) keyOf, var root = i; while (true) { final left = 2 * root + 1; - final right = 2 * root + 2; - var largest = root; + if (left >= n) break; // Root is a leaf. - if (left < n && - compare(keyOf(elements[start + largest]), - keyOf(elements[start + left])) < - 0) { - largest = left; - } - if (right < n && - compare(keyOf(elements[start + largest]), - keyOf(elements[start + right])) < - 0) { - largest = right; + var largest = root; + var largestKey = keyOf(elements[start + largest]); + + // Compare with left child. + var child = left; + var childKey = keyOf(elements[start + child]); + if (compare(largestKey, childKey) < 0) { + largest = child; + largestKey = childKey; } - if (largest == root) { - break; + + // Compare with right child if it exists. + child = left + 1; + if (child < n) { + childKey = keyOf(elements[start + child]); + if (compare(largestKey, childKey) < 0) { + largest = child; + largestKey = childKey; + } } + + if (largest == root) break; + _pdqSwap(elements, start + root, start + largest); root = largest; } From 3c80f38f1dbc0be2e9f6836577ce6763c8699068 Mon Sep 17 00:00:00 2001 From: Abbas Date: Wed, 5 Nov 2025 20:49:36 +0330 Subject: [PATCH 05/27] docs(benchmark): Improve documentation clarity for BenchmarkResult class 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. --- pkgs/collection/benchmark/sort_comparison_benchmark.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/collection/benchmark/sort_comparison_benchmark.dart b/pkgs/collection/benchmark/sort_comparison_benchmark.dart index 7bfd7577..31210ef1 100644 --- a/pkgs/collection/benchmark/sort_comparison_benchmark.dart +++ b/pkgs/collection/benchmark/sort_comparison_benchmark.dart @@ -60,7 +60,7 @@ class DatasetGenerator { } } -/// Represents the final aggregated result of a benchmark. +/// The final aggregated result of a benchmark. class BenchmarkResult { final double mean; final int median; From 5c3d1ef45b66bd0d30c5bf11b25ef30cd224c4c3 Mon Sep 17 00:00:00 2001 From: Abbas Date: Wed, 5 Nov 2025 21:21:08 +0330 Subject: [PATCH 06/27] feat(benchmark): Add dataset generator and sorting benchmarks for quickSort 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. --- .../benchmark/dataset_generator.dart | 54 ++++++++++++++ ...son_benchmark.dart => sort_benchmark.dart} | 73 ++++--------------- 2 files changed, 67 insertions(+), 60 deletions(-) create mode 100644 pkgs/collection/benchmark/dataset_generator.dart rename pkgs/collection/benchmark/{sort_comparison_benchmark.dart => sort_benchmark.dart} (81%) diff --git a/pkgs/collection/benchmark/dataset_generator.dart b/pkgs/collection/benchmark/dataset_generator.dart new file mode 100644 index 00000000..b51c07f5 --- /dev/null +++ b/pkgs/collection/benchmark/dataset_generator.dart @@ -0,0 +1,54 @@ +// 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. + +/// Centralized generation of datasets for all benchmarks. +/// +/// Ensures all algorithms are tested on the exact same data. +library; + + +import 'dart:math'; + +const size = 50000; +const count = 128; // Number of lists to cycle through. + +final List> random = _generateRandom(); +final List> sorted = _generateSorted(); +final List> reverse = _generateReverse(); +final List> fewUnique = _generateFewUnique(); +final List> pathological = _generatePathological(); + +List> _generateRandom() { + final r = Random(12345); + return List.generate( + count, (_) => List.generate(size, (_) => r.nextInt(2000))); +} + +List> _generateSorted() { + final base = List.generate(size, (i) => i); + return List.generate(count, (_) => List.from(base)); +} + +List> _generateReverse() { + final base = List.generate(size, (i) => size - 1 - i); + return List.generate(count, (_) => List.from(base)); +} + +List> _generateFewUnique() { + final r = Random(67890); + return List.generate(count, (_) => List.generate(size, (_) => r.nextInt(7))); +} + +List> _generatePathological() { + final base = List.generate(size, (i) => i); + // Creates a "V-shape" or "organ pipe" array that can be challenging + // for quicksort implementations by promoting unbalanced partitions. + final pathological = [ + for (int i = 0; i < size; i++) + if (i.isEven) base[i], + for (int i = size - 1; i > 0; i--) + if (i.isOdd) base[i], + ]; + return List.generate(count, (_) => List.from(pathological)); +} \ No newline at end of file diff --git a/pkgs/collection/benchmark/sort_comparison_benchmark.dart b/pkgs/collection/benchmark/sort_benchmark.dart similarity index 81% rename from pkgs/collection/benchmark/sort_comparison_benchmark.dart rename to pkgs/collection/benchmark/sort_benchmark.dart index 31210ef1..b4acd587 100644 --- a/pkgs/collection/benchmark/sort_comparison_benchmark.dart +++ b/pkgs/collection/benchmark/sort_benchmark.dart @@ -8,58 +8,11 @@ import 'package:benchmark_harness/benchmark_harness.dart'; import 'package:collection/src/algorithms.dart' show quickSort; import 'package:collection/src/utils.dart'; +import 'dataset_generator.dart' as dataset_generator; + // Sink variable to prevent the compiler from optimizing away benchmark code. int sink = 0; -/// Centralized generation of datasets for all benchmarks. -/// -/// Ensures all algorithms are tested on the exact same data. -class DatasetGenerator { - static const size = 50000; - static const count = 128; // Number of lists to cycle through. - - static final List> random = _generateRandom(); - static final List> sorted = _generateSorted(); - static final List> reverse = _generateReverse(); - static final List> fewUnique = _generateFewUnique(); - static final List> pathological = _generatePathological(); - - static List> _generateRandom() { - final r = Random(12345); - return List.generate( - count, (_) => List.generate(size, (_) => r.nextInt(2000))); - } - - static List> _generateSorted() { - final base = List.generate(size, (i) => i); - return List.generate(count, (_) => List.from(base)); - } - - static List> _generateReverse() { - final base = List.generate(size, (i) => size - 1 - i); - return List.generate(count, (_) => List.from(base)); - } - - static List> _generateFewUnique() { - final r = Random(67890); - return List.generate( - count, (_) => List.generate(size, (_) => r.nextInt(7))); - } - - static List> _generatePathological() { - final base = List.generate(size, (i) => i); - // Creates a "V-shape" or "organ pipe" array that can be challenging - // for quicksort implementations by promoting unbalanced partitions. - final pathological = [ - for (int i = 0; i < size; i++) - if (i.isEven) base[i], - for (int i = size - 1; i > 0; i--) - if (i.isOdd) base[i], - ]; - return List.generate(count, (_) => List.from(pathological)); - } -} - /// The final aggregated result of a benchmark. class BenchmarkResult { final double mean; @@ -99,7 +52,7 @@ abstract class SortBenchmarkBase extends BenchmarkBase { // Baseline (Old SDK quickSort) class QuickSortBaselineRandomBenchmark extends SortBenchmarkBase { QuickSortBaselineRandomBenchmark() - : super('Baseline.Random', DatasetGenerator.random); + : super('Baseline.Random', dataset_generator.random); @override void performSort() { final list = getNextList(); @@ -110,7 +63,7 @@ class QuickSortBaselineRandomBenchmark extends SortBenchmarkBase { class QuickSortBaselineSortedBenchmark extends SortBenchmarkBase { QuickSortBaselineSortedBenchmark() - : super('Baseline.Sorted', DatasetGenerator.sorted); + : super('Baseline.Sorted', dataset_generator.sorted); @override void performSort() { final list = getNextList(); @@ -121,7 +74,7 @@ class QuickSortBaselineSortedBenchmark extends SortBenchmarkBase { class QuickSortBaselineReverseBenchmark extends SortBenchmarkBase { QuickSortBaselineReverseBenchmark() - : super('Baseline.Reverse', DatasetGenerator.reverse); + : super('Baseline.Reverse', dataset_generator.reverse); @override void performSort() { final list = getNextList(); @@ -132,7 +85,7 @@ class QuickSortBaselineReverseBenchmark extends SortBenchmarkBase { class QuickSortBaselineFewUniqueBenchmark extends SortBenchmarkBase { QuickSortBaselineFewUniqueBenchmark() - : super('Baseline.FewUnique', DatasetGenerator.fewUnique); + : super('Baseline.FewUnique', dataset_generator.fewUnique); @override void performSort() { final list = getNextList(); @@ -143,7 +96,7 @@ class QuickSortBaselineFewUniqueBenchmark extends SortBenchmarkBase { class QuickSortBaselinePathologicalBenchmark extends SortBenchmarkBase { QuickSortBaselinePathologicalBenchmark() - : super('Baseline.Pathological', DatasetGenerator.pathological); + : super('Baseline.Pathological', dataset_generator.pathological); @override void performSort() { final list = getNextList(); @@ -155,7 +108,7 @@ class QuickSortBaselinePathologicalBenchmark extends SortBenchmarkBase { // Enhancement (New pdqsort) class PdqSortEnhancementRandomBenchmark extends SortBenchmarkBase { PdqSortEnhancementRandomBenchmark() - : super('Enhancement.Random', DatasetGenerator.random); + : super('Enhancement.Random', dataset_generator.random); @override void performSort() { final list = getNextList(); @@ -166,7 +119,7 @@ class PdqSortEnhancementRandomBenchmark extends SortBenchmarkBase { class PdqSortEnhancementSortedBenchmark extends SortBenchmarkBase { PdqSortEnhancementSortedBenchmark() - : super('Enhancement.Sorted', DatasetGenerator.sorted); + : super('Enhancement.Sorted', dataset_generator.sorted); @override void performSort() { final list = getNextList(); @@ -177,7 +130,7 @@ class PdqSortEnhancementSortedBenchmark extends SortBenchmarkBase { class PdqSortEnhancementReverseBenchmark extends SortBenchmarkBase { PdqSortEnhancementReverseBenchmark() - : super('Enhancement.Reverse', DatasetGenerator.reverse); + : super('Enhancement.Reverse', dataset_generator.reverse); @override void performSort() { final list = getNextList(); @@ -188,7 +141,7 @@ class PdqSortEnhancementReverseBenchmark extends SortBenchmarkBase { class PdqSortEnhancementFewUniqueBenchmark extends SortBenchmarkBase { PdqSortEnhancementFewUniqueBenchmark() - : super('Enhancement.FewUnique', DatasetGenerator.fewUnique); + : super('Enhancement.FewUnique', dataset_generator.fewUnique); @override void performSort() { final list = getNextList(); @@ -199,7 +152,7 @@ class PdqSortEnhancementFewUniqueBenchmark extends SortBenchmarkBase { class PdqSortEnhancementPathologicalBenchmark extends SortBenchmarkBase { PdqSortEnhancementPathologicalBenchmark() - : super('Enhancement.Pathological', DatasetGenerator.pathological); + : super('Enhancement.Pathological', dataset_generator.pathological); @override void performSort() { final list = getNextList(); @@ -416,4 +369,4 @@ void _movingInsertionSortBaseline( target.setRange(min + 1, targetOffset + i + 1, target, min); target[min] = element; } -} +} \ No newline at end of file From a6e53f9babc4a712e7438b54d206998bb3bdfaa3 Mon Sep 17 00:00:00 2001 From: Abbas Date: Sat, 15 Nov 2025 20:54:40 +0330 Subject: [PATCH 07/27] chore: format code with `dart format` --- pkgs/collection/benchmark/dataset_generator.dart | 3 +-- pkgs/collection/benchmark/sort_benchmark.dart | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pkgs/collection/benchmark/dataset_generator.dart b/pkgs/collection/benchmark/dataset_generator.dart index b51c07f5..2a7a0403 100644 --- a/pkgs/collection/benchmark/dataset_generator.dart +++ b/pkgs/collection/benchmark/dataset_generator.dart @@ -7,7 +7,6 @@ /// Ensures all algorithms are tested on the exact same data. library; - import 'dart:math'; const size = 50000; @@ -51,4 +50,4 @@ List> _generatePathological() { if (i.isOdd) base[i], ]; return List.generate(count, (_) => List.from(pathological)); -} \ No newline at end of file +} diff --git a/pkgs/collection/benchmark/sort_benchmark.dart b/pkgs/collection/benchmark/sort_benchmark.dart index b4acd587..83c3307f 100644 --- a/pkgs/collection/benchmark/sort_benchmark.dart +++ b/pkgs/collection/benchmark/sort_benchmark.dart @@ -369,4 +369,4 @@ void _movingInsertionSortBaseline( target.setRange(min + 1, targetOffset + i + 1, target, min); target[min] = element; } -} \ No newline at end of file +} From ec130790a00d75a27078742a1320829e2b3d8bfc Mon Sep 17 00:00:00 2001 From: Abbas Date: Sat, 15 Nov 2025 21:50:25 +0330 Subject: [PATCH 08/27] refactor(algorithms): Remove unused imports and optimize _log2 function --- pkgs/collection/lib/src/algorithms.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkgs/collection/lib/src/algorithms.dart b/pkgs/collection/lib/src/algorithms.dart index 652bdd4e..c38f24ee 100644 --- a/pkgs/collection/lib/src/algorithms.dart +++ b/pkgs/collection/lib/src/algorithms.dart @@ -5,7 +5,7 @@ /// A selection of data manipulation algorithms. library; -import 'dart:math' show Random, ln2, log; +import 'dart:math' show Random; import 'utils.dart'; @@ -535,7 +535,11 @@ void quickSortBy( const int _pdqInsertionSortThreshold = 24; /// Computes the base-2 logarithm of [n]. -int _log2(int n) => n == 0 ? 0 : (log(n) / ln2).floor(); +/// Computes the base-2 logarithm of [n]. +/// +/// Uses bitLength to compute the floor(log2(n)) efficiently. For n == 0 +/// we return 0. +int _log2(int n) => n > 0 ? n.bitLength - 1 : 0; /// Swaps the elements at positions [i] and [j] in [elements]. void _pdqSwap(List elements, int i, int j) { From 4073118d319a0c007c968c2059122f910d4d0cf7 Mon Sep 17 00:00:00 2001 From: Abbas Date: Sat, 15 Nov 2025 22:50:42 +0330 Subject: [PATCH 09/27] refactor(sort): Optimize `_pdqSort3` based on @lrhn's suggestion Caches elements and keys in local variables within `_pdqSort3` to reduce `keyOf` calls from a potential 6 down to 3, and to minimize list access during pivot selection. --- pkgs/collection/lib/src/algorithms.dart | 27 +++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/pkgs/collection/lib/src/algorithms.dart b/pkgs/collection/lib/src/algorithms.dart index c38f24ee..345added 100644 --- a/pkgs/collection/lib/src/algorithms.dart +++ b/pkgs/collection/lib/src/algorithms.dart @@ -615,15 +615,30 @@ void _pdqSiftDown(List elements, K Function(E) keyOf, /// Sorts three elements at indices [a], [b], and [c]. void _pdqSort3(List elements, K Function(E) keyOf, int Function(K, K) compare, int a, int b, int c) { - if (compare(keyOf(elements[a]), keyOf(elements[b])) > 0) { - _pdqSwap(elements, a, b); + var elementA = elements[a]; + var keyA = keyOf(elementA); + var elementB = elements[b]; + var keyB = keyOf(elementB); + var elementC = elements[c]; + var keyC = keyOf(elementC); + + if (compare(keyA, keyB) > 0) { + (elementA, elementB) = (elementB, elementA); + (keyA, keyB) = (keyB, keyA); } - if (compare(keyOf(elements[b]), keyOf(elements[c])) > 0) { - _pdqSwap(elements, b, c); - if (compare(keyOf(elements[a]), keyOf(elements[b])) > 0) { - _pdqSwap(elements, a, b); + + if (compare(keyB, keyC) > 0) { + (elementB, elementC) = (elementC, elementB); + (keyB, keyC) = (keyC, keyB); + if (compare(keyA, keyB) > 0) { + (elementA, elementB) = (elementB, elementA); + (keyA, keyB) = (keyB, keyA); } } + + elements[a] = elementA; + elements[b] = elementB; + elements[c] = elementC; } /// The core implementation of Pattern-defeating Quicksort. From f530baee8f9b303bf346a49d2292bd24823b17d9 Mon Sep 17 00:00:00 2001 From: Abbas Date: Sun, 16 Nov 2025 08:11:29 +0330 Subject: [PATCH 10/27] refactor(benchmark): Replace `getNextList` method with a getter to follow Effective Dart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I removed the leading "get" from method names and updated call sites so the API doesn't start method names with "get". This change follows the Effective Dart guideline: avoid starting a method name with get โ€” see https://dart.dev/effective-dart/design#avoid-starting-a-method-name-with-get --- pkgs/collection/benchmark/sort_benchmark.dart | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/pkgs/collection/benchmark/sort_benchmark.dart b/pkgs/collection/benchmark/sort_benchmark.dart index 83c3307f..9c5b1bfd 100644 --- a/pkgs/collection/benchmark/sort_benchmark.dart +++ b/pkgs/collection/benchmark/sort_benchmark.dart @@ -30,10 +30,8 @@ abstract class SortBenchmarkBase extends BenchmarkBase { SortBenchmarkBase(super.name, this.datasets); - List getNextList() { - // Cloning the list is crucial so each run sorts an unsorted list. - return List.from(datasets[_iteration++ % datasets.length]); - } + List get nextList => + List.from(datasets[_iteration++ % datasets.length]); void updateChecksum(List list) { // A simple checksum to ensure the list is used and not optimized away. @@ -55,7 +53,7 @@ class QuickSortBaselineRandomBenchmark extends SortBenchmarkBase { : super('Baseline.Random', dataset_generator.random); @override void performSort() { - final list = getNextList(); + final list = nextList; quickSortBaseline(list, (a, b) => a.compareTo(b)); updateChecksum(list); } @@ -66,7 +64,7 @@ class QuickSortBaselineSortedBenchmark extends SortBenchmarkBase { : super('Baseline.Sorted', dataset_generator.sorted); @override void performSort() { - final list = getNextList(); + final list = nextList; quickSortBaseline(list, (a, b) => a.compareTo(b)); updateChecksum(list); } @@ -77,7 +75,7 @@ class QuickSortBaselineReverseBenchmark extends SortBenchmarkBase { : super('Baseline.Reverse', dataset_generator.reverse); @override void performSort() { - final list = getNextList(); + final list = nextList; quickSortBaseline(list, (a, b) => a.compareTo(b)); updateChecksum(list); } @@ -88,7 +86,7 @@ class QuickSortBaselineFewUniqueBenchmark extends SortBenchmarkBase { : super('Baseline.FewUnique', dataset_generator.fewUnique); @override void performSort() { - final list = getNextList(); + final list = nextList; quickSortBaseline(list, (a, b) => a.compareTo(b)); updateChecksum(list); } @@ -99,7 +97,7 @@ class QuickSortBaselinePathologicalBenchmark extends SortBenchmarkBase { : super('Baseline.Pathological', dataset_generator.pathological); @override void performSort() { - final list = getNextList(); + final list = nextList; quickSortBaseline(list, (a, b) => a.compareTo(b)); updateChecksum(list); } @@ -111,7 +109,7 @@ class PdqSortEnhancementRandomBenchmark extends SortBenchmarkBase { : super('Enhancement.Random', dataset_generator.random); @override void performSort() { - final list = getNextList(); + final list = nextList; quickSort(list, (a, b) => a.compareTo(b)); updateChecksum(list); } @@ -122,7 +120,7 @@ class PdqSortEnhancementSortedBenchmark extends SortBenchmarkBase { : super('Enhancement.Sorted', dataset_generator.sorted); @override void performSort() { - final list = getNextList(); + final list = nextList; quickSort(list, (a, b) => a.compareTo(b)); updateChecksum(list); } @@ -133,7 +131,7 @@ class PdqSortEnhancementReverseBenchmark extends SortBenchmarkBase { : super('Enhancement.Reverse', dataset_generator.reverse); @override void performSort() { - final list = getNextList(); + final list = nextList; quickSort(list, (a, b) => a.compareTo(b)); updateChecksum(list); } @@ -144,7 +142,7 @@ class PdqSortEnhancementFewUniqueBenchmark extends SortBenchmarkBase { : super('Enhancement.FewUnique', dataset_generator.fewUnique); @override void performSort() { - final list = getNextList(); + final list = nextList; quickSort(list, (a, b) => a.compareTo(b)); updateChecksum(list); } @@ -155,7 +153,7 @@ class PdqSortEnhancementPathologicalBenchmark extends SortBenchmarkBase { : super('Enhancement.Pathological', dataset_generator.pathological); @override void performSort() { - final list = getNextList(); + final list = nextList; quickSort(list, (a, b) => a.compareTo(b)); updateChecksum(list); } From 4a239f2248f37076c7c5d09f82226421e466445c Mon Sep 17 00:00:00 2001 From: Abbas <121819986+abbashosseinii@users.noreply.github.com> Date: Sun, 16 Nov 2025 08:14:32 +0330 Subject: [PATCH 11/27] Update pkgs/collection/lib/src/algorithms.dart Co-authored-by: Nate Bosch --- pkgs/collection/lib/src/algorithms.dart | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkgs/collection/lib/src/algorithms.dart b/pkgs/collection/lib/src/algorithms.dart index 345added..5b1268b0 100644 --- a/pkgs/collection/lib/src/algorithms.dart +++ b/pkgs/collection/lib/src/algorithms.dart @@ -482,10 +482,6 @@ void _merge( ); } -// --------------------------------------------------------------------------- -// QuickSort based on Pattern-defeating Quicksort (pdqsort). -// --------------------------------------------------------------------------- - /// Sorts a list between [start] (inclusive) and [end] (exclusive). /// /// The sorting algorithm is a Pattern-defeating Quicksort (pdqsort), a From 660314a5926b7e8d4815efdb3d7f7446987d2206 Mon Sep 17 00:00:00 2001 From: Abbas Date: Sun, 16 Nov 2025 08:34:36 +0330 Subject: [PATCH 12/27] fix(dataset_generator): Adjust random dataset generation to use full range of values --- pkgs/collection/benchmark/dataset_generator.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/collection/benchmark/dataset_generator.dart b/pkgs/collection/benchmark/dataset_generator.dart index 2a7a0403..5157bdc1 100644 --- a/pkgs/collection/benchmark/dataset_generator.dart +++ b/pkgs/collection/benchmark/dataset_generator.dart @@ -21,7 +21,7 @@ final List> pathological = _generatePathological(); List> _generateRandom() { final r = Random(12345); return List.generate( - count, (_) => List.generate(size, (_) => r.nextInt(2000))); + count, (_) => List.generate(size, (_) => r.nextInt(size))); } List> _generateSorted() { From 3b64a20e115803687f4a3e190425a35d222cda6a Mon Sep 17 00:00:00 2001 From: Abbas Date: Sun, 16 Nov 2025 09:44:52 +0330 Subject: [PATCH 13/27] Refactor: Improve list generation in _generatePathological Refine the list creation to be more explicit and performant. - Use a more descriptive name `sortedBase` instead of `base`. - Create the initial `sortedBase` list with `growable: false` as it is never modified. - Use `List.of` and explicitly set `growable: true` for the final copies to make the intent clear that they are mutable. --- pkgs/collection/benchmark/dataset_generator.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkgs/collection/benchmark/dataset_generator.dart b/pkgs/collection/benchmark/dataset_generator.dart index 5157bdc1..ca208f03 100644 --- a/pkgs/collection/benchmark/dataset_generator.dart +++ b/pkgs/collection/benchmark/dataset_generator.dart @@ -40,14 +40,14 @@ List> _generateFewUnique() { } List> _generatePathological() { - final base = List.generate(size, (i) => i); + final sortedBase = List.generate(size, (i) => i, growable: false); // Creates a "V-shape" or "organ pipe" array that can be challenging // for quicksort implementations by promoting unbalanced partitions. final pathological = [ - for (int i = 0; i < size; i++) - if (i.isEven) base[i], - for (int i = size - 1; i > 0; i--) - if (i.isOdd) base[i], + for (var i = 0; i < size; i++) + if (i.isEven) sortedBase[i], + for (var i = size - 1; i > 0; i--) + if (i.isOdd) sortedBase[i], ]; - return List.generate(count, (_) => List.from(pathological)); + return List.generate(count, (_) => List.of(pathological, growable: true)); } From d5d4f52ff4f7b4c61ee3620ddc4e72da5beb4905 Mon Sep 17 00:00:00 2001 From: Abbas Date: Sun, 16 Nov 2025 09:55:28 +0330 Subject: [PATCH 14/27] Refactor: Improve list creation in _generateReverse Make the list generation in the `_generateReverse` helper more explicit and performant. - The `base` list is now created with `growable: false`, as it's a read-only template. - The final copies are created using `List.of` with an explicit `growable: true` to clearly signal that they are mutable. --- pkgs/collection/benchmark/dataset_generator.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/collection/benchmark/dataset_generator.dart b/pkgs/collection/benchmark/dataset_generator.dart index ca208f03..44de9ebb 100644 --- a/pkgs/collection/benchmark/dataset_generator.dart +++ b/pkgs/collection/benchmark/dataset_generator.dart @@ -30,8 +30,8 @@ List> _generateSorted() { } List> _generateReverse() { - final base = List.generate(size, (i) => size - 1 - i); - return List.generate(count, (_) => List.from(base)); + final base = List.generate(size, (i) => size - 1 - i, growable: false); + return List.generate(count, (_) => List.of(base, growable: true)); } List> _generateFewUnique() { From 6546d0cf6a1912aa78df93fa5361f3c93601beea Mon Sep 17 00:00:00 2001 From: Abbas Date: Sun, 16 Nov 2025 09:57:02 +0330 Subject: [PATCH 15/27] Refactor: Improve list creation in _generateSorted Make the list generation in the `_generateSorted` helper more explicit and performant. - The `base` list is now created with `growable: false` as it's a read-only template. - The final copies are created using `List.of` with an explicit `growable: true` to clearly signal that they are mutable. --- pkgs/collection/benchmark/dataset_generator.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/collection/benchmark/dataset_generator.dart b/pkgs/collection/benchmark/dataset_generator.dart index 44de9ebb..ef94d14b 100644 --- a/pkgs/collection/benchmark/dataset_generator.dart +++ b/pkgs/collection/benchmark/dataset_generator.dart @@ -25,8 +25,8 @@ List> _generateRandom() { } List> _generateSorted() { - final base = List.generate(size, (i) => i); - return List.generate(count, (_) => List.from(base)); + final base = List.generate(size, (i) => i, growable: false); + return List.generate(count, (_) => List.of(base, growable: true)); } List> _generateReverse() { From 5122e32da83977957dd28a7308ac34196495f8f2 Mon Sep 17 00:00:00 2001 From: Abbas Date: Sun, 16 Nov 2025 10:49:25 +0330 Subject: [PATCH 16/27] Refactor(benchmark): Pre-compute deterministic base lists Move the generation of `sorted`, `reverse`, and `pathological` base data into top-level `final` variables. - **Efficiency:** Ensures these lists are computed only once at startup. `_generatePathological` now correctly reuses `_sortedBase` instead of re-calculating it. - **Clarity:** Distinguishes between the immutable data templates and the mutable copies used for testing. - **Consistency:** Simplifies the deterministic generator functions into concise one-liners. --- .../benchmark/dataset_generator.dart | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/pkgs/collection/benchmark/dataset_generator.dart b/pkgs/collection/benchmark/dataset_generator.dart index ef94d14b..eedfb8a2 100644 --- a/pkgs/collection/benchmark/dataset_generator.dart +++ b/pkgs/collection/benchmark/dataset_generator.dart @@ -12,6 +12,16 @@ import 'dart:math'; const size = 50000; const count = 128; // Number of lists to cycle through. +final List _sortedBase = List.generate(size, (i) => i, growable: false); +final List _reversedBase = + List.generate(size, (i) => size - 1 - i, growable: false); +final List _pathologicalBase = [ + for (var i = 0; i < size; i++) + if (i.isEven) _sortedBase[i], + for (var i = size - 1; i > 0; i--) + if (i.isOdd) _sortedBase[i], +]; + final List> random = _generateRandom(); final List> sorted = _generateSorted(); final List> reverse = _generateReverse(); @@ -24,30 +34,16 @@ List> _generateRandom() { count, (_) => List.generate(size, (_) => r.nextInt(size))); } -List> _generateSorted() { - final base = List.generate(size, (i) => i, growable: false); - return List.generate(count, (_) => List.of(base, growable: true)); -} +List> _generateSorted() => + List.generate(count, (_) => List.of(_sortedBase, growable: true)); -List> _generateReverse() { - final base = List.generate(size, (i) => size - 1 - i, growable: false); - return List.generate(count, (_) => List.of(base, growable: true)); -} +List> _generateReverse() => + List.generate(count, (_) => List.of(_reversedBase, growable: true)); List> _generateFewUnique() { final r = Random(67890); return List.generate(count, (_) => List.generate(size, (_) => r.nextInt(7))); } -List> _generatePathological() { - final sortedBase = List.generate(size, (i) => i, growable: false); - // Creates a "V-shape" or "organ pipe" array that can be challenging - // for quicksort implementations by promoting unbalanced partitions. - final pathological = [ - for (var i = 0; i < size; i++) - if (i.isEven) sortedBase[i], - for (var i = size - 1; i > 0; i--) - if (i.isOdd) sortedBase[i], - ]; - return List.generate(count, (_) => List.of(pathological, growable: true)); -} +List> _generatePathological() => + List.generate(count, (_) => List.of(_pathologicalBase, growable: true)); From cc7f09af62a6ebd4b5605d747d0b8b2e14dfd08b Mon Sep 17 00:00:00 2001 From: Abbas Date: Sun, 16 Nov 2025 10:57:03 +0330 Subject: [PATCH 17/27] Chore: Use direct stepping in pathological list creation loops Refactor the loops that build `_pathologicalBase` to use `i += 2` for a minor performance and clarity improvement. --- pkgs/collection/benchmark/dataset_generator.dart | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pkgs/collection/benchmark/dataset_generator.dart b/pkgs/collection/benchmark/dataset_generator.dart index eedfb8a2..4720e432 100644 --- a/pkgs/collection/benchmark/dataset_generator.dart +++ b/pkgs/collection/benchmark/dataset_generator.dart @@ -15,11 +15,9 @@ const count = 128; // Number of lists to cycle through. final List _sortedBase = List.generate(size, (i) => i, growable: false); final List _reversedBase = List.generate(size, (i) => size - 1 - i, growable: false); -final List _pathologicalBase = [ - for (var i = 0; i < size; i++) - if (i.isEven) _sortedBase[i], - for (var i = size - 1; i > 0; i--) - if (i.isOdd) _sortedBase[i], +final _pathologicalBase = [ + for (var i = 0; i < size; i += 2) _sortedBase[i], + for (var i = size - 1; i > 0; i -= 2) _sortedBase[i], ]; final List> random = _generateRandom(); From 9ebe846733fb2d37592261fa3ed5629840215a6a Mon Sep 17 00:00:00 2001 From: Abbas Date: Sun, 16 Nov 2025 11:08:36 +0330 Subject: [PATCH 18/27] Chore: Use `.reversed` to create the reversed base list Improves code readability. --- pkgs/collection/benchmark/dataset_generator.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/collection/benchmark/dataset_generator.dart b/pkgs/collection/benchmark/dataset_generator.dart index 4720e432..02027dee 100644 --- a/pkgs/collection/benchmark/dataset_generator.dart +++ b/pkgs/collection/benchmark/dataset_generator.dart @@ -13,8 +13,8 @@ const size = 50000; const count = 128; // Number of lists to cycle through. final List _sortedBase = List.generate(size, (i) => i, growable: false); -final List _reversedBase = - List.generate(size, (i) => size - 1 - i, growable: false); +final List _reversedBase = _sortedBase.reversed.toList(growable: false); + final _pathologicalBase = [ for (var i = 0; i < size; i += 2) _sortedBase[i], for (var i = size - 1; i > 0; i -= 2) _sortedBase[i], From 3960d630f3507e09c61aeec4dd42095ad7c2b252 Mon Sep 17 00:00:00 2001 From: Abbas Date: Sun, 16 Nov 2025 11:16:01 +0330 Subject: [PATCH 19/27] Refactor: Improve clarity and correctness of benchmark data generators - Fix a bug in the `_pathologicalBase` creation logic for odd sizes. - Make `_reversedBase` generation more declarative using `.reversed.toList()`. - Add clarifying comments for complex calculations. --- pkgs/collection/benchmark/dataset_generator.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pkgs/collection/benchmark/dataset_generator.dart b/pkgs/collection/benchmark/dataset_generator.dart index 02027dee..0a21ccb7 100644 --- a/pkgs/collection/benchmark/dataset_generator.dart +++ b/pkgs/collection/benchmark/dataset_generator.dart @@ -15,9 +15,15 @@ const count = 128; // Number of lists to cycle through. final List _sortedBase = List.generate(size, (i) => i, growable: false); final List _reversedBase = _sortedBase.reversed.toList(growable: false); -final _pathologicalBase = [ +// Ensure the reverse loop for the pathological list starts on the highest +// odd index, regardless of whether `size` is even or odd. +final int secondLoopStart = (size - 1).isOdd ? size - 1 : size - 2; + +/// It consists of all even-indexed elements followed by all odd-indexed +/// elements in reverse order. +final List _pathologicalBase = [ for (var i = 0; i < size; i += 2) _sortedBase[i], - for (var i = size - 1; i > 0; i -= 2) _sortedBase[i], + for (var i = secondLoopStart; i > -1; i -= 2) _sortedBase[i], ]; final List> random = _generateRandom(); From 5cb0eb63b956f26fe88d31250681fa04ae91de26 Mon Sep 17 00:00:00 2001 From: Abbas Date: Wed, 19 Nov 2025 17:01:22 +0330 Subject: [PATCH 20/27] refactor(benchmark): Restructure benchmark infrastructure and address review feedback - Move dataset generation to setUp() method as suggested by @lrhn - Add support for variable input sizes via command-line arguments - Split benchmark code into three modular files: * benchmark_utils.dart: Reusable infrastructure (SortBenchmarkBase, DatasetGenerators, result printing) * legacy_quicksort.dart: Previous implementation for performance comparison * sort_benchmark.dart: Clean benchmark implementation using utilities - Rename "baseline" to "legacy" for clearer semantics - Make printResultsAsMarkdownTable() generic with configurable algorithm names - Fix table formatting with proper alignment and dynamic column widths - Add DatasetGenerators class with centralized dataset creation methods - Add "Nearly Sorted" benchmark pattern for more comprehensive testing - Include standard deviation in BenchmarkResult for statistical analysis This refactoring improves maintainability, reusability, and makes it easier to benchmark different sorting algorithms with various input patterns and sizes. Addresses feedback from PR #922 review. --- .../collection/benchmark/benchmark_utils.dart | 235 ++++++++++ .../benchmark/dataset_generator.dart | 53 --- .../benchmark/legacy_quicksort.dart | 122 ++++++ pkgs/collection/benchmark/sort_benchmark.dart | 411 ++++++------------ 4 files changed, 499 insertions(+), 322 deletions(-) create mode 100644 pkgs/collection/benchmark/benchmark_utils.dart delete mode 100644 pkgs/collection/benchmark/dataset_generator.dart create mode 100644 pkgs/collection/benchmark/legacy_quicksort.dart diff --git a/pkgs/collection/benchmark/benchmark_utils.dart b/pkgs/collection/benchmark/benchmark_utils.dart new file mode 100644 index 00000000..d8198681 --- /dev/null +++ b/pkgs/collection/benchmark/benchmark_utils.dart @@ -0,0 +1,235 @@ +// 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. + +/// Reusable utilities for benchmarking sorting algorithms. +library; + +import 'dart:math'; +import 'package:benchmark_harness/benchmark_harness.dart'; + +// Sink variable to prevent the compiler from optimizing away benchmark code. +int sink = 0; + +/// The aggregated result of a benchmark run. +class BenchmarkResult { + final double mean; + final int median; + final double stdDev; + final List allTimes; + + BenchmarkResult(this.mean, this.median, this.stdDev, this.allTimes); +} + +/// Base class for sorting benchmarks with dataset generation. +abstract class SortBenchmarkBase extends BenchmarkBase { + final int size; + late final List> _datasets; + int _iteration = 0; + int _checksum = 0; + + SortBenchmarkBase(super.name, this.size); + + /// Generate datasets for this benchmark condition. + List> generateDatasets(); + + @override + void setup() { + _datasets = generateDatasets(); + } + + /// Get the next list to sort (creates a copy). + List get nextList { + final dataset = _datasets[_iteration]; + _iteration++; + if (_iteration == _datasets.length) _iteration = 0; + return dataset.toList(); + } + + /// Update checksum to prevent compiler optimization. + void updateChecksum(List list) { + sink ^= list.first ^ list.last ^ list[list.length >> 1] ^ _checksum++; + } + + /// The core sorting operation to benchmark. + void performSort(); + + @override + void run() => performSort(); +} + +/// Data pattern generators for consistent testing. +class DatasetGenerators { + /// Generate random integer lists. + static List> random(int size, {int count = 128, int? seed}) { + final r = Random(seed ?? 12345); + return List.generate( + count, (_) => List.generate(size, (_) => r.nextInt(size))); + } + + /// Generate sorted lists. + static List> sorted(int size) { + return [List.generate(size, (i) => i, growable: true)]; + } + + /// Generate reverse-sorted lists. + static List> reverse(int size) { + return [List.generate(size, (i) => size - i - 1, growable: true)]; + } + + /// Generate lists with few unique values. + static List> fewUnique(int size, + {int uniqueCount = 7, int count = 128, int? seed}) { + final r = Random(seed ?? 67890); + return List.generate( + count, (_) => List.generate(size, (_) => r.nextInt(uniqueCount))); + } + + /// Generate pathological input (worst-case for naive quicksort). + /// Contains even-indexed elements followed by odd-indexed in reverse. + static List> pathological(int size) { + final sorted = List.generate(size, (i) => i, growable: false); + final secondLoopStart = (size - 1).isOdd ? size - 1 : size - 2; + final pathological = [ + for (var i = 0; i < size; i += 2) sorted[i], + for (var i = secondLoopStart; i > -1; i -= 2) sorted[i], + ]; + return [pathological]; + } + + /// Generate nearly sorted lists (only a few elements out of place). + static List> nearlySorted(int size, + {double swapPercent = 0.05, int count = 128, int? seed}) { + final r = Random(seed ?? 11111); + return List.generate(count, (_) { + final list = List.generate(size, (i) => i, growable: true); + final numSwaps = (size * swapPercent).round(); + for (var i = 0; i < numSwaps; i++) { + final idx1 = r.nextInt(size); + final idx2 = r.nextInt(size); + final temp = list[idx1]; + list[idx1] = list[idx2]; + list[idx2] = temp; + } + return list; + }); + } +} + +/// Run a benchmark multiple times and collect statistics. +BenchmarkResult runBenchmark(SortBenchmarkBase benchmark, int samples) { + final times = []; + + // Setup datasets + benchmark.setup(); + + // Warmup runs (not timed) + for (var i = 0; i < 3; i++) { + benchmark.run(); + } + + // Timed runs + for (var i = 0; i < samples; i++) { + final stopwatch = Stopwatch()..start(); + benchmark.run(); + stopwatch.stop(); + times.add(stopwatch.elapsedMicroseconds); + } + + times.sort(); + final mean = times.reduce((a, b) => a + b) / samples; + final median = times[samples >> 1]; + + // Calculate standard deviation + final variance = + times.map((t) => pow(t - mean, 2)).reduce((a, b) => a + b) / samples; + final stdDev = sqrt(variance); + + return BenchmarkResult(mean, median, stdDev, times); +} + +/// Print benchmark results as a markdown table. +/// +/// [baselineName] and [comparisonName] are the labels for the +/// two implementations +/// being compared (e.g., "Legacy", "pdqsort", "MergeSort", etc.). +void printResultsAsMarkdownTable( + Map results, int size, + {required String baselineName, + required String comparisonName, + bool showStdDev = false}) { + final separator = '=' * 100; + print('\n$separator'); + print('Benchmark Results (Size: $size): $comparisonName vs. $baselineName'); + print(separator); + + // Calculate dynamic column widths based on name lengths + final baselineColWidth = max(baselineName.length + 5, 13); + final comparisonColWidth = max(comparisonName.length + 5, 13); + + final baselineHeader = '$baselineName (ยตs)'.padRight(baselineColWidth); + final comparisonHeader = '$comparisonName (ยตs)'.padRight(comparisonColWidth); + + if (showStdDev) { + print( + '''| Data Condition | $baselineHeader | $comparisonHeader | Improvement | StdDev |'''); + print( + '''| :------------------ | :${'-' * (baselineColWidth - 2)}: | :${'-' * (comparisonColWidth - 2)}: | :---------: | :-----------: |'''); + } else { + print( + '''| Data Condition | $baselineHeader | $comparisonHeader | Improvement | Winner |'''); + print( + '''| :------------------ | :${'-' * (baselineColWidth - 2)}: | :${'-' * (comparisonColWidth - 2)}: | :---------: | :-------------: |'''); + } + + print( + '''| **Mean** | ${' ' * baselineColWidth} | ${' ' * comparisonColWidth} | | |'''); + + for (final entry in results.entries) { + final condition = entry.key; + final (baseline, comparison) = entry.value; + + final improvement = (baseline.mean - comparison.mean) / baseline.mean * 100; + final improvementString = + '${improvement > 0 ? '+' : ''}${improvement.toStringAsFixed(2)}%'; + final baselineMean = baseline.mean.round().toString(); + final comparisonMean = comparison.mean.round().toString(); + + if (showStdDev) { + final stdDevString = + '${baseline.stdDev.round()}/${comparison.stdDev.round()}'; + print( + '''| ${condition.padRight(19)} | ${baselineMean.padLeft(baselineColWidth)} | ${comparisonMean.padLeft(comparisonColWidth)} | ${improvementString.padLeft(11)} | ${stdDevString.padLeft(13)} |'''); + } else { + final winner = improvement > 0 ? '$comparisonName ๐Ÿš€' : baselineName; + print( + '''| ${condition.padRight(19)} | ${baselineMean.padLeft(baselineColWidth)} | ${comparisonMean.padLeft(comparisonColWidth)} | ${improvementString.padLeft(11)} | ${winner.padLeft(15)} |'''); + } + } + + print( + '''| **Median** | ${' ' * baselineColWidth} | ${' ' * comparisonColWidth} | | |'''); + + for (final entry in results.entries) { + final condition = entry.key; + final (baseline, comparison) = entry.value; + + final improvement = + (baseline.median - comparison.median) / baseline.median * 100; + final improvementString = + '${improvement > 0 ? '+' : ''}${improvement.toStringAsFixed(2)}%'; + final baselineMedian = baseline.median.toString(); + final comparisonMedian = comparison.median.toString(); + + if (showStdDev) { + print( + '''| ${condition.padRight(19)} | ${baselineMedian.padLeft(baselineColWidth)} | ${comparisonMedian.padLeft(comparisonColWidth)} | ${improvementString.padLeft(11)} | ${' '.padLeft(13)} |'''); + } else { + final winner = improvement > 0 ? '$comparisonName ๐Ÿš€' : baselineName; + print( + '''| ${condition.padRight(19)} | ${baselineMedian.padLeft(baselineColWidth)} | ${comparisonMedian.padLeft(comparisonColWidth)} | ${improvementString.padLeft(11)} | ${winner.padLeft(15)} |'''); + } + } + + print(separator); +} diff --git a/pkgs/collection/benchmark/dataset_generator.dart b/pkgs/collection/benchmark/dataset_generator.dart deleted file mode 100644 index 0a21ccb7..00000000 --- a/pkgs/collection/benchmark/dataset_generator.dart +++ /dev/null @@ -1,53 +0,0 @@ -// 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. - -/// Centralized generation of datasets for all benchmarks. -/// -/// Ensures all algorithms are tested on the exact same data. -library; - -import 'dart:math'; - -const size = 50000; -const count = 128; // Number of lists to cycle through. - -final List _sortedBase = List.generate(size, (i) => i, growable: false); -final List _reversedBase = _sortedBase.reversed.toList(growable: false); - -// Ensure the reverse loop for the pathological list starts on the highest -// odd index, regardless of whether `size` is even or odd. -final int secondLoopStart = (size - 1).isOdd ? size - 1 : size - 2; - -/// It consists of all even-indexed elements followed by all odd-indexed -/// elements in reverse order. -final List _pathologicalBase = [ - for (var i = 0; i < size; i += 2) _sortedBase[i], - for (var i = secondLoopStart; i > -1; i -= 2) _sortedBase[i], -]; - -final List> random = _generateRandom(); -final List> sorted = _generateSorted(); -final List> reverse = _generateReverse(); -final List> fewUnique = _generateFewUnique(); -final List> pathological = _generatePathological(); - -List> _generateRandom() { - final r = Random(12345); - return List.generate( - count, (_) => List.generate(size, (_) => r.nextInt(size))); -} - -List> _generateSorted() => - List.generate(count, (_) => List.of(_sortedBase, growable: true)); - -List> _generateReverse() => - List.generate(count, (_) => List.of(_reversedBase, growable: true)); - -List> _generateFewUnique() { - final r = Random(67890); - return List.generate(count, (_) => List.generate(size, (_) => r.nextInt(7))); -} - -List> _generatePathological() => - List.generate(count, (_) => List.of(_pathologicalBase, growable: true)); diff --git a/pkgs/collection/benchmark/legacy_quicksort.dart b/pkgs/collection/benchmark/legacy_quicksort.dart new file mode 100644 index 00000000..e27f4686 --- /dev/null +++ b/pkgs/collection/benchmark/legacy_quicksort.dart @@ -0,0 +1,122 @@ +/// Legacy quickSort implementation preserved for benchmarking purposes. +/// This code is ONLY for benchmarking and should not be used in production. +library; + +import 'dart:math'; +import 'package:collection/src/utils.dart'; + +/// Performs an insertion sort into a potentially different list than the +/// one containing the original values. +/// +/// It will work in-place as well. +void _movingInsertionSort( + List list, + K Function(E element) keyOf, + int Function(K, K) compare, + int start, + int end, + List target, + int targetOffset, +) { + var length = end - start; + if (length == 0) return; + target[targetOffset] = list[start]; + for (var i = 1; i < length; i++) { + var element = list[start + i]; + var elementKey = keyOf(element); + var min = targetOffset; + var max = targetOffset + i; + while (min < max) { + var mid = min + ((max - min) >> 1); + if (compare(elementKey, keyOf(target[mid])) < 0) { + max = mid; + } else { + min = mid + 1; + } + } + target.setRange(min + 1, targetOffset + i + 1, target, min); + target[min] = element; + } +} + +/// Sort [elements] using a quick-sort algorithm. +/// +/// The elements are compared using [compare] on the elements. +/// If [start] and [end] are provided, only that range is sorted. +/// +/// Uses insertion sort for smaller sublists. +void quickSort( + List elements, + int Function(E a, E b) compare, [ + int start = 0, + int? end, +]) { + end = RangeError.checkValidRange(start, end, elements.length); + _quickSort(elements, identity, compare, Random(), start, end); +} + +/// Sort [list] using a quick-sort algorithm. +/// +/// The elements are compared using [compare] on the value provided by [keyOf] +/// on the element. +/// If [start] and [end] are provided, only that range is sorted. +/// +/// Uses insertion sort for smaller sublists. +void quickSortBy( + List list, + K Function(E element) keyOf, + int Function(K a, K b) compare, [ + int start = 0, + int? end, +]) { + end = RangeError.checkValidRange(start, end, list.length); + _quickSort(list, keyOf, compare, Random(), start, end); +} + +void _quickSort( + List list, + K Function(E element) keyOf, + int Function(K a, K b) compare, + Random random, + int start, + int end, +) { + const minQuickSortLength = 24; + var length = end - start; + while (length >= minQuickSortLength) { + var pivotIndex = random.nextInt(length) + start; + var pivot = list[pivotIndex]; + var pivotKey = keyOf(pivot); + var endSmaller = start; + var startGreater = end; + var startPivots = end - 1; + list[pivotIndex] = list[startPivots]; + list[startPivots] = pivot; + while (endSmaller < startPivots) { + var current = list[endSmaller]; + var relation = compare(keyOf(current), pivotKey); + if (relation < 0) { + endSmaller++; + } else { + startPivots--; + var currentTarget = startPivots; + list[endSmaller] = list[startPivots]; + if (relation > 0) { + startGreater--; + currentTarget = startGreater; + list[startPivots] = list[startGreater]; + } + list[currentTarget] = current; + } + } + if (endSmaller - start < end - startGreater) { + _quickSort(list, keyOf, compare, random, start, endSmaller); + start = startGreater; + } else { + _quickSort(list, keyOf, compare, random, startGreater, end); + end = endSmaller; + } + length = end - start; + } + _movingInsertionSort(list, keyOf, compare, start, end, list, start); +} diff --git a/pkgs/collection/benchmark/sort_benchmark.dart b/pkgs/collection/benchmark/sort_benchmark.dart index 9c5b1bfd..d76f9aa0 100644 --- a/pkgs/collection/benchmark/sort_benchmark.dart +++ b/pkgs/collection/benchmark/sort_benchmark.dart @@ -2,111 +2,108 @@ // 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. -import 'dart:math'; +/// Benchmarks comparing the new pdqsort implementation against the baseline. +/// This code is ONLY for benchmarking and should not be used in production. +library; -import 'package:benchmark_harness/benchmark_harness.dart'; import 'package:collection/src/algorithms.dart' show quickSort; -import 'package:collection/src/utils.dart'; +import 'benchmark_utils.dart'; +import 'legacy_quicksort.dart' as legacy; -import 'dataset_generator.dart' as dataset_generator; +// --- Legacy Benchmarks (Old quickSort) --- -// Sink variable to prevent the compiler from optimizing away benchmark code. -int sink = 0; +class LegacyRandomBenchmark extends SortBenchmarkBase { + LegacyRandomBenchmark(int size) : super('Legacy.Random', size); -/// The final aggregated result of a benchmark. -class BenchmarkResult { - final double mean; - final int median; - BenchmarkResult(this.mean, this.median); -} - -/// A base class for our sort benchmarks to reduce boilerplate. -/// Note: We extend `BenchmarkBase` for its structure but will use our own -/// timing. -abstract class SortBenchmarkBase extends BenchmarkBase { - final List> datasets; - int _iteration = 0; - int _checksum = 0; - - SortBenchmarkBase(super.name, this.datasets); - - List get nextList => - List.from(datasets[_iteration++ % datasets.length]); + @override + List> generateDatasets() => DatasetGenerators.random(size); - void updateChecksum(List list) { - // A simple checksum to ensure the list is used and not optimized away. - sink ^= list.first ^ list.last ^ list[list.length >> 1] ^ _checksum++; + @override + void performSort() { + final list = nextList; + legacy.quickSort(list, (a, b) => a.compareTo(b)); + updateChecksum(list); } +} - /// The core operation to be benchmarked. - void performSort(); +class LegacySortedBenchmark extends SortBenchmarkBase { + LegacySortedBenchmark(int size) : super('Legacy.Sorted', size); @override - void run() => performSort(); -} + List> generateDatasets() => DatasetGenerators.sorted(size); -// --- Benchmark Classes --- - -// Baseline (Old SDK quickSort) -class QuickSortBaselineRandomBenchmark extends SortBenchmarkBase { - QuickSortBaselineRandomBenchmark() - : super('Baseline.Random', dataset_generator.random); @override void performSort() { final list = nextList; - quickSortBaseline(list, (a, b) => a.compareTo(b)); + legacy.quickSort(list, (a, b) => a.compareTo(b)); updateChecksum(list); } } -class QuickSortBaselineSortedBenchmark extends SortBenchmarkBase { - QuickSortBaselineSortedBenchmark() - : super('Baseline.Sorted', dataset_generator.sorted); +class LegacyReverseBenchmark extends SortBenchmarkBase { + LegacyReverseBenchmark(int size) : super('Legacy.Reverse', size); + + @override + List> generateDatasets() => DatasetGenerators.reverse(size); + @override void performSort() { final list = nextList; - quickSortBaseline(list, (a, b) => a.compareTo(b)); + legacy.quickSort(list, (a, b) => a.compareTo(b)); updateChecksum(list); } } -class QuickSortBaselineReverseBenchmark extends SortBenchmarkBase { - QuickSortBaselineReverseBenchmark() - : super('Baseline.Reverse', dataset_generator.reverse); +class LegacyFewUniqueBenchmark extends SortBenchmarkBase { + LegacyFewUniqueBenchmark(int size) : super('Legacy.FewUnique', size); + + @override + List> generateDatasets() => DatasetGenerators.fewUnique(size); + @override void performSort() { final list = nextList; - quickSortBaseline(list, (a, b) => a.compareTo(b)); + legacy.quickSort(list, (a, b) => a.compareTo(b)); updateChecksum(list); } } -class QuickSortBaselineFewUniqueBenchmark extends SortBenchmarkBase { - QuickSortBaselineFewUniqueBenchmark() - : super('Baseline.FewUnique', dataset_generator.fewUnique); +class LegacyPathologicalBenchmark extends SortBenchmarkBase { + LegacyPathologicalBenchmark(int size) : super('Legacy.Pathological', size); + + @override + List> generateDatasets() => DatasetGenerators.pathological(size); + @override void performSort() { final list = nextList; - quickSortBaseline(list, (a, b) => a.compareTo(b)); + legacy.quickSort(list, (a, b) => a.compareTo(b)); updateChecksum(list); } } -class QuickSortBaselinePathologicalBenchmark extends SortBenchmarkBase { - QuickSortBaselinePathologicalBenchmark() - : super('Baseline.Pathological', dataset_generator.pathological); +class LegacyNearlySortedBenchmark extends SortBenchmarkBase { + LegacyNearlySortedBenchmark(int size) : super('Legacy.NearlySorted', size); + + @override + List> generateDatasets() => DatasetGenerators.nearlySorted(size); + @override void performSort() { final list = nextList; - quickSortBaseline(list, (a, b) => a.compareTo(b)); + legacy.quickSort(list, (a, b) => a.compareTo(b)); updateChecksum(list); } } -// Enhancement (New pdqsort) -class PdqSortEnhancementRandomBenchmark extends SortBenchmarkBase { - PdqSortEnhancementRandomBenchmark() - : super('Enhancement.Random', dataset_generator.random); +// --- Enhancement Benchmarks (New pdqsort) --- + +class EnhancementRandomBenchmark extends SortBenchmarkBase { + EnhancementRandomBenchmark(int size) : super('Enhancement.Random', size); + + @override + List> generateDatasets() => DatasetGenerators.random(size); + @override void performSort() { final list = nextList; @@ -115,9 +112,12 @@ class PdqSortEnhancementRandomBenchmark extends SortBenchmarkBase { } } -class PdqSortEnhancementSortedBenchmark extends SortBenchmarkBase { - PdqSortEnhancementSortedBenchmark() - : super('Enhancement.Sorted', dataset_generator.sorted); +class EnhancementSortedBenchmark extends SortBenchmarkBase { + EnhancementSortedBenchmark(int size) : super('Enhancement.Sorted', size); + + @override + List> generateDatasets() => DatasetGenerators.sorted(size); + @override void performSort() { final list = nextList; @@ -126,9 +126,12 @@ class PdqSortEnhancementSortedBenchmark extends SortBenchmarkBase { } } -class PdqSortEnhancementReverseBenchmark extends SortBenchmarkBase { - PdqSortEnhancementReverseBenchmark() - : super('Enhancement.Reverse', dataset_generator.reverse); +class EnhancementReverseBenchmark extends SortBenchmarkBase { + EnhancementReverseBenchmark(int size) : super('Enhancement.Reverse', size); + + @override + List> generateDatasets() => DatasetGenerators.reverse(size); + @override void performSort() { final list = nextList; @@ -137,9 +140,13 @@ class PdqSortEnhancementReverseBenchmark extends SortBenchmarkBase { } } -class PdqSortEnhancementFewUniqueBenchmark extends SortBenchmarkBase { - PdqSortEnhancementFewUniqueBenchmark() - : super('Enhancement.FewUnique', dataset_generator.fewUnique); +class EnhancementFewUniqueBenchmark extends SortBenchmarkBase { + EnhancementFewUniqueBenchmark(int size) + : super('Enhancement.FewUnique', size); + + @override + List> generateDatasets() => DatasetGenerators.fewUnique(size); + @override void performSort() { final list = nextList; @@ -148,9 +155,13 @@ class PdqSortEnhancementFewUniqueBenchmark extends SortBenchmarkBase { } } -class PdqSortEnhancementPathologicalBenchmark extends SortBenchmarkBase { - PdqSortEnhancementPathologicalBenchmark() - : super('Enhancement.Pathological', dataset_generator.pathological); +class EnhancementPathologicalBenchmark extends SortBenchmarkBase { + EnhancementPathologicalBenchmark(int size) + : super('Enhancement.Pathological', size); + + @override + List> generateDatasets() => DatasetGenerators.pathological(size); + @override void performSort() { final list = nextList; @@ -159,212 +170,74 @@ class PdqSortEnhancementPathologicalBenchmark extends SortBenchmarkBase { } } -// --- Main Execution Logic --- - -void main() { - const samples = 12; +class EnhancementNearlySortedBenchmark extends SortBenchmarkBase { + EnhancementNearlySortedBenchmark(int size) + : super('Enhancement.NearlySorted', size); - final benchmarks = [ - ( - 'Random', - QuickSortBaselineRandomBenchmark(), - PdqSortEnhancementRandomBenchmark() - ), - ( - 'Sorted', - QuickSortBaselineSortedBenchmark(), - PdqSortEnhancementSortedBenchmark() - ), - ( - 'Reverse Sorted', - QuickSortBaselineReverseBenchmark(), - PdqSortEnhancementReverseBenchmark() - ), - ( - 'Few Unique', - QuickSortBaselineFewUniqueBenchmark(), - PdqSortEnhancementFewUniqueBenchmark() - ), - ( - 'Pathological', - QuickSortBaselinePathologicalBenchmark(), - PdqSortEnhancementPathologicalBenchmark() - ), - ]; - - final results = {}; - - print('Running benchmarks ($samples samples each)...'); - for (final (condition, baseline, enhancement) in benchmarks) { - final baselineResult = _runBenchmark(baseline, samples); - final enhancementResult = _runBenchmark(enhancement, samples); - results[condition] = (baselineResult, enhancementResult); - } - - _printResultsAsMarkdownTable(results); -} + @override + List> generateDatasets() => DatasetGenerators.nearlySorted(size); -BenchmarkResult _runBenchmark(SortBenchmarkBase benchmark, int samples) { - final times = []; - // Warmup run (not timed). - benchmark.run(); - for (var i = 0; i < samples; i++) { - final stopwatch = Stopwatch()..start(); - benchmark.run(); - stopwatch.stop(); - times.add(stopwatch.elapsedMicroseconds); + @override + void performSort() { + final list = nextList; + quickSort(list, (a, b) => a.compareTo(b)); + updateChecksum(list); } - times.sort(); - final mean = times.reduce((a, b) => a + b) / samples; - final median = times[samples >> 1]; - return BenchmarkResult(mean, median); } -void _printResultsAsMarkdownTable( - Map results) { - final separator = '=' * 80; - print('\n$separator'); - print( - 'Benchmark Results: pdqsort (Enhancement) vs. SDK quickSort (Baseline)'); - print(separator); - print( - '''| Data Condition | Baseline (ยตs) | Enhancement (ยตs) | Improvement | Winner |'''); - print( - '''| :------------------ | :------------ | :--------------- | :---------- | :------------ |'''); - print( - '''| *Mean* | | | | |'''); - - for (final entry in results.entries) { - final condition = entry.key; - final (baseline, enhancement) = entry.value; - - final improvement = - (baseline.mean - enhancement.mean) / baseline.mean * 100; - final winner = improvement > 0 ? 'Enhancement ๐Ÿš€' : 'Baseline'; - final improvementString = - '${improvement > 0 ? '+' : ''}${improvement.toStringAsFixed(2)}%'; - - final baselineMean = baseline.mean.round(); - final enhancementMean = enhancement.mean.round(); - - print( - '''| ${condition.padRight(19)} | ${baselineMean.toString().padLeft(13)} | ${enhancementMean.toString().padLeft(16)} | ${improvementString.padLeft(11)} | $winner |'''); - } - - print( - '''| *Median* | | | | |'''); - - for (final entry in results.entries) { - final condition = entry.key; - final (baseline, enhancement) = entry.value; - - final improvement = - (baseline.median - enhancement.median) / baseline.median * 100; - final winner = improvement > 0 ? 'Enhancement ๐Ÿš€' : 'Baseline'; - final improvementString = - '${improvement > 0 ? '+' : ''}${improvement.toStringAsFixed(2)}%'; - - // No rounding needed for median as it's an int. - final baselineMedian = baseline.median; - final enhancementMedian = enhancement.median; - - print( - '''| ${condition.padRight(19)} | ${baselineMedian.toString().padLeft(13)} | ${enhancementMedian.toString().padLeft(16)} | ${improvementString.padLeft(11)} | $winner |'''); - } - - print(separator); -} +// --- Main --- -// =========================================================================== -// BASELINE IMPLEMENTATION -// A direct copy of the original SDK quickSort, renamed to avoid conflicts. -// =========================================================================== - -void quickSortBaseline( - List elements, - int Function(E a, E b) compare, [ - int start = 0, - int? end, -]) { - end = RangeError.checkValidRange(start, end, elements.length); - _quickSortBaseline(elements, identity, compare, Random(), start, end); -} +void main(List args) { + const samples = 12; -void _quickSortBaseline( - List list, - K Function(E element) keyOf, - int Function(K a, K b) compare, - Random random, - int start, - int end, -) { - const minQuickSortLength = 32; - var length = end - start; - while (length >= minQuickSortLength) { - var pivotIndex = random.nextInt(length) + start; - var pivot = list[pivotIndex]; - var pivotKey = keyOf(pivot); - var endSmaller = start; - var startGreater = end; - var startPivots = end - 1; - list[pivotIndex] = list[startPivots]; - list[startPivots] = pivot; - while (endSmaller < startPivots) { - var current = list[endSmaller]; - var relation = compare(keyOf(current), pivotKey); - if (relation < 0) { - endSmaller++; - } else { - startPivots--; - var currentTarget = startPivots; - list[endSmaller] = list[startPivots]; - if (relation > 0) { - startGreater--; - currentTarget = startGreater; - list[startPivots] = list[startGreater]; - } - list[currentTarget] = current; - } + // Support multiple sizes via command-line args or use default + final sizes = args.isEmpty ? [50000] : args.map(int.parse).toList(); + + for (final size in sizes) { + print('\n${'=' * 80}'); + print('Running benchmarks with size: $size'); + print('=' * 80); + + final benchmarks = [ + ('Random', LegacyRandomBenchmark(size), EnhancementRandomBenchmark(size)), + ('Sorted', LegacySortedBenchmark(size), EnhancementSortedBenchmark(size)), + ( + 'Reverse Sorted', + LegacyReverseBenchmark(size), + EnhancementReverseBenchmark(size) + ), + ( + 'Few Unique', + LegacyFewUniqueBenchmark(size), + EnhancementFewUniqueBenchmark(size) + ), + ( + 'Nearly Sorted', + LegacyNearlySortedBenchmark(size), + EnhancementNearlySortedBenchmark(size) + ), + ( + 'Pathological', + LegacyPathologicalBenchmark(size), + EnhancementPathologicalBenchmark(size) + ), + ]; + + final results = {}; + + print('Running benchmarks ($samples samples each)...'); + for (final (condition, legacy, enhancement) in benchmarks) { + print(' Testing $condition...'); + final legacyResult = runBenchmark(legacy, samples); + final enhancementResult = runBenchmark(enhancement, samples); + results[condition] = (legacyResult, enhancementResult); } - if (endSmaller - start < end - startGreater) { - _quickSortBaseline(list, keyOf, compare, random, start, endSmaller); - start = startGreater; - } else { - _quickSortBaseline(list, keyOf, compare, random, startGreater, end); - end = endSmaller; - } - length = end - start; - } - _movingInsertionSortBaseline( - list, keyOf, compare, start, end, list, start); -} -void _movingInsertionSortBaseline( - List list, - K Function(E element) keyOf, - int Function(K, K) compare, - int start, - int end, - List target, - int targetOffset, -) { - var length = end - start; - if (length == 0) return; - target[targetOffset] = list[start]; - for (var i = 1; i < length; i++) { - var element = list[start + i]; - var elementKey = keyOf(element); - var min = targetOffset; - var max = targetOffset + i; - while (min < max) { - var mid = min + ((max - min) >> 1); - if (compare(elementKey, keyOf(target[mid])) < 0) { - max = mid; - } else { - min = mid + 1; - } - } - target.setRange(min + 1, targetOffset + i + 1, target, min); - target[min] = element; + printResultsAsMarkdownTable( + results, + size, + baselineName: 'Legacy', + comparisonName: 'pdqsort', + ); } } From 841afa76fe0e6726acd808ebdb34d7e3cbc89db4 Mon Sep 17 00:00:00 2001 From: Abbas Date: Thu, 20 Nov 2025 12:51:28 +0330 Subject: [PATCH 21/27] Refactor: Optimize quickSort with specialized direct implementation - Decouple `quickSort` from `quickSortBy` to remove `identity` function overhead. - Implement `_pdqSortImpl` for direct comparisons. - Inline element swaps in the partition loop for better performance. - Replace generic helpers with specialized direct helpers (`_insertionSort`, `_heapSort`, etc.). --- pkgs/collection/lib/src/algorithms.dart | 143 +++++++++++++++++++++++- 1 file changed, 142 insertions(+), 1 deletion(-) diff --git a/pkgs/collection/lib/src/algorithms.dart b/pkgs/collection/lib/src/algorithms.dart index 5b1268b0..b12d2730 100644 --- a/pkgs/collection/lib/src/algorithms.dart +++ b/pkgs/collection/lib/src/algorithms.dart @@ -499,7 +499,148 @@ void quickSort( int start = 0, int? end, ]) { - quickSortBy(elements, identity, compare, start, end); + end = RangeError.checkValidRange(start, end, elements.length); + if (end - start < 2) return; + _pdqSortImpl(elements, compare, start, end, _log2(end - start)); +} + +void _pdqSortImpl(List elements, int Function(E, E) compare, int start, + int end, int badAllowed) { + while (true) { + final size = end - start; + if (size < _pdqInsertionSortThreshold) { + _insertionSort(elements, compare, start, end); + return; + } + + if (badAllowed == 0) { + _heapSort(elements, compare, start, end); + return; + } + + final mid = start + size ~/ 2; + _selectPivot(elements, compare, start, mid, end, size); + + final pivot = elements[start]; + var less = start; + var equal = start + 1; + var greater = end; + + while (equal < greater) { + final element = elements[equal]; + final comparison = compare(element, pivot); + + if (comparison < 0) { + elements[equal] = elements[less]; + elements[less] = element; + less++; + equal++; + } else if (comparison > 0) { + greater--; + elements[equal] = elements[greater]; + elements[greater] = element; + } else { + equal++; + } + } + + if ((less - start) < size ~/ 8 || (end - greater) < size ~/ 8) { + badAllowed--; + } + + if (less - start < end - greater) { + _pdqSortImpl(elements, compare, start, less, badAllowed); + start = greater; + } else { + _pdqSortImpl(elements, compare, greater, end, badAllowed); + end = less; + } + } +} + +void _insertionSort( + List elements, int Function(E, E) compare, int start, int end) { + for (var i = start + 1; i < end; i++) { + var current = elements[i]; + var j = i - 1; + while (j >= start && compare(elements[j], current) > 0) { + elements[j + 1] = elements[j]; + j--; + } + elements[j + 1] = current; + } +} + +void _heapSort( + List elements, int Function(E, E) compare, int start, int end) { + final n = end - start; + for (var i = n ~/ 2 - 1; i >= 0; i--) { + _siftDown(elements, compare, i, n, start); + } + for (var i = n - 1; i > 0; i--) { + final temp = elements[start]; + elements[start] = elements[start + i]; + elements[start + i] = temp; + _siftDown(elements, compare, 0, i, start); + } +} + +void _siftDown( + List elements, int Function(E, E) compare, int i, int n, int start) { + var root = i; + while (true) { + final left = 2 * root + 1; + if (left >= n) break; + var largest = root; + if (compare(elements[start + left], elements[start + largest]) > 0) { + largest = left; + } + final right = left + 1; + if (right < n && + compare(elements[start + right], elements[start + largest]) > 0) { + largest = right; + } + if (largest == root) break; + final temp = elements[start + root]; + elements[start + root] = elements[start + largest]; + elements[start + largest] = temp; + root = largest; + } +} + +void _selectPivot(List elements, int Function(E, E) compare, int start, + int mid, int end, int size) { + if (size > 80) { + final s = size ~/ 8; + _sort3(elements, compare, start, start + s, start + 2 * s); + _sort3(elements, compare, mid - s, mid, mid + s); + _sort3(elements, compare, end - 1 - 2 * s, end - 1 - s, end - 1); + _sort3(elements, compare, start + s, mid, end - 1 - s); + } else { + _sort3(elements, compare, start, mid, end - 1); + } + final temp = elements[start]; + elements[start] = elements[mid]; + elements[mid] = temp; +} + +void _sort3( + List elements, int Function(E, E) compare, int a, int b, int c) { + if (compare(elements[a], elements[b]) > 0) { + final t = elements[a]; + elements[a] = elements[b]; + elements[b] = t; + } + if (compare(elements[b], elements[c]) > 0) { + final t = elements[b]; + elements[b] = elements[c]; + elements[c] = t; + if (compare(elements[a], elements[b]) > 0) { + final t2 = elements[a]; + elements[a] = elements[b]; + elements[b] = t2; + } + } } /// Sorts a list between [start] (inclusive) and [end] (exclusive) by key. From d5267171651caa3cb1a072786918efbd4f4efc9a Mon Sep 17 00:00:00 2001 From: Abbas Date: Thu, 20 Nov 2025 12:58:50 +0330 Subject: [PATCH 22/27] Refactor: Optimize keyed quickSort with key caching and inlining - Update `quickSortBy` to use the new `_pdqSortByImpl`. - Inline swaps in the keyed partition loop. - Optimize `_insertionSortBy` to cache element keys, reducing `keyOf` function calls. - Remove obsolete `_pdq*` helper functions (`_pdqSwap`, `_pdqInsertionSort`, etc.). --- pkgs/collection/lib/src/algorithms.dart | 246 +++++++++++------------- 1 file changed, 114 insertions(+), 132 deletions(-) diff --git a/pkgs/collection/lib/src/algorithms.dart b/pkgs/collection/lib/src/algorithms.dart index b12d2730..5e9a0087 100644 --- a/pkgs/collection/lib/src/algorithms.dart +++ b/pkgs/collection/lib/src/algorithms.dart @@ -656,16 +656,15 @@ void _sort3( /// Elements are ordered by the [compare] function applied to the result of /// the [keyOf] function. For a stable sort, use [mergeSortBy]. void quickSortBy( - List list, + List elements, K Function(E element) keyOf, int Function(K a, K b) compare, [ int start = 0, int? end, ]) { - end = RangeError.checkValidRange(start, end, list.length); - final length = end - start; - if (length < 2) return; - _pdqSortByImpl(list, keyOf, compare, start, end, _log2(length)); + end = RangeError.checkValidRange(start, end, elements.length); + if (end - start < 2) return; + _pdqSortByImpl(elements, keyOf, compare, start, end, _log2(end - start)); } /// Minimum list size below which pdqsort uses insertion sort. @@ -678,21 +677,74 @@ const int _pdqInsertionSortThreshold = 24; /// we return 0. int _log2(int n) => n > 0 ? n.bitLength - 1 : 0; -/// Swaps the elements at positions [i] and [j] in [elements]. -void _pdqSwap(List elements, int i, int j) { - final temp = elements[i]; - elements[i] = elements[j]; - elements[j] = temp; +/// The core implementation of Pattern-defeating Quicksort. +/// +/// [badAllowed] tracks how many bad pivot selections are allowed before +/// falling back to heap sort. +void _pdqSortByImpl(List elements, K Function(E) keyOf, + int Function(K, K) compare, int start, int end, int badAllowed) { + while (true) { + final size = end - start; + if (size < _pdqInsertionSortThreshold) { + _insertionSortBy(elements, keyOf, compare, start, end); + return; + } + + if (badAllowed == 0) { + _heapSortBy(elements, keyOf, compare, start, end); + return; + } + + final mid = start + size ~/ 2; + _selectPivotBy(elements, keyOf, compare, start, mid, end, size); + + final pivotElement = elements[start]; + final pivotKey = keyOf(pivotElement); + + var less = start; + var equal = start + 1; + var greater = end; + + while (equal < greater) { + final element = elements[equal]; + final elementKey = keyOf(element); + final comparison = compare(elementKey, pivotKey); + + if (comparison < 0) { + elements[equal] = elements[less]; + elements[less] = element; + less++; + equal++; + } else if (comparison > 0) { + greater--; + elements[equal] = elements[greater]; + elements[greater] = element; + } else { + equal++; + } + } + + if ((less - start) < size ~/ 8 || (end - greater) < size ~/ 8) { + badAllowed--; + } + + if (less - start < end - greater) { + _pdqSortByImpl(elements, keyOf, compare, start, less, badAllowed); + start = greater; + } else { + _pdqSortByImpl(elements, keyOf, compare, greater, end, badAllowed); + end = less; + } + } } -/// A simple, non-binary insertion sort for the base case of pdqsort. -void _pdqInsertionSort(List elements, K Function(E) keyOf, +void _insertionSortBy(List elements, K Function(E) keyOf, int Function(K, K) compare, int start, int end) { for (var i = start + 1; i < end; i++) { final current = elements[i]; - final key = keyOf(current); + final currentKey = keyOf(current); var j = i - 1; - while (j >= start && compare(keyOf(elements[j]), key) > 0) { + while (j >= start && compare(keyOf(elements[j]), currentKey) > 0) { elements[j + 1] = elements[j]; j--; } @@ -700,151 +752,81 @@ void _pdqInsertionSort(List elements, K Function(E) keyOf, } } -/// Heapsort implementation for the fallback case of pdqsort. -void _pdqHeapSort(List elements, K Function(E) keyOf, +void _heapSortBy(List elements, K Function(E) keyOf, int Function(K, K) compare, int start, int end) { final n = end - start; for (var i = n ~/ 2 - 1; i >= 0; i--) { - _pdqSiftDown(elements, keyOf, compare, i, n, start); + _siftDownBy(elements, keyOf, compare, i, n, start); } for (var i = n - 1; i > 0; i--) { - _pdqSwap(elements, start, start + i); - _pdqSiftDown(elements, keyOf, compare, 0, i, start); + final temp = elements[start]; + elements[start] = elements[start + i]; + elements[start + i] = temp; + _siftDownBy(elements, keyOf, compare, 0, i, start); } } -/// Sift-down operation for the heapsort fallback. -void _pdqSiftDown(List elements, K Function(E) keyOf, +void _siftDownBy(List elements, K Function(E) keyOf, int Function(K, K) compare, int i, int n, int start) { var root = i; while (true) { final left = 2 * root + 1; - if (left >= n) break; // Root is a leaf. - + if (left >= n) break; var largest = root; var largestKey = keyOf(elements[start + largest]); - // Compare with left child. - var child = left; - var childKey = keyOf(elements[start + child]); - if (compare(largestKey, childKey) < 0) { - largest = child; - largestKey = childKey; + final leftKey = keyOf(elements[start + left]); + if (compare(leftKey, largestKey) > 0) { + largest = left; + largestKey = leftKey; } - // Compare with right child if it exists. - child = left + 1; - if (child < n) { - childKey = keyOf(elements[start + child]); - if (compare(largestKey, childKey) < 0) { - largest = child; - largestKey = childKey; + final right = left + 1; + if (right < n) { + final rightKey = keyOf(elements[start + right]); + if (compare(rightKey, largestKey) > 0) { + largest = right; } } - if (largest == root) break; - - _pdqSwap(elements, start + root, start + largest); + final temp = elements[start + root]; + elements[start + root] = elements[start + largest]; + elements[start + largest] = temp; root = largest; } } -/// Sorts three elements at indices [a], [b], and [c]. -void _pdqSort3(List elements, K Function(E) keyOf, - int Function(K, K) compare, int a, int b, int c) { - var elementA = elements[a]; - var keyA = keyOf(elementA); - var elementB = elements[b]; - var keyB = keyOf(elementB); - var elementC = elements[c]; - var keyC = keyOf(elementC); - - if (compare(keyA, keyB) > 0) { - (elementA, elementB) = (elementB, elementA); - (keyA, keyB) = (keyB, keyA); - } - - if (compare(keyB, keyC) > 0) { - (elementB, elementC) = (elementC, elementB); - (keyB, keyC) = (keyC, keyB); - if (compare(keyA, keyB) > 0) { - (elementA, elementB) = (elementB, elementA); - (keyA, keyB) = (keyB, keyA); - } +void _selectPivotBy(List elements, K Function(E) keyOf, + int Function(K, K) compare, int start, int mid, int end, int size) { + if (size > 80) { + final s = size ~/ 8; + _sort3By(elements, keyOf, compare, start, start + s, start + 2 * s); + _sort3By(elements, keyOf, compare, mid - s, mid, mid + s); + _sort3By(elements, keyOf, compare, end - 1 - 2 * s, end - 1 - s, end - 1); + _sort3By(elements, keyOf, compare, start + s, mid, end - 1 - s); + } else { + _sort3By(elements, keyOf, compare, start, mid, end - 1); } - - elements[a] = elementA; - elements[b] = elementB; - elements[c] = elementC; + final temp = elements[start]; + elements[start] = elements[mid]; + elements[mid] = temp; } -/// The core implementation of Pattern-defeating Quicksort. -/// -/// [badAllowed] tracks how many bad pivot selections are allowed before -/// falling back to heap sort. -void _pdqSortByImpl(List elements, K Function(E) keyOf, - int Function(K, K) compare, int start, int end, int badAllowed) { - while (true) { - final size = end - start; - if (size < _pdqInsertionSortThreshold) { - _pdqInsertionSort(elements, keyOf, compare, start, end); - return; - } - - if (badAllowed == 0) { - _pdqHeapSort(elements, keyOf, compare, start, end); - return; - } - - final mid = start + size ~/ 2; - if (size > 80) { - // Ninther pivot selection for large arrays. - final s = size ~/ 8; - _pdqSort3(elements, keyOf, compare, start, start + s, start + 2 * s); - _pdqSort3(elements, keyOf, compare, mid - s, mid, mid + s); - _pdqSort3( - elements, keyOf, compare, end - 1 - 2 * s, end - 1 - s, end - 1); - _pdqSort3(elements, keyOf, compare, start + s, mid, end - 1 - s); - } else { - // Median-of-three for smaller arrays. - _pdqSort3(elements, keyOf, compare, start, mid, end - 1); - } - - // 3-Way Partitioning (Dutch National Flag). - _pdqSwap(elements, start, mid); - final pivotKey = keyOf(elements[start]); - - var less = start; - var equal = start; - var greater = end; - - while (equal < greater) { - var comparison = compare(keyOf(elements[equal]), pivotKey); - if (comparison < 0) { - _pdqSwap(elements, less++, equal++); - } else if (comparison > 0) { - greater--; - _pdqSwap(elements, equal, greater); - } else { - equal++; - } - } - - final leftSize = less - start; - final rightSize = end - greater; - - // Detect highly unbalanced partitions and decrement badAllowed. - if (leftSize < size ~/ 8 || rightSize < size ~/ 8) { - badAllowed--; - } - - // Recurse on the smaller partition first to keep stack depth low. - if (leftSize < rightSize) { - _pdqSortByImpl(elements, keyOf, compare, start, less, badAllowed); - start = greater; // Tail-call optimization on the larger partition - } else { - _pdqSortByImpl(elements, keyOf, compare, greater, end, badAllowed); - end = less; // Tail-call optimization on the larger partition +void _sort3By(List elements, K Function(E) keyOf, + int Function(K, K) compare, int a, int b, int c) { + if (compare(keyOf(elements[a]), keyOf(elements[b])) > 0) { + final t = elements[a]; + elements[a] = elements[b]; + elements[b] = t; + } + if (compare(keyOf(elements[b]), keyOf(elements[c])) > 0) { + final t = elements[b]; + elements[b] = elements[c]; + elements[c] = t; + if (compare(keyOf(elements[a]), keyOf(elements[b])) > 0) { + final t2 = elements[a]; + elements[a] = elements[b]; + elements[b] = t2; } } } From 6e2afe425100e8e75b7e4c1de70715baccd5672b Mon Sep 17 00:00:00 2001 From: Abbas Date: Thu, 20 Nov 2025 13:02:20 +0330 Subject: [PATCH 23/27] Feat: Add O(n) shortcuts for presorted and reverse-sorted lists - Introduce `_handlePresorted` and `_handlePresortedBy` to detect existing order. - Add `_reverseRange` helper to handle strictly decreasing sequences in-place. - Integrate these checks into the main `_pdqSortImpl` and `_pdqSortByImpl` loops. --- pkgs/collection/lib/src/algorithms.dart | 61 +++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/pkgs/collection/lib/src/algorithms.dart b/pkgs/collection/lib/src/algorithms.dart index 5e9a0087..2d44a137 100644 --- a/pkgs/collection/lib/src/algorithms.dart +++ b/pkgs/collection/lib/src/algorithms.dart @@ -513,6 +513,8 @@ void _pdqSortImpl(List elements, int Function(E, E) compare, int start, return; } + if (_handlePresorted(elements, compare, start, end)) return; + if (badAllowed == 0) { _heapSort(elements, compare, start, end); return; @@ -558,6 +560,41 @@ void _pdqSortImpl(List elements, int Function(E, E) compare, int start, } } +void _reverseRange(List elements, int start, int end) { + var left = start; + var right = end - 1; + while (left < right) { + final temp = elements[left]; + elements[left] = elements[right]; + elements[right] = temp; + left++; + right--; + } +} + +bool _handlePresorted( + List elements, int Function(E, E) compare, int start, int end) { + if (compare(elements[start], elements[start + 1]) > 0) { + // Check strictly decreasing + var i = start + 1; + while (i < end && compare(elements[i - 1], elements[i]) > 0) { + i++; + } + if (i == end) { + _reverseRange(elements, start, end); + return true; + } + } else { + // Check non-decreasing + var i = start + 1; + while (i < end && compare(elements[i - 1], elements[i]) <= 0) { + i++; + } + if (i == end) return true; + } + return false; +} + void _insertionSort( List elements, int Function(E, E) compare, int start, int end) { for (var i = start + 1; i < end; i++) { @@ -690,6 +727,8 @@ void _pdqSortByImpl(List elements, K Function(E) keyOf, return; } + if (_handlePresortedBy(elements, keyOf, compare, start, end)) return; + if (badAllowed == 0) { _heapSortBy(elements, keyOf, compare, start, end); return; @@ -738,6 +777,28 @@ void _pdqSortByImpl(List elements, K Function(E) keyOf, } } +bool _handlePresortedBy(List elements, K Function(E) keyOf, + int Function(K, K) compare, int start, int end) { + if (compare(keyOf(elements[start]), keyOf(elements[start + 1])) > 0) { + var i = start + 1; + while (i < end && compare(keyOf(elements[i - 1]), keyOf(elements[i])) > 0) { + i++; + } + if (i == end) { + _reverseRange(elements, start, end); + return true; + } + } else { + var i = start + 1; + while ( + i < end && compare(keyOf(elements[i - 1]), keyOf(elements[i])) <= 0) { + i++; + } + if (i == end) return true; + } + return false; +} + void _insertionSortBy(List elements, K Function(E) keyOf, int Function(K, K) compare, int start, int end) { for (var i = start + 1; i < end; i++) { From d3d0b20bbd4f51e4e39c5ade9ee251654ff617ce Mon Sep 17 00:00:00 2001 From: Abbas Date: Thu, 20 Nov 2025 13:09:57 +0330 Subject: [PATCH 24/27] Increase quickSort insertion threshold to 32 and organize implementation Increases _pdqInsertionSortThreshold from 24 to 32. Reorganizes the internal helper functions for quickSort and quickSortBy into distinct sections ("Direct Comparison" and "Keyed Comparison") to improve readability and maintainability. --- pkgs/collection/lib/src/algorithms.dart | 99 +++++++++++-------------- 1 file changed, 45 insertions(+), 54 deletions(-) diff --git a/pkgs/collection/lib/src/algorithms.dart b/pkgs/collection/lib/src/algorithms.dart index 2d44a137..f40667e6 100644 --- a/pkgs/collection/lib/src/algorithms.dart +++ b/pkgs/collection/lib/src/algorithms.dart @@ -486,13 +486,6 @@ void _merge( /// /// The sorting algorithm is a Pattern-defeating Quicksort (pdqsort), a /// hybrid of Quicksort, Heapsort, and Insertion Sort. -/// It is not stable, but is typically very fast. -/// -/// This implementation is highly efficient for common data patterns -/// (such as sorted, reverse-sorted, or with few unique values) and has a -/// guaranteed worst-case time complexity of O(n*log(n)). -/// -/// For a stable sort, use [mergeSort]. void quickSort( List elements, int Function(E a, E b) compare, [ @@ -504,6 +497,36 @@ void quickSort( _pdqSortImpl(elements, compare, start, end, _log2(end - start)); } +/// Sorts a list between [start] (inclusive) and [end] (exclusive) by key. +/// +/// Elements are ordered by the [compare] function applied to the result of +/// the [keyOf] function. +void quickSortBy( + List elements, + K Function(E element) keyOf, + int Function(K a, K b) compare, [ + int start = 0, + int? end, +]) { + end = RangeError.checkValidRange(start, end, elements.length); + if (end - start < 2) return; + _pdqSortByImpl(elements, keyOf, compare, start, end, _log2(end - start)); +} + +/// Minimum list size below which pdqsort uses insertion sort. +const int _pdqInsertionSortThreshold = 32; + +/// Computes the base-2 logarithm of [n]. +/// Computes the base-2 logarithm of [n]. +/// +/// Uses bitLength to compute the floor(log2(n)) efficiently. +/// For n == 0 we return 0. +int _log2(int n) => n > 0 ? n.bitLength - 1 : 0; + +// ========================================== +// Implementation: Direct Comparison +// ========================================== + void _pdqSortImpl(List elements, int Function(E, E) compare, int start, int end, int badAllowed) { while (true) { @@ -560,18 +583,6 @@ void _pdqSortImpl(List elements, int Function(E, E) compare, int start, } } -void _reverseRange(List elements, int start, int end) { - var left = start; - var right = end - 1; - while (left < right) { - final temp = elements[left]; - elements[left] = elements[right]; - elements[right] = temp; - left++; - right--; - } -} - bool _handlePresorted( List elements, int Function(E, E) compare, int start, int end) { if (compare(elements[start], elements[start + 1]) > 0) { @@ -595,6 +606,18 @@ bool _handlePresorted( return false; } +void _reverseRange(List elements, int start, int end) { + var left = start; + var right = end - 1; + while (left < right) { + final temp = elements[left]; + elements[left] = elements[right]; + elements[right] = temp; + left++; + right--; + } +} + void _insertionSort( List elements, int Function(E, E) compare, int start, int end) { for (var i = start + 1; i < end; i++) { @@ -680,42 +703,10 @@ void _sort3( } } -/// Sorts a list between [start] (inclusive) and [end] (exclusive) by key. -/// -/// The sorting algorithm is a Pattern-defeating Quicksort (pdqsort), a -/// hybrid of Quicksort, Heapsort, and Insertion Sort. -/// It is not stable, but is typically very fast. -/// -/// This implementation is highly efficient for common data patterns -/// (such as sorted, reverse-sorted, or with few unique values) and has a -/// guaranteed worst-case time complexity of O(n*log(n)). -/// -/// Elements are ordered by the [compare] function applied to the result of -/// the [keyOf] function. For a stable sort, use [mergeSortBy]. -void quickSortBy( - List elements, - K Function(E element) keyOf, - int Function(K a, K b) compare, [ - int start = 0, - int? end, -]) { - end = RangeError.checkValidRange(start, end, elements.length); - if (end - start < 2) return; - _pdqSortByImpl(elements, keyOf, compare, start, end, _log2(end - start)); -} - -/// Minimum list size below which pdqsort uses insertion sort. -const int _pdqInsertionSortThreshold = 24; +// ========================================== +// Implementation: Keyed Comparison (By) +// ========================================== -/// Computes the base-2 logarithm of [n]. -/// Computes the base-2 logarithm of [n]. -/// -/// Uses bitLength to compute the floor(log2(n)) efficiently. For n == 0 -/// we return 0. -int _log2(int n) => n > 0 ? n.bitLength - 1 : 0; - -/// The core implementation of Pattern-defeating Quicksort. -/// /// [badAllowed] tracks how many bad pivot selections are allowed before /// falling back to heap sort. void _pdqSortByImpl(List elements, K Function(E) keyOf, From 8029a6a1c18ab80fcaab9137ca4e85e0769c3d80 Mon Sep 17 00:00:00 2001 From: Abbas Date: Thu, 20 Nov 2025 13:31:14 +0330 Subject: [PATCH 25/27] Remove redundant comment on base-2 logarithm computation in algorithms.dart for clarity. --- pkgs/collection/lib/src/algorithms.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/pkgs/collection/lib/src/algorithms.dart b/pkgs/collection/lib/src/algorithms.dart index f40667e6..f3f44518 100644 --- a/pkgs/collection/lib/src/algorithms.dart +++ b/pkgs/collection/lib/src/algorithms.dart @@ -516,7 +516,6 @@ void quickSortBy( /// Minimum list size below which pdqsort uses insertion sort. const int _pdqInsertionSortThreshold = 32; -/// Computes the base-2 logarithm of [n]. /// Computes the base-2 logarithm of [n]. /// /// Uses bitLength to compute the floor(log2(n)) efficiently. From 942384d326bd8a0a55ac51eb2da53f9a87c4589e Mon Sep 17 00:00:00 2001 From: Abbas Date: Thu, 20 Nov 2025 13:35:03 +0330 Subject: [PATCH 26/27] Fix: Handle reverse-sorted lists with duplicates in presorted check - Updated `_handlePresorted` and '_handlePresortedBy' to treat non-increasing sequences (e.g., [3, 2, 2, 1]) as reverse-sorted, allowing O(n) reversal instead of O(n log n) sorting. --- pkgs/collection/lib/src/algorithms.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/collection/lib/src/algorithms.dart b/pkgs/collection/lib/src/algorithms.dart index f3f44518..bcf9484a 100644 --- a/pkgs/collection/lib/src/algorithms.dart +++ b/pkgs/collection/lib/src/algorithms.dart @@ -587,7 +587,7 @@ bool _handlePresorted( if (compare(elements[start], elements[start + 1]) > 0) { // Check strictly decreasing var i = start + 1; - while (i < end && compare(elements[i - 1], elements[i]) > 0) { + while (i < end && compare(elements[i - 1], elements[i]) >= 0) { i++; } if (i == end) { @@ -771,7 +771,7 @@ bool _handlePresortedBy(List elements, K Function(E) keyOf, int Function(K, K) compare, int start, int end) { if (compare(keyOf(elements[start]), keyOf(elements[start + 1])) > 0) { var i = start + 1; - while (i < end && compare(keyOf(elements[i - 1]), keyOf(elements[i])) > 0) { + while (i < end && compare(keyOf(elements[i - 1]), keyOf(elements[i])) >= 0) { i++; } if (i == end) { From f6f0fcd0262003b07c3d0276c834d5c891b132d5 Mon Sep 17 00:00:00 2001 From: Abbas Date: Sat, 22 Nov 2025 10:52:13 +0330 Subject: [PATCH 27/27] fix: Adhere to 80-character line limit in pdqsort --- pkgs/collection/lib/src/algorithms.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkgs/collection/lib/src/algorithms.dart b/pkgs/collection/lib/src/algorithms.dart index bcf9484a..70430b95 100644 --- a/pkgs/collection/lib/src/algorithms.dart +++ b/pkgs/collection/lib/src/algorithms.dart @@ -771,7 +771,8 @@ bool _handlePresortedBy(List elements, K Function(E) keyOf, int Function(K, K) compare, int start, int end) { if (compare(keyOf(elements[start]), keyOf(elements[start + 1])) > 0) { var i = start + 1; - while (i < end && compare(keyOf(elements[i - 1]), keyOf(elements[i])) >= 0) { + while ( + i < end && compare(keyOf(elements[i - 1]), keyOf(elements[i])) >= 0) { i++; } if (i == end) {