Line | Count | Source (jump to first uncovered line) |
1 | | /* SPDX-License-Identifier: GPL-3.0-or-later |
2 | | * Copyright © 2023-2025 The TokTok team. |
3 | | */ |
4 | | |
5 | | #include "sort.h" |
6 | | |
7 | | #include <assert.h> |
8 | | |
9 | | #include "attributes.h" |
10 | | #include "ccompat.h" |
11 | | #include "util.h" |
12 | | |
13 | | /** |
14 | | * @brief Threshold for when to switch to insertion sort. |
15 | | * |
16 | | * This is a trade-off between the complexity of insertion sort and the |
17 | | * overhead of merge sort. The threshold is chosen to be the smallest value |
18 | | * that gives a measurable speedup for insertion sort over merge sort. This is |
19 | | * based on measurements done in sort_bench.cc. Starting from 32 elements, |
20 | | * merge sort is faster than insertion sort in all our tests (both unsorted |
21 | | * and mostly-sorted). |
22 | | * |
23 | | * Toxcore has a lot of small arrays it wants to sort, so this optimisation |
24 | | * makes sense. |
25 | | */ |
26 | 131k | #define SMALL_ARRAY_THRESHOLD 16 |
27 | | |
28 | | static void merge_sort_merge_back(void *_Nonnull arr, const void *_Nonnull l_arr, uint32_t l_arr_size, const void *_Nonnull r_arr, uint32_t r_arr_size, uint32_t left_start, |
29 | | const void *_Nonnull object, const Sort_Funcs *_Nonnull funcs) |
30 | 1.18M | { |
31 | 1.18M | uint32_t li = 0; |
32 | 1.18M | uint32_t ri = 0; |
33 | 1.18M | uint32_t k = left_start; |
34 | | |
35 | 6.04M | while (li < l_arr_size && ri < r_arr_size) { |
36 | 4.85M | const void *l = funcs->get_callback(l_arr, li); |
37 | 4.85M | const void *r = funcs->get_callback(r_arr, ri); |
38 | | // !(r < l) <=> (r >= l) <=> (l <= r) |
39 | 4.85M | if (!funcs->less_callback(object, r, l)) { |
40 | 4.84M | funcs->set_callback(arr, k, l); |
41 | 4.84M | ++li; |
42 | 4.84M | } else { |
43 | 11.4k | funcs->set_callback(arr, k, r); |
44 | 11.4k | ++ri; |
45 | 11.4k | } |
46 | 4.85M | ++k; |
47 | 4.85M | } |
48 | | |
49 | | /* Copy the remaining elements of `l_arr[]`, if there are any. */ |
50 | 1.66M | while (li < l_arr_size) { |
51 | 475k | funcs->set_callback(arr, k, funcs->get_callback(l_arr, li)); |
52 | 475k | ++li; |
53 | 475k | ++k; |
54 | 475k | } |
55 | | |
56 | | /* Copy the remaining elements of `r_arr[]`, if there are any. */ |
57 | 5.31M | while (ri < r_arr_size) { |
58 | 4.12M | funcs->set_callback(arr, k, funcs->get_callback(r_arr, ri)); |
59 | 4.12M | ++ri; |
60 | 4.12M | ++k; |
61 | 4.12M | } |
62 | 1.18M | } |
63 | | |
64 | | /** Function to merge the two haves `arr[left_start..mid]` and `arr[mid+1..right_end]` of array `arr[]`. */ |
65 | | static void merge_sort_merge(void *_Nonnull arr, uint32_t left_start, uint32_t mid, uint32_t right_end, void *_Nonnull tmp, const void *_Nonnull object, const Sort_Funcs *_Nonnull funcs) |
66 | 1.18M | { |
67 | 1.18M | const uint32_t l_arr_size = mid - left_start + 1; |
68 | 1.18M | const uint32_t r_arr_size = right_end - mid; |
69 | | |
70 | | /* Temporary arrays, using the tmp buffer created in `merge_sort` below. */ |
71 | 1.18M | void *l_arr = funcs->subarr_callback(tmp, 0, l_arr_size); |
72 | 1.18M | void *r_arr = funcs->subarr_callback(tmp, l_arr_size, r_arr_size); |
73 | | |
74 | | /* Copy data to temp arrays `l_arr[]` and `r_arr[]`. |
75 | | * |
76 | | * This is iterating and repeatedly calling `get` and `set`, which sounds |
77 | | * slow, but is only marginally slower than having a `copy` callback. With |
78 | | * a `copy` callback, we'd save 3-4% in time. |
79 | | */ |
80 | 6.50M | for (uint32_t i = 0; i < l_arr_size; ++i) { |
81 | 5.31M | funcs->set_callback(l_arr, i, funcs->get_callback(arr, left_start + i)); |
82 | 5.31M | } |
83 | 5.32M | for (uint32_t i = 0; i < r_arr_size; ++i) { |
84 | 4.13M | funcs->set_callback(r_arr, i, funcs->get_callback(arr, mid + 1 + i)); |
85 | 4.13M | } |
86 | | |
87 | | /* Merge the temp arrays back into `arr[left_start..right_end]`. */ |
88 | 1.18M | merge_sort_merge_back(arr, l_arr, l_arr_size, r_arr, r_arr_size, left_start, object, funcs); |
89 | 1.18M | } |
90 | | |
91 | | static void insertion_sort_step(void *_Nonnull arr, void *_Nonnull tmp, uint32_t i, const void *_Nonnull object, const Sort_Funcs *_Nonnull funcs) |
92 | 968k | { |
93 | 968k | funcs->set_callback(tmp, 0, funcs->get_callback(arr, i)); |
94 | 968k | uint32_t j = i; |
95 | | |
96 | 1.13M | while (j > 0) { |
97 | 1.10M | if (!funcs->less_callback(object, tmp, funcs->get_callback(arr, j - 1))) { |
98 | 941k | break; |
99 | 941k | } |
100 | 164k | funcs->set_callback(arr, j, funcs->get_callback(arr, j - 1)); |
101 | 164k | --j; |
102 | 164k | } |
103 | | |
104 | 968k | funcs->set_callback(arr, j, tmp); |
105 | 968k | } |
106 | | |
107 | | static void insertion_sort_with_buf(void *_Nonnull arr, uint32_t arr_size, void *_Nonnull tmp, uint32_t tmp_size, const void *_Nonnull object, const Sort_Funcs *_Nonnull funcs) |
108 | 116k | { |
109 | 1.08M | for (uint32_t i = 1; i < arr_size; ++i) { |
110 | 968k | insertion_sort_step(arr, tmp, i, object, funcs); |
111 | 968k | } |
112 | 116k | } |
113 | | |
114 | | static bool insertion_sort(void *_Nonnull arr, uint32_t arr_size, const void *_Nonnull object, const Sort_Funcs *_Nonnull funcs) |
115 | 116k | { |
116 | 116k | void *tmp = funcs->alloc_callback(object, 1); |
117 | | |
118 | 116k | if (tmp == nullptr) { |
119 | 31 | return false; |
120 | 31 | } |
121 | | |
122 | 116k | insertion_sort_with_buf(arr, arr_size, tmp, 1, object, funcs); |
123 | | |
124 | 116k | funcs->delete_callback(object, tmp, 1); |
125 | 116k | return true; |
126 | 116k | } |
127 | | |
128 | | void merge_sort_with_buf(void *arr, uint32_t arr_size, void *tmp, uint32_t tmp_size, const void *object, const Sort_Funcs *funcs) |
129 | 7.38k | { |
130 | 7.38k | assert(tmp_size >= arr_size); |
131 | | |
132 | 7.38k | if (arr_size <= SMALL_ARRAY_THRESHOLD) { |
133 | 0 | assert(tmp_size >= 1); |
134 | 0 | insertion_sort_with_buf(arr, arr_size, tmp, tmp_size, object, funcs); |
135 | 0 | return; |
136 | 0 | } |
137 | | |
138 | | // Merge subarrays in bottom up manner. First merge subarrays of |
139 | | // size 1 to create sorted subarrays of size 2, then merge subarrays |
140 | | // of size 2 to create sorted subarrays of size 4, and so on. |
141 | 66.4k | for (uint32_t curr_size = 1; curr_size <= arr_size - 1; curr_size = 2 * curr_size) { |
142 | | // Pick starting point of different subarrays of current size |
143 | 1.24M | for (uint32_t left_start = 0; left_start < arr_size - 1; left_start += 2 * curr_size) { |
144 | | // Find ending point of left subarray. mid+1 is starting |
145 | | // point of right |
146 | 1.18M | const uint32_t mid = min_u32(left_start + curr_size - 1, arr_size - 1); |
147 | 1.18M | const uint32_t right_end = min_u32(left_start + 2 * curr_size - 1, arr_size - 1); |
148 | | |
149 | | // Merge Subarrays arr[left_start...mid] & arr[mid+1...right_end] |
150 | 1.18M | merge_sort_merge(arr, left_start, mid, right_end, tmp, object, funcs); |
151 | 1.18M | } |
152 | 59.0k | } |
153 | 7.38k | } |
154 | | |
155 | | bool merge_sort(void *arr, uint32_t arr_size, const void *object, const Sort_Funcs *funcs) |
156 | 123k | { |
157 | 123k | if (arr_size <= SMALL_ARRAY_THRESHOLD) { |
158 | 116k | return insertion_sort(arr, arr_size, object, funcs); |
159 | 116k | } |
160 | | |
161 | 7.39k | void *tmp = funcs->alloc_callback(object, arr_size); |
162 | | |
163 | 7.39k | if (tmp == nullptr) { |
164 | 8 | return false; |
165 | 8 | } |
166 | | |
167 | 7.38k | merge_sort_with_buf(arr, arr_size, tmp, arr_size, object, funcs); |
168 | | |
169 | 7.38k | funcs->delete_callback(object, tmp, arr_size); |
170 | 7.38k | return true; |
171 | 7.39k | } |