From d8be4c4b6e45b2ef2ee4af352a8e8f0dfce24d94 Mon Sep 17 00:00:00 2001 From: sanenelisiwe1975 Date: Tue, 7 Apr 2026 12:44:05 +0200 Subject: [PATCH] Optimize _quickSort with median-of-three pivot selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Using the first element as pivot degrades to O(n²) on already-sorted inputs, which is the most common real-world case. Switch to median-of- three selection (first, middle, last) to guarantee a balanced split on sorted inputs and avoid worst-case behavior. For arrays of exactly 2 elements the middle and last pointers coincide, so the full three-way selection is skipped and a single comparison sorts them directly. --- .changeset/quicksort-median-pivot.md | 5 +++++ contracts/utils/Arrays.sol | 13 ++++++++++++- scripts/generate/templates/Arrays.js | 13 ++++++++++++- 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 .changeset/quicksort-median-pivot.md diff --git a/.changeset/quicksort-median-pivot.md b/.changeset/quicksort-median-pivot.md new file mode 100644 index 00000000000..133776b1da4 --- /dev/null +++ b/.changeset/quicksort-median-pivot.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`Arrays`: Optimize `_quickSort` with median-of-three pivot selection to avoid worst-case O(n²) behavior on already-sorted inputs. diff --git a/contracts/utils/Arrays.sol b/contracts/utils/Arrays.sol index f837004ff92..f730b644973 100644 --- a/contracts/utils/Arrays.sol +++ b/contracts/utils/Arrays.sol @@ -116,7 +116,18 @@ library Arrays { unchecked { if (end - begin < 0x40) return; - // Use first element as pivot + // Median-of-three pivot selection: pick the first, middle, and last elements, sort them, + // then use the middle value as pivot. This avoids worst-case O(n²) behavior on sorted inputs. + uint256 mid = begin + (((end - begin) >> 1) & ~uint256(0x1f)); + if (comp(_mload(mid), _mload(begin))) _swap(begin, mid); + if (end - begin > 0x40) { + uint256 last = end - 0x20; + if (comp(_mload(last), _mload(begin))) _swap(begin, last); + if (comp(_mload(last), _mload(mid))) _swap(mid, last); + _swap(begin, mid); // Put median at begin to use as pivot + } + + // Use first element (now the median of first/middle/last) as pivot uint256 pivot = _mload(begin); // Position where the pivot should be at the end of the loop uint256 pos = begin; diff --git a/scripts/generate/templates/Arrays.js b/scripts/generate/templates/Arrays.js index 3dd4d8c7eea..605c842e12b 100644 --- a/scripts/generate/templates/Arrays.js +++ b/scripts/generate/templates/Arrays.js @@ -64,7 +64,18 @@ function _quickSort(uint256 begin, uint256 end, function(uint256, uint256) pure unchecked { if (end - begin < 0x40) return; - // Use first element as pivot + // Median-of-three pivot selection: pick the first, middle, and last elements, sort them, + // then use the middle value as pivot. This avoids worst-case O(n²) behavior on sorted inputs. + uint256 mid = begin + (((end - begin) >> 1) & ~uint256(0x1f)); + if (comp(_mload(mid), _mload(begin))) _swap(begin, mid); + if (end - begin > 0x40) { + uint256 last = end - 0x20; + if (comp(_mload(last), _mload(begin))) _swap(begin, last); + if (comp(_mload(last), _mload(mid))) _swap(mid, last); + _swap(begin, mid); // Put median at begin to use as pivot + } + + // Use first element (now the median of first/middle/last) as pivot uint256 pivot = _mload(begin); // Position where the pivot should be at the end of the loop uint256 pos = begin;