From c3be3f2acd70d84ea38e555861c3cac1687caa54 Mon Sep 17 00:00:00 2001 From: Niklas Fiekas Date: Mon, 10 Nov 2025 21:16:22 +0100 Subject: [PATCH] switch from heap-based partialSort to quickselect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before (based on 13a0d52924814bc): HuffmanPgnBench.decode avgt 40 4297.839 ± 63.417 us/op After: HuffmanPgnBench.decode avgt 40 3344.431 ± 15.417 us/op # quickselect only <-- HuffmanPgnBench.decode avgt 20 3344.925 ± 31.764 us/op # insertionSortCutoff = 3 HuffmanPgnBench.decode avgt 20 3345.941 ± 29.388 us/op # insertionSortCutoff = 4 HuffmanPgnBench.decode avgt 40 3370.571 ± 24.539 us/op # insertionSortCutoff = 5 HuffmanPgnBench.decode avgt 40 3365.226 ± 25.166 us/op # insertionSortCutoff = 6 HuffmanPgnBench.decode avgt 120 3390.946 ± 17.816 us/op # insertionSortCutoff = 7 HuffmanPgnBench.decode avgt 40 3415.760 ± 27.759 us/op # sortCutoff = 4 HuffmanPgnBench.decode avgt 40 3431.564 ± 17.375 us/op # sortCutoff = 5 HuffmanPgnBench.decode avgt 40 3437.750 ± 23.575 us/op # sortCutoff = 6 HuffmanPgnBench.decode avgt 40 3430.231 ± 30.012 us/op # sortCutoff = 7 HuffmanPgnBench.decode avgt 40 3430.460 ± 16.339 us/op # sortCutoff = 8 HuffmanPgnBench.decode avgt 40 3504.320 ± 20.214 us/op # sortCutoff = 10 --- src/main/scala/game/Encoder.scala | 5 ++- src/main/scala/game/MoveList.scala | 54 +++++++++++++----------------- 2 files changed, 25 insertions(+), 34 deletions(-) diff --git a/src/main/scala/game/Encoder.scala b/src/main/scala/game/Encoder.scala index ecccda9..e7c3b62 100644 --- a/src/main/scala/game/Encoder.scala +++ b/src/main/scala/game/Encoder.scala @@ -96,9 +96,8 @@ object Encoder: if 0 < i then if board.isCheck() then output(i - 1) += (if legals.isEmpty then "#" else "+") if i < plies then - val moveIndex = Huffman.read(reader) - legals.partialSort(moveIndex + 1) - val move = legals.get(moveIndex) + val rank = Huffman.read(reader) + val move = legals.selectRank(rank) output(i) = san(move, legals) board.play(move) diff --git a/src/main/scala/game/MoveList.scala b/src/main/scala/game/MoveList.scala index 938d90f..7e27e75 100644 --- a/src/main/scala/game/MoveList.scala +++ b/src/main/scala/game/MoveList.scala @@ -51,37 +51,29 @@ final class MoveList(capacity: Int = 256): buffer(i) = buffer(j) buffer(j) = tmp - def partialSort(last: Int): Unit = - require(last <= size) - makeHeap(last) - for i <- last until size do - if buffer(i) < buffer(0) then - swap(0, i) - adjustHeap(0, last) - sortHeap(last) - - private def makeHeap(last: Int): Unit = - for parent <- last / 2 until 0 by -1 do adjustHeap(parent - 1, last) - - private def adjustHeap(holeIndex: Int, len: Int): Unit = - require(len <= size) - require(holeIndex < size) - var leftChild = holeIndex * 2 + 1 - var holeDest = holeIndex - val tmp = buffer(holeDest) - while leftChild < len do - if leftChild + 1 < len && buffer(leftChild) < buffer(leftChild + 1) then leftChild += 1 - if tmp < buffer(leftChild) then - buffer(holeDest) = buffer(leftChild) - holeDest = leftChild - leftChild = leftChild * 2 + 1 - else leftChild = len - buffer(holeDest) = tmp - - private def sortHeap(last: Int): Unit = - for i <- last - 1 until 0 by -1 do - swap(0, i) - adjustHeap(0, i) + def selectRank(rank: Int): Move = + require(rank < size) + // Quickselect. Bounds are small enough that naive pivot selection is ok, + // even on adversarial inputs. + var left = 0 + var right = size - 1 + while left < right do + val pivot = partition(left, right) + if pivot == rank then return buffer(rank) + if pivot < rank then left = pivot + 1 + else right = pivot - 1 + buffer(rank) + + private def partition(left: Int, right: Int): Int = + val pivot = buffer(right) + var i = left - 1 + for j <- left until right do + if buffer(j) < pivot then + i += 1 + swap(i, j) + i += 1 + swap(i, right) + i def pretty(): String = val builder = StringBuilder()