authorgravatar for 2762638a@student.gla.ac.ukPaul Anderson <2762638a@student.gla.ac.uk> 2026-09-01 00:07:30+01:00
committergravatar for alichraghi@noreply.codeberg.orgAli Cheraghi <alichraghi@noreply.codeberg.org> 2026-09-05 17:50:56+02:00
logaf24fd11a5c37ef5531ddb86356cd6c55ed71d77
treef6468929ad369fa6822e8f50c14d242220daac0f
parent1dd182d24b1b0da00ef0803898024734204dca5c

std.sort.pdq: push larger partition to stack to avoid stack overflow


1 files changed, 57 insertions(+), 23 deletions(-)

lib/std/sort/pdq.zig+57-23
...@@ -35,6 +35,15 @@ const Hint = enum {...@@ -35,6 +35,15 @@ const Hint = enum {
35 unknown,35 unknown,
36};36};
3737
38const Range = struct {
39 a: usize,
40 b: usize,
41 limit: usize,
42 leftmost: bool,
43 balanced: bool,
44 partitioned: bool,
45};
46
38/// Unstable in-place sort. O(n) best case, O(n*log(n)) worst case and average case.47/// Unstable in-place sort. O(n) best case, O(n*log(n)) worst case and average case.
39/// O(log(n)) memory (no allocator required).48/// O(log(n)) memory (no allocator required).
40/// `context` must have methods `swap` and `lessThan`,49/// `context` must have methods `swap` and `lessThan`,
...@@ -46,16 +55,19 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void {...@@ -46,16 +55,19 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void {
46 // number of allowed imbalanced partitions before switching to heap sort.55 // number of allowed imbalanced partitions before switching to heap sort.
47 const max_limit = if (b > a) math.log2_int(usize, b - a) else 0;56 const max_limit = if (b > a) math.log2_int(usize, b - a) else 0;
4857
49 // set upper bound on stack memory usage.58 // stack usage is bounded by log_2(n) due to placing longer partition onto stack each iteration.
50 const Range = struct { a: usize, b: usize, limit: usize, leftmost: bool };59 var stack: [@bitSizeOf(usize)]Range = undefined;
51 var stack: [2 * @bitSizeOf(usize)]Range = undefined;60 var range = Range{
52 var range = Range{ .a = a, .b = b, .limit = max_limit, .leftmost = true };61 .a = a,
62 .b = b,
63 .limit = max_limit,
64 .leftmost = true,
65 .balanced = true,
66 .partitioned = true,
67 };
53 var top: usize = 0;68 var top: usize = 0;
5469
55 while (true) {70 while (true) {
56 var was_balanced = true;
57 var was_partitioned = true;
58
59 while (true) {71 while (true) {
60 const len = range.b - range.a;72 const len = range.b - range.a;
6173
...@@ -76,7 +88,7 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void {...@@ -76,7 +88,7 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void {
7688
77 // if the last partitioning was imbalanced, try breaking patterns in the slice by shuffling89 // if the last partitioning was imbalanced, try breaking patterns in the slice by shuffling
78 // some elements around. Hopefully we'll choose a better pivot this time.90 // some elements around. Hopefully we'll choose a better pivot this time.
79 if (!was_balanced) {91 if (!range.balanced) {
80 breakPatterns(range.a, range.b, context);92 breakPatterns(range.a, range.b, context);
81 range.limit -= 1;93 range.limit -= 1;
82 }94 }
...@@ -95,7 +107,7 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void {...@@ -95,7 +107,7 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void {
95107
96 // if the last partitioning was decently balanced and didn't shuffle elements, and if pivot108 // if the last partitioning was decently balanced and didn't shuffle elements, and if pivot
97 // selection predicts the slice is likely already sorted...109 // selection predicts the slice is likely already sorted...
98 if (was_balanced and was_partitioned and hint == .increasing) {110 if (range.balanced and range.partitioned and hint == .increasing) {
99 // try identifying several out-of-order elements and shifting them to correct111 // try identifying several out-of-order elements and shifting them to correct
100 // positions. If the slice ends up being completely sorted, we're done.112 // positions. If the slice ends up being completely sorted, we're done.
101 if (partialInsertionSort(range.a, range.b, context)) break;113 if (partialInsertionSort(range.a, range.b, context)) break;
...@@ -111,23 +123,45 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void {...@@ -111,23 +123,45 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void {
111123
112 // partition the slice.124 // partition the slice.
113 var mid = pivot;125 var mid = pivot;
114 was_partitioned = partition(range.a, range.b, &mid, context);126 const was_partitioned = partition(range.a, range.b, &mid, context);
115127
116 const left_len = mid - range.a;128 const left_len = mid - range.a;
117 const right_len = range.b - mid;129 const right_len = range.b - (mid + 1);
118 const balanced_threshold = len / 8;130 const balanced_threshold = len / 8;
119 if (left_len < right_len) {131
120 was_balanced = left_len >= balanced_threshold;132 const left_is_smaller = left_len < right_len;
121 stack[top] = .{ .a = range.a, .b = mid, .limit = range.limit, .leftmost = range.leftmost };133
122 top += 1;134 const smaller_len = if (left_is_smaller) left_len else right_len;
123 range.a = mid + 1;135 const was_balanced = smaller_len >= balanced_threshold;
124 range.leftmost = false;136
125 } else {137 const smaller_start_offset = if (left_is_smaller) range.a else mid + 1;
126 was_balanced = right_len >= balanced_threshold;138 const larger_start_offset = if (left_is_smaller) mid + 1 else range.a;
127 stack[top] = .{ .a = mid + 1, .b = range.b, .limit = range.limit, .leftmost = false };139 const smaller_end_exclusive_offset = if (left_is_smaller) mid else range.b;
128 top += 1;140 const larger_end_exclusive_offset = if (left_is_smaller) range.b else mid;
129 range.b = mid;141
130 }142 const smaller_is_leftmost = if (left_is_smaller) range.leftmost else false;
143 const larger_is_leftmost = if (left_is_smaller) false else range.leftmost;
144
145 // defer sorting the larger range until later to ensure stack usage is always less than log_2(n):
146 // as if we always push more than half the range to the stack then each time we push to the stack
147 // we reduce the amount of items we can push to it in later iterations by at least n/2
148 // therefore the count of items on the stack can never be more than log_2(n)
149 stack[top] = .{
150 .a = larger_start_offset,
151 .b = larger_end_exclusive_offset,
152 .limit = range.limit,
153 .leftmost = larger_is_leftmost,
154 .balanced = was_balanced,
155 .partitioned = was_partitioned,
156 };
157 top += 1;
158
159 // sort the smaller range immediately
160 range.a = smaller_start_offset;
161 range.b = smaller_end_exclusive_offset;
162 range.leftmost = smaller_is_leftmost;
163 range.balanced = true; // this either already true, or the range is small so we don't care
164 range.partitioned = was_partitioned;
131 }165 }
132166
133 top = math.sub(usize, top, 1) catch break;167 top = math.sub(usize, top, 1) catch break;