Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ebe5aba
feat(collection): Replace quickSort with pdqsort for performance and …
abbashosseinii Nov 4, 2025
ea66e16
test(collection): add benchmark for pdqsort vs. baseline quickSort
abbashosseinii Nov 4, 2025
63fcfac
Merge branch 'main' into feature/replace-quicksort-with-pdqsort
abbashosseinii Nov 4, 2025
307ec48
docs(collection): add changelog for quickSort pdqsort enhancement
abbashosseinii Nov 5, 2025
009090e
refactor(core): Optimize and clarify _pdqSiftDown in heapsort
abbashosseinii Nov 5, 2025
3c80f38
docs(benchmark): Improve documentation clarity for BenchmarkResult class
abbashosseinii Nov 5, 2025
5c3d1ef
feat(benchmark): Add dataset generator and sorting benchmarks for qui…
abbashosseinii Nov 5, 2025
a6e53f9
chore: format code with `dart format`
abbashosseinii Nov 15, 2025
ec13079
refactor(algorithms): Remove unused imports and optimize _log2 function
abbashosseinii Nov 15, 2025
4073118
refactor(sort): Optimize `_pdqSort3` based on @lrhn's suggestion
abbashosseinii Nov 15, 2025
f530bae
refactor(benchmark): Replace `getNextList` method with a getter to fo…
abbashosseinii Nov 16, 2025
4a239f2
Update pkgs/collection/lib/src/algorithms.dart
abbashosseinii Nov 16, 2025
660314a
fix(dataset_generator): Adjust random dataset generation to use full …
abbashosseinii Nov 16, 2025
3b64a20
Refactor: Improve list generation in _generatePathological
abbashosseinii Nov 16, 2025
d5d4f52
Refactor: Improve list creation in _generateReverse
abbashosseinii Nov 16, 2025
6546d0c
Refactor: Improve list creation in _generateSorted
abbashosseinii Nov 16, 2025
5122e32
Refactor(benchmark): Pre-compute deterministic base lists
abbashosseinii Nov 16, 2025
cc7f09a
Chore: Use direct stepping in pathological list creation loops
abbashosseinii Nov 16, 2025
9ebe846
Chore: Use `.reversed` to create the reversed base list
abbashosseinii Nov 16, 2025
3960d63
Refactor: Improve clarity and correctness of benchmark data generators
abbashosseinii Nov 16, 2025
5cb0eb6
refactor(benchmark): Restructure benchmark infrastructure and address…
abbashosseinii Nov 19, 2025
841afa7
Refactor: Optimize quickSort with specialized direct implementation
abbashosseinii Nov 20, 2025
d526717
Refactor: Optimize keyed quickSort with key caching and inlining
abbashosseinii Nov 20, 2025
6e2afe4
Feat: Add O(n) shortcuts for presorted and reverse-sorted lists
abbashosseinii Nov 20, 2025
d3d0b20
Increase quickSort insertion threshold to 32 and organize implementation
abbashosseinii Nov 20, 2025
8029a6a
Remove redundant comment on base-2 logarithm computation in algorithm…
abbashosseinii Nov 20, 2025
942384d
Fix: Handle reverse-sorted lists with duplicates in presorted check
abbashosseinii Nov 20, 2025
f6f0fcd
fix: Adhere to 80-character line limit in pdqsort
abbashosseinii Nov 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pkgs/collection/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
235 changes: 235 additions & 0 deletions pkgs/collection/benchmark/benchmark_utils.dart
Original file line number Diff line number Diff line change
@@ -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<int> 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<List<int>> _datasets;
int _iteration = 0;
int _checksum = 0;

SortBenchmarkBase(super.name, this.size);

/// Generate datasets for this benchmark condition.
List<List<int>> generateDatasets();

@override
void setup() {
_datasets = generateDatasets();
}

/// Get the next list to sort (creates a copy).
List<int> 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<int> 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<List<int>> 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<List<int>> sorted(int size) {
return [List.generate(size, (i) => i, growable: true)];
}

/// Generate reverse-sorted lists.
static List<List<int>> reverse(int size) {
return [List.generate(size, (i) => size - i - 1, growable: true)];
}

/// Generate lists with few unique values.
static List<List<int>> 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<List<int>> 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<List<int>> 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 = <int>[];

// 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<String, (BenchmarkResult, BenchmarkResult)> 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);
}
122 changes: 122 additions & 0 deletions pkgs/collection/benchmark/legacy_quicksort.dart
Original file line number Diff line number Diff line change
@@ -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<E, K>(
List<E> list,
K Function(E element) keyOf,
int Function(K, K) compare,
int start,
int end,
List<E> 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<E>(
List<E> elements,
int Function(E a, E b) compare, [
int start = 0,
int? end,
]) {
end = RangeError.checkValidRange(start, end, elements.length);
_quickSort<E, E>(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<E, K>(
List<E> 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<E, K>(
List<E> 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<E, K>(list, keyOf, compare, start, end, list, start);
}
Loading