Commit 67cebf00 authored by Sven Woltmann's avatar Sven Woltmann Committed by Sven Woltmann

Add Ultimate test with basic and efficient sort algorithms.

parent 5076a006
.classpath
.idea
.project
.settings
out
target
**/gitignore
*.iml
*.class
*.bin
src/eu/happycoders/cds/sandbox
# Ultimate Guide - Sorting in Java
# Sorting in Java - Ultimate Guide
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>eu.happycoders</groupId>
<artifactId>sort</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.target>14</maven.compiler.target>
<maven.compiler.source>14</maven.compiler.source>
</properties>
<dependencies>
<dependency>
<scope>test</scope>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.6.2</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.6.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
package eu.happycoders.sort;
import eu.happycoders.sort.method.*;
import eu.happycoders.sort.utils.ArrayUtils;
import java.util.Locale;
import java.util.function.Function;
/**
* Measures the performance of all sorting algorithms for various input sizes.
*
* @author <a href="[email protected]">Sven Woltmann</a>
*/
public class CountOperations {
private static final int MIN_SORTING_SIZE = 1 << 3;
private static final int MAX_SORTING_SIZE = 1 << 15;
// Stop when counting takes longer than 20 seconds
private static final int MAX_COUNTING_TIME_SECS = 20;
public static void main(String[] args) {
for (SortAlgorithm algorithm : UltimateTest.ALGORITHMS) {
if (algorithm.supportsCounting()) {
countOps(algorithm);
}
}
}
private static void countOps(SortAlgorithm algorithm) {
// Test with a random, a sorted, and a reversed (= sorted descending) array
countOps(algorithm, "random", ArrayUtils::createRandomArray);
// Quicksort with right pivot element would go n x into recursion here...
if (algorithm.isSuitableForSortedInput()) {
countOps(algorithm, "ascending", ArrayUtils::createSortedArray);
countOps(algorithm, "descending", ArrayUtils::createReversedArray);
}
}
private static void countOps(SortAlgorithm algorithm,
String inputOrder,
Function<Integer, int[]> arraySupplier) {
System.out.printf("%n--- %s (order: %s) ---%n",
algorithm.getName(), inputOrder);
// Sort until sorting takes more than MAX_SORTING_TIME_SECS
// Upper limit used by insertion sort on already sorted data
for (int size = MIN_SORTING_SIZE;
size <= MAX_SORTING_SIZE && algorithm.isSuitableForInputSize(size);
size <<= 1) {
long time = System.currentTimeMillis();
Counters counters = countOps(algorithm, arraySupplier.apply(size));
time = System.currentTimeMillis() - time;
System.out.printf(Locale.US,
"%s (order: %s): size = %,11d --> %s%n",
algorithm.getName(),
inputOrder,
size,
counters);
// Stop after specified time
if (time > MAX_COUNTING_TIME_SECS * 1_000L) {
break;
}
}
}
private static Counters countOps(SortAlgorithm algorithm, int[] elements) {
Counters counters = new Counters();
algorithm.sort(elements, counters);
return counters;
}
}
package eu.happycoders.sort;
import eu.happycoders.sort.method.DualPivotQuicksort;
import eu.happycoders.sort.method.*;
import eu.happycoders.sort.utils.*;
import java.util.*;
import java.util.function.Function;
/**
* Measures the performance of all sorting algorithms for various input sizes.
*
* @author <a href="[email protected]">Sven Woltmann</a>
*/
public class UltimateTest {
static final SortAlgorithm[] ALGORITHMS = {
new InsertionSort(),
new SelectionSort(),
new BubbleSort(),
new Quicksort(Quicksort.PivotStrategy.MIDDLE),
new QuicksortImproved(48, Quicksort.PivotStrategy.MIDDLE),
new DualPivotQuicksort(DualPivotQuicksort.PivotStrategy.MIDDLES),
new DualPivotQuicksortImproved(48,
DualPivotQuicksort.PivotStrategy.MIDDLES),
new MergeSort(),
new HeapSort(),
new CountingSort(),
new JavaArraysSort()
};
private static final int WARM_UPS = 2;
private static final int ITERATIONS = 50;
private static final int MIN_SORTING_SIZE = 1 << 10;
private static final int MAX_SORTING_SIZE = 1 << 29;
// Stop when sorting takes longer than 20 seconds
private static final int MAX_SORTING_TIME_SECS = 20;
private static final boolean TEST_SORTED_INPUT = true;
private static final Map<String, Scorecard> scorecards = new HashMap<>();
public static void main(String[] args) {
for (int i = 1; i <= WARM_UPS; i++) {
System.out.printf("%n===== Warm up %d of %d =====%n", i, WARM_UPS);
for (SortAlgorithm algorithm : ALGORITHMS) {
test(algorithm, true);
}
}
for (int i = 1; i <= ITERATIONS; i++) {
System.out.printf("%n===== Iteration %d of %d =====%n", i, ITERATIONS);
for (SortAlgorithm algorithm : ALGORITHMS) {
test(algorithm, false);
}
System.out.printf("%n===== Results for iteration %d of %d =====%n", i,
ITERATIONS);
for (SortAlgorithm algorithm : ALGORITHMS) {
printResults(i, algorithm, "random");
if (TEST_SORTED_INPUT) {
printResults(i, algorithm, "ascending");
printResults(i, algorithm, "descending");
}
}
}
}
private static void test(SortAlgorithm algorithm, boolean warmingUp) {
// Test with a random, a sorted, and a reversed (= sorted descending) array
test(algorithm, "random", ArrayUtils::createRandomArray, warmingUp);
// Quicksort would go n x into recursion here...
if (TEST_SORTED_INPUT && algorithm.isSuitableForSortedInput()) {
test(algorithm, "ascending", ArrayUtils::createSortedArray, warmingUp);
test(algorithm, "descending", ArrayUtils::createReversedArray,
warmingUp);
}
}
private static void test(SortAlgorithm algorithm,
String inputOrder,
Function<Integer, int[]> arraySupplier,
boolean warmingUp) {
System.out.printf("%n--- %s (order: %s) ---%n",
algorithm.getName(), inputOrder);
// Sort until sorting takes more than MAX_SORTING_TIME_SECS
// Upper limit used by insertion sort on already sorted data
for (int size = MIN_SORTING_SIZE;
size <= MAX_SORTING_SIZE && algorithm.isSuitableForInputSize(size);
size <<= 1) {
long time = measureTime(algorithm, arraySupplier.apply(size));
boolean newRecord = !warmingUp
&& scorecard(algorithm, inputOrder, size, true).add(time);
System.out.printf(Locale.US,
"%s (order: %s): size = %,11d --> time = %,10.3f ms %s%n",
algorithm.getName(),
inputOrder,
size,
time / 1_000_000.0,
newRecord ? "<<< NEW RECORD :-)" : "");
// Stop after specified time
if (time > MAX_SORTING_TIME_SECS * 1_000_000_000L) {
break;
}
}
}
private static long measureTime(SortAlgorithm algorithm, int[] elements) {
System.gc();
long time = System.nanoTime();
algorithm.sort(elements);
return System.nanoTime() - time;
}
private static Scorecard scorecard(SortAlgorithm algorithm,
String inputOrder, int size,
boolean create) {
String key = algorithm.getName() + "/" + inputOrder + "/" + size;
return create
? scorecards.computeIfAbsent(key, Scorecard::new)
: scorecards.get(key);
}
private static void printResults(int iteration, SortAlgorithm algorithm,
String inputOrder) {
System.out.printf("%n--- Results %d/%d for: %s (order: %s) ---%n",
iteration, ITERATIONS, algorithm.getName(), inputOrder);
int longestNameLength = 0;
for (int size = MIN_SORTING_SIZE;
size <= MAX_SORTING_SIZE && algorithm.isSuitableForInputSize(size);
size <<= 1) {
Scorecard scorecard = scorecard(algorithm, inputOrder, size, false);
if (scorecard != null) {
int nameLength = scorecard.getName().length();
if (nameLength > longestNameLength) {
longestNameLength = nameLength;
}
}
}
for (int size = MIN_SORTING_SIZE;
size <= MAX_SORTING_SIZE && algorithm.isSuitableForInputSize(size);
size <<= 1) {
Scorecard scorecard = scorecard(algorithm, inputOrder, size, false);
if (scorecard != null) {
scorecard.printResult(longestNameLength, "");
}
}
}
}
package eu.happycoders.sort.comparisons;
import eu.happycoders.sort.method.DualPivotQuicksort;
import eu.happycoders.sort.method.*;
import java.util.*;
/**
* Compares Dual-Pivot Quicksort with the improved version for various
* thresholds at which the algorithm switches from Quicksort to Insertion Sort.
*
* @author <a href="[email protected]">Sven Woltmann</a>
*/
public class CompareImprovedDualPivotQuickSort extends DirectComparison {
private static final int SIZE = 5_555_555; // ~500 ms for Quicksort
public static void main(String[] args) {
List<SortAlgorithm> algorithms = new ArrayList<>();
algorithms.add(new DualPivotQuicksort(DualPivotQuicksort.PivotStrategy.MIDDLES));
for (int i = 2; i < 1 << 8; i <<= 1) {
algorithms.add(new DualPivotQuicksortImproved(i,
DualPivotQuicksort.PivotStrategy.MIDDLES));
// From 16 elements on, add i+i/2 (-> 16, 24, 32, 48, 64, 96, 128, ...)
if (i >= 16) {
int i2 = i + i / 2;
algorithms.add(new DualPivotQuicksortImproved(i2,
DualPivotQuicksort.PivotStrategy.MIDDLES));
}
}
runTest(algorithms.toArray(SortAlgorithm[]::new), SIZE);
}
}
package eu.happycoders.sort.comparisons;
import eu.happycoders.sort.method.*;
import eu.happycoders.sort.method.Quicksort.PivotStrategy;
import java.util.*;
/**
* Compares the regular Quicksort with the improved Quicksort for various
* thresholds at which the algorithm switches from Quicksort to Insertion Sort.
*
* @author <a href="[email protected]">Sven Woltmann</a>
*/
public class CompareImprovedQuickSort extends DirectComparison {
private static final int SIZE = 5_555_555; // ~500 ms for Quicksort
public static void main(String[] args) {
List<SortAlgorithm> algorithms = new ArrayList<>();
algorithms.add(new Quicksort(PivotStrategy.MIDDLE));
algorithms.add(new Quicksort(PivotStrategy.MEDIAN3));
for (int i = 2; i < 1 << 8; i <<= 1) {
algorithms.add(new QuicksortImproved(i, PivotStrategy.MIDDLE));
algorithms.add(new QuicksortImproved(i, PivotStrategy.MEDIAN3));
// From 16 elements on, add i+i/2 (-> 16, 24, 32, 48, 64, 96, 128, ...)
if (i >= 16) {
int i2 = i + i / 2;
algorithms.add(new QuicksortImproved(i2, PivotStrategy.MIDDLE));
algorithms.add(new QuicksortImproved(i2, PivotStrategy.MEDIAN3));
}
}
runTest(algorithms.toArray(SortAlgorithm[]::new), SIZE);
}
}
package eu.happycoders.sort.comparisons;
import eu.happycoders.sort.method.*;
/**
* Compares the three merge sort algorithms.
*
* @author <a href="[email protected]">Sven Woltmann</a>
*/
public class CompareMergeSorts extends DirectComparison {
private static final int SIZE = 5_555_555; // ~840 ms for Merge Sort
public static void main(String[] args) {
SortAlgorithm[] algorithms = new SortAlgorithm[]{
new MergeSort(),
new MergeSort2(),
new MergeSort3()
};
runTest(algorithms, SIZE);
}
}
package eu.happycoders.sort.comparisons;
import eu.happycoders.sort.method.*;
import eu.happycoders.sort.method.Quicksort.PivotStrategy;
import java.util.*;
/**
* Compares the regular Quicksort with the improved Quicksort for various
* thresholds at which the algorithm switches from Quicksort to Insertion Sort.
*
* @author <a href="[email protected]">Sven Woltmann</a>
*/
public class CompareQuickSorts extends DirectComparison {
private static final int SIZE = 5_555_555; // ~500 ms for Quicksort
public static void main(String[] args) {
List<SortAlgorithm> algorithms = new ArrayList<>();
algorithms.add(new Quicksort(PivotStrategy.MIDDLE));
algorithms.add(new Quicksort(PivotStrategy.MEDIAN3));
runTest(algorithms.toArray(SortAlgorithm[]::new), SIZE);
}
}
package eu.happycoders.sort.comparisons;
import eu.happycoders.sort.method.SortAlgorithm;
import eu.happycoders.sort.utils.*;
import java.util.*;
/**
* Base class to directly compare two or more sort algorithms.
*
* @author <a href="[email protected]">Sven Woltmann</a>
*/
public class DirectComparison {
private static final int WARM_UPS = 10;
private static final int ITERATIONS = 100;
private static final int PRINT_RESULTS_ALL_X_ITERATIONS = 10;
private static final Map<String, Scorecard> scorecards = new HashMap<>();
private static int longestNameLength;
public static void runTest(SortAlgorithm[] algorithms, int size) {
longestNameLength = Scorecard.findLongestAlgorithmName(algorithms);
int numAlgorithms = algorithms.length;
System.out.println("----- Warming up -----");
for (int i = 1; i <= WARM_UPS; i++) {
System.out.printf("Iteration %d%n", i);
int[] elements = ArrayUtils.createRandomArray(size);
// The order in which we test affects which test is the fastest.
// Maybe the JDK optimizes some parts specific to the input data!?
// --> Shuffle the algorithms!
int[] indices = getShuffledIndices(numAlgorithms);
for (int j = 0; j < numAlgorithms; j++) {
SortAlgorithm algorithm = algorithms[indices[j]];
measure(algorithm, elements.clone());
System.out.println();
}
}
System.out.println("\n----- Measuring -----");
for (int i = 1; i <= ITERATIONS; i++) {
System.out.printf("Iteration %d%n", i);
int[] elements = ArrayUtils.createRandomArray(size);
int[] indices = getShuffledIndices(numAlgorithms);
for (int j = 0; j < numAlgorithms; j++) {
SortAlgorithm algorithm = algorithms[indices[j]];
long time = measure(algorithm, elements.clone());
boolean newRecord = scorecardForAlgorithm(algorithm).add(time);
System.out.println(newRecord ? " <<< NEW RECORD :-)" : "");
}
if (i % PRINT_RESULTS_ALL_X_ITERATIONS == 0) {
printResults(algorithms, i);
System.out.println();
}
}
}
private static int[] getShuffledIndices(int numTestSets) {
int[] indices = ArrayUtils.createSortedArray(numTestSets);
ArrayUtils.shuffle(indices);
return indices;
}
private static long measure(SortAlgorithm sortAlgorithm, int[] elements) {
System.gc();
long time = System.nanoTime();
sortAlgorithm.sort(elements);
time = System.nanoTime() - time;
System.out.printf(Locale.US,
" %-" + longestNameLength + "s -> time = %,10.3f ms",
sortAlgorithm.getName(), (time / 1_000_000.0));
return time;
}
private static Scorecard scorecardForAlgorithm(SortAlgorithm algorithm) {
return scorecards.computeIfAbsent(algorithm.getName(), Scorecard::new);
}
private static void printResults(SortAlgorithm[] algorithms, int iterations) {
System.out.printf("%n----- Results after %d iterations-----%n", iterations);
// 1. Find the fastest median
int fastestAlgorithmIndex = 0;
long fastestAlgorithmMedian = Long.MAX_VALUE;
for (int i = 0; i < algorithms.length; i++) {
long median = scorecardForAlgorithm(algorithms[i]).getMedian();
if (median < fastestAlgorithmMedian) {
fastestAlgorithmIndex = i;
fastestAlgorithmMedian = median;
}
}
// 2. Print them, showing which one has the fastest median
for (int i = 0; i < algorithms.length; i++) {
scorecardForAlgorithm(algorithms[i]).printResult(longestNameLength,
i == fastestAlgorithmIndex ? "<<< FASTEST MEDIAN :-)" : null);
}
}
}
package eu.happycoders.sort.method;
/**
* Bubble Sort implementation for performance tests.
*
* @author <a href="[email protected]">Sven Woltmann</a>
*/
public class BubbleSort implements SortAlgorithm {
@Override
public void sort(int[] elements) {
for (int max = elements.length - 1; max > 0; max--) {
boolean swapped = false;
for (int i = 0; i < max; i++) {
int left = elements[i];
int right = elements[i + 1];
if (left > right) {
elements[i + 1] = left;
elements[i] = right;
swapped = true;
}
}
if (!swapped) break;
}
}
@Override
public void sort(int[] elements, Counters counters) {
for (int max = elements.length - 1; max > 0; max--) {
counters.incIterations();
boolean swapped = false;
for (int i = 0; i < max; i++) {
counters.incIterations();
int left = elements[i];
int right = elements[i + 1];
counters.addReads(2);
counters.incComparisons();
if (left > right) {
elements[i + 1] = left;
elements[i] = right;
counters.addWrites(2);
swapped = true;
}
}
if (!swapped) break;
}
}
}
package eu.happycoders.sort.method;
import java.util.Locale;
/**
* Counter for sort operations.
*
* @author <a href="[email protected]">Sven Woltmann</a>
*/
public class Counters {
private long iterations;
private long comparisons;
private long reads;
private long writes;
public void incIterations() {
iterations++;
}
public void addIterations(int x) {
iterations += x;
}
public void incComparisons() {
comparisons++;
}
public void addComparisons(int x) {
comparisons += x;
}
public void incReads() {
reads++;
}
public void addReads(int x) {
reads += x;
}
public void incWrites() {
writes++;
}
public void addWrites(int x) {
writes += x;
}