authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-02-18 12:13:34+01:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-05-29 05:47:35+02:00
log0569f1f6a779d8021cac1a63334564347f617605
treec818b7cffa19b2ac6cf80c84c80b86aec0ccacec
parent7bd363f61b024c8925eaf86cbf87315094d5ae04

std.sort.pdq: use unguarded insertion for non-leftmost partitions

After partitioning, the pivot acts as a sentinel for the right sub-partition, guaranteeing termination without a bounds check. So we can skip the `j > a` check for all non-leftmost partitions. Give a consistent ~10-50% speedup, with the biggest gains at smaller sizes.

1 files changed, 22 insertions(+), 5 deletions(-)

lib/std/sort/pdq.zig+22-5
...@@ -47,10 +47,10 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void {...@@ -47,10 +47,10 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void {
47 const max_limit = std.math.floorPowerOfTwo(usize, b - a) + 1;47 const max_limit = std.math.floorPowerOfTwo(usize, b - a) + 1;
4848
49 // set upper bound on stack memory usage.49 // set upper bound on stack memory usage.
50 const Range = struct { a: usize, b: usize, limit: usize };50 const Range = struct { a: usize, b: usize, limit: usize, leftmost: bool };
51 const stack_size = math.log2(math.maxInt(usize) + 1);51 const stack_size = math.log2(math.maxInt(usize) + 1);
52 var stack: [stack_size]Range = undefined;52 var stack: [stack_size]Range = undefined;
53 var range = Range{ .a = a, .b = b, .limit = max_limit };53 var range = Range{ .a = a, .b = b, .limit = max_limit, .leftmost = true };
54 var top: usize = 0;54 var top: usize = 0;
5555
56 while (true) {56 while (true) {
...@@ -62,7 +62,11 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void {...@@ -62,7 +62,11 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void {
6262
63 // very short slices get sorted using insertion sort.63 // very short slices get sorted using insertion sort.
64 if (len <= max_insertion) {64 if (len <= max_insertion) {
65 break sort.insertionContext(range.a, range.b, context);65 if (range.leftmost) {
66 break sort.insertionContext(range.a, range.b, context);
67 } else {
68 break unguardedInsertionContext(range.a, range.b, context);
69 }
66 }70 }
6771
68 // if too many bad pivot choices were made, simply fall back to heapsort in order to72 // if too many bad pivot choices were made, simply fall back to heapsort in order to
...@@ -115,12 +119,13 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void {...@@ -115,12 +119,13 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void {
115 const balanced_threshold = len / 8;119 const balanced_threshold = len / 8;
116 if (left_len < right_len) {120 if (left_len < right_len) {
117 was_balanced = left_len >= balanced_threshold;121 was_balanced = left_len >= balanced_threshold;
118 stack[top] = .{ .a = range.a, .b = mid, .limit = range.limit };122 stack[top] = .{ .a = range.a, .b = mid, .limit = range.limit, .leftmost = range.leftmost };
119 top += 1;123 top += 1;
120 range.a = mid + 1;124 range.a = mid + 1;
125 range.leftmost = false;
121 } else {126 } else {
122 was_balanced = right_len >= balanced_threshold;127 was_balanced = right_len >= balanced_threshold;
123 stack[top] = .{ .a = mid + 1, .b = range.b, .limit = range.limit };128 stack[top] = .{ .a = mid + 1, .b = range.b, .limit = range.limit, .leftmost = false };
124 top += 1;129 top += 1;
125 range.b = mid;130 range.b = mid;
126 }131 }
...@@ -131,6 +136,18 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void {...@@ -131,6 +136,18 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void {
131 }136 }
132}137}
133138
139/// Insertion sort that assumes `items[a-1]` exists and is <= all elements in `[a, b)`,
140/// allowing the inner loop to skip the bounds check.
141fn unguardedInsertionContext(a: usize, b: usize, context: anytype) void {
142 var i = a + 1;
143 while (i < b) : (i += 1) {
144 var j = i;
145 while (context.lessThan(j, j - 1)) : (j -= 1) {
146 context.swap(j, j - 1);
147 }
148 }
149}
150
134/// partitions `items[a..b]` into elements smaller than `items[pivot]`,151/// partitions `items[a..b]` into elements smaller than `items[pivot]`,
135/// followed by elements greater than or equal to `items[pivot]`.152/// followed by elements greater than or equal to `items[pivot]`.
136///153///