From: Peter Taraba <peter.schuster.taraba@gmail.com>
To: akpm@linux-foundation.org
Cc: linux-kernel@vger.kernel.org,
Peter Taraba <peter.schuster.taraba@gmail.com>
Subject: [PATCH] lib: add statsort(), a distribution/bucket sort
Date: Tue, 4 Aug 2026 16:34:49 -0700 [thread overview]
Message-ID: <20260804233449.70815-1-peter.schuster.taraba@gmail.com> (raw)
Adds statsort(), a bucket/distribution sort for arrays keyed by a
signed 64-bit value. Recursively distributes elements into sqrt(n)
buckets based on linear interpolation of the key over the current
range, narrowing the range at each level. Falls back to sort_r()
(lib/sort.c) for small ranges, skewed/degenerate key distributions,
and allocation failure, so worst-case behavior is never worse than
sort_r() plus O(n) bucketing overhead.
Benchmarked against sort() and glibc qsort() on uniform and
Gaussian-distributed long arrays:
❯ ./run.sh
Compiling...
Running...
Running edge-case checks...
Edge cases OK.
== n = 10000 distribution = uniform ==
statsort_longs best of 5: 0.0004 s ( 37.04 ns/elem)
sort (kernel) best of 5: 0.0008 s ( 78.48 ns/elem)
qsort (libc) best of 5: 0.0007 s ( 74.27 ns/elem)
== n = 10000 distribution = gaussian ==
statsort_longs best of 5: 0.0004 s ( 39.69 ns/elem)
sort (kernel) best of 5: 0.0008 s ( 77.78 ns/elem)
qsort (libc) best of 5: 0.0007 s ( 74.42 ns/elem)
== n = 10000 distribution = duplicates ==
statsort_longs best of 5: 0.0008 s ( 78.19 ns/elem)
sort (kernel) best of 5: 0.0007 s ( 74.88 ns/elem)
qsort (libc) best of 5: 0.0005 s ( 47.52 ns/elem)
== n = 10000 distribution = small_range ==
statsort_longs best of 5: 0.0006 s ( 55.06 ns/elem)
sort (kernel) best of 5: 0.0008 s ( 77.46 ns/elem)
qsort (libc) best of 5: 0.0006 s ( 61.69 ns/elem)
== n = 10000 distribution = sorted ==
statsort_longs best of 5: 0.0002 s ( 24.67 ns/elem)
sort (kernel) best of 5: 0.0007 s ( 73.20 ns/elem)
qsort (libc) best of 5: 0.0002 s ( 17.29 ns/elem)
== n = 10000 distribution = reversed ==
statsort_longs best of 5: 0.0004 s ( 39.31 ns/elem)
sort (kernel) best of 5: 0.0007 s ( 73.46 ns/elem)
qsort (libc) best of 5: 0.0002 s ( 22.86 ns/elem)
== n = 10000 distribution = constant ==
statsort_longs best of 5: 0.0000 s ( 1.38 ns/elem)
sort (kernel) best of 5: 0.0006 s ( 62.94 ns/elem)
qsort (libc) best of 5: 0.0002 s ( 17.30 ns/elem)
== n = 100000 distribution = uniform ==
statsort_longs best of 5: 0.0042 s ( 41.62 ns/elem)
sort (kernel) best of 5: 0.0112 s ( 111.84 ns/elem)
qsort (libc) best of 5: 0.0093 s ( 93.15 ns/elem)
== n = 100000 distribution = gaussian ==
statsort_longs best of 5: 0.0042 s ( 41.71 ns/elem)
sort (kernel) best of 5: 0.0114 s ( 113.96 ns/elem)
qsort (libc) best of 5: 0.0094 s ( 93.86 ns/elem)
== n = 100000 distribution = duplicates ==
statsort_longs best of 5: 0.0106 s ( 105.76 ns/elem)
sort (kernel) best of 5: 0.0097 s ( 96.88 ns/elem)
qsort (libc) best of 5: 0.0055 s ( 54.64 ns/elem)
== n = 100000 distribution = small_range ==
statsort_longs best of 5: 0.0080 s ( 80.38 ns/elem)
sort (kernel) best of 5: 0.0101 s ( 100.98 ns/elem)
qsort (libc) best of 5: 0.0070 s ( 70.33 ns/elem)
== n = 100000 distribution = sorted ==
statsort_longs best of 5: 0.0033 s ( 33.44 ns/elem)
sort (kernel) best of 5: 0.0095 s ( 94.56 ns/elem)
qsort (libc) best of 5: 0.0020 s ( 20.41 ns/elem)
== n = 100000 distribution = reversed ==
statsort_longs best of 5: 0.0040 s ( 39.83 ns/elem)
sort (kernel) best of 5: 0.0094 s ( 93.55 ns/elem)
qsort (libc) best of 5: 0.0027 s ( 27.24 ns/elem)
== n = 100000 distribution = constant ==
statsort_longs best of 5: 0.0001 s ( 1.39 ns/elem)
sort (kernel) best of 5: 0.0084 s ( 84.11 ns/elem)
qsort (libc) best of 5: 0.0020 s ( 20.42 ns/elem)
== n = 100000000 distribution = uniform ==
statsort_longs best of 1: 9.0722 s ( 90.72 ns/elem)
sort (kernel) best of 1: 171.2293 s (1712.29 ns/elem)
qsort (libc) best of 1: 18.4959 s ( 184.96 ns/elem)
== n = 100000000 distribution = gaussian ==
statsort_longs best of 1: 8.6535 s ( 86.54 ns/elem)
sort (kernel) best of 1: 170.0339 s (1700.34 ns/elem)
qsort (libc) best of 1: 17.1339 s ( 171.34 ns/elem)
== n = 100000000 distribution = duplicates ==
statsort_longs best of 1: 23.0730 s ( 230.73 ns/elem)
sort (kernel) best of 1: 17.0882 s ( 170.88 ns/elem)
qsort (libc) best of 1: 9.6171 s ( 96.17 ns/elem)
== n = 100000000 distribution = small_range ==
statsort_longs best of 1: 19.4232 s ( 194.23 ns/elem)
sort (kernel) best of 1: 31.0023 s ( 310.02 ns/elem)
qsort (libc) best of 1: 11.5071 s ( 115.07 ns/elem)
== n = 100000000 distribution = sorted ==
statsort_longs best of 1: 3.9397 s ( 39.40 ns/elem)
sort (kernel) best of 1: 16.4800 s ( 164.80 ns/elem)
qsort (libc) best of 1: 3.7129 s ( 37.13 ns/elem)
== n = 100000000 distribution = reversed ==
statsort_longs best of 1: 5.9803 s ( 59.80 ns/elem)
sort (kernel) best of 1: 19.3678 s ( 193.68 ns/elem)
qsort (libc) best of 1: 5.7287 s ( 57.29 ns/elem)
== n = 100000000 distribution = constant ==
statsort_longs best of 1: 0.1562 s ( 1.56 ns/elem)
sort (kernel) best of 1: 22.3140 s ( 223.14 ns/elem)
qsort (libc) best of 1: 3.7718 s ( 37.72 ns/elem)
Signed-off-by: Peter Taraba <peter.schuster.taraba@gmail.com>
---
include/linux/statsort.h | 22 +++
lib/statsort.c | 326 +++++++++++++++++++++++++++++++++++++++
2 files changed, 348 insertions(+)
create mode 100644 include/linux/statsort.h
create mode 100644 lib/statsort.c
diff --git a/include/linux/statsort.h b/include/linux/statsort.h
new file mode 100644
index 000000000000..877f83cd2cfa
--- /dev/null
+++ b/include/linux/statsort.h
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _LINUX_STATSORT_H
+#define _LINUX_STATSORT_H
+
+#include <linux/types.h>
+
+/**
+ * typedef statsort_key_func_t - sort-key extraction callback
+ * @elem: pointer to the element to extract a key from
+ * @priv: opaque pointer, passed through from statsort()'s @priv
+ *
+ * Return: a signed 64-bit key such that, for any two elements a and
+ * b, (key(a) < key(b)) matches the order statsort() should produce.
+ */
+typedef s64 (*statsort_key_func_t)(const void *elem, const void *priv);
+
+void statsort(void *base, size_t num, size_t size,
+ statsort_key_func_t key_func, const void *priv);
+
+void statsort_longs(long *base, size_t num);
+
+#endif /* _LINUX_STATSORT_H */
diff --git a/lib/statsort.c b/lib/statsort.c
new file mode 100644
index 000000000000..6cabaefa4f6f
--- /dev/null
+++ b/lib/statsort.c
@@ -0,0 +1,326 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * A distribution (bucket) sort for the Linux kernel
+ *
+ * statsort() sorts an array of elements by a caller-supplied signed
+ * 64-bit key. At each level of recursion it distributes the elements
+ * of a range into m = isqrt(n) buckets by linearly interpolating each
+ * element's key over the range's [min, max) span, then recurses into
+ * every non-empty bucket with the range narrowed to that bucket. This
+ * is the same idea as radix/bucket sort: when keys are close to
+ * uniformly distributed, each level shrinks n by a factor of roughly
+ * sqrt(n), giving close to O(n) average behaviour overall.
+ *
+ * Two safety nets keep this from degrading to O(n^2) on adversarial
+ * or skewed input:
+ *
+ * - Ranges of STATSORT_THRESHOLD elements or fewer are finished off
+ * with a plain insertion sort, which is faster than recursing
+ * further for small n and bounds the recursion depth.
+ *
+ * - If every element in a range lands in the same bucket (the key
+ * distribution didn't actually discriminate between them),
+ * recursing again would repeat the same bucketing forever. That
+ * case, and any allocation failure, falls back to sort_r() (see
+ * lib/sort.c), which guarantees O(n log n) worst-case behaviour.
+ *
+ * All arithmetic is done with plain integer keys; the kernel has no
+ * business touching the FPU, so unlike a userspace bucket sort there
+ * is no floating-point interpolation here, only scaled 64-bit integer
+ * division.
+ */
+
+#include <linux/export.h>
+#include <linux/kernel.h>
+#include <linux/limits.h>
+#include <linux/math.h>
+#include <linux/math64.h>
+#include <linux/slab.h>
+#include <linux/sort.h>
+#include <linux/statsort.h>
+#include <linux/string.h>
+#include <linux/types.h>
+
+/* Ranges at or below this size are finished off with insertion sort. */
+#define STATSORT_THRESHOLD 16
+
+/*
+ * Bundles the key function and its private argument so they can be
+ * threaded through the sort_r()-based fallback path, which only has
+ * room for a single opaque priv pointer.
+ */
+struct statsort_priv {
+ statsort_key_func_t key;
+ const void *arg;
+};
+
+static int statsort_cmp(const void *a, const void *b, const void *priv)
+{
+ const struct statsort_priv *p = priv;
+ s64 ka = p->key(a, p->arg);
+ s64 kb = p->key(b, p->arg);
+
+ if (ka < kb)
+ return -1;
+ if (ka > kb)
+ return 1;
+ return 0;
+}
+
+/*
+ * Guaranteed O(n log n) fallback used for small ranges' degenerate
+ * siblings, allocation failure, and the single-bucket degenerate case.
+ */
+static void statsort_fallback(void *base, size_t num, size_t size,
+ statsort_key_func_t key_func, const void *priv)
+{
+ struct statsort_priv p = { .key = key_func, .arg = priv };
+
+ sort_r(base, num, size, statsort_cmp, NULL, &p);
+}
+
+/**
+ * statsort_insertion - insertion sort by key, for small ranges
+ * @base: pointer to the range to sort
+ * @num: number of elements in the range
+ * @size: size of each element
+ * @key_func: pointer to the key-extraction function
+ * @priv: opaque pointer passed through to @key_func
+ * @tmp: scratch buffer of at least @size bytes, for the sifted element
+ *
+ * O(n^2) worst case, but with very low constant factors, which makes
+ * it faster than recursing further once a range is small.
+ */
+static void statsort_insertion(void *base, size_t num, size_t size,
+ statsort_key_func_t key_func, const void *priv,
+ void *tmp)
+{
+ size_t i, j;
+
+ for (i = 1; i < num; i++) {
+ s64 key;
+
+ memcpy(tmp, base + i * size, size);
+ key = key_func(tmp, priv);
+
+ j = i;
+ while (j > 0 && key_func(base + (j - 1) * size, priv) > key) {
+ memcpy(base + j * size, base + (j - 1) * size, size);
+ j--;
+ }
+ memcpy(base + j * size, tmp, size);
+ }
+}
+
+/*
+ * Map a key into one of @m buckets spanning the half-open key range
+ * [@min, @min + @span). @span is precomputed by the caller as an
+ * unsigned 64-bit quantity to sidestep signed overflow.
+ */
+static size_t statsort_bucket(s64 key, s64 min, u64 span, size_t m)
+{
+ size_t b;
+
+ if (!span)
+ return 0;
+
+ b = div64_u64((u64)(key - min) * (u64)m, span);
+ if (b >= m)
+ b = m - 1;
+ return b;
+}
+
+/*
+ * Recursive core. @data holds the range to sort on entry; @scratch is
+ * a same-sized buffer that either the caller allocated (top level) or
+ * the previous level's own @data region (recursive levels), and ends
+ * up holding the sorted range, which is then copied back into @data.
+ */
+static void statsort_recurse(void *data, size_t num, size_t size,
+ s64 min, s64 max, void *scratch,
+ statsort_key_func_t key_func, const void *priv,
+ void *tmp)
+{
+ size_t m, i, b, nonempty;
+ size_t *cnt, *off, *pos;
+ u64 span;
+
+ if (num <= STATSORT_THRESHOLD) {
+ statsort_insertion(data, num, size, key_func, priv, tmp);
+ return;
+ }
+
+ m = int_sqrt(num);
+ if (!m)
+ m = 1;
+ span = (u64)(max - min);
+
+ cnt = kcalloc(m, sizeof(*cnt), GFP_KERNEL);
+ if (!cnt) {
+ statsort_fallback(data, num, size, key_func, priv);
+ return;
+ }
+
+ for (i = 0; i < num; i++) {
+ b = statsort_bucket(key_func(data + i * size, priv), min, span, m);
+ cnt[b]++;
+ }
+
+ off = kmalloc_array(m + 1, sizeof(*off), GFP_KERNEL);
+ if (!off) {
+ kfree(cnt);
+ statsort_fallback(data, num, size, key_func, priv);
+ return;
+ }
+ off[0] = 0;
+ for (i = 0; i < m; i++)
+ off[i + 1] = off[i] + cnt[i];
+
+ pos = kmalloc_array(m, sizeof(*pos), GFP_KERNEL);
+ if (!pos) {
+ kfree(off);
+ kfree(cnt);
+ statsort_fallback(data, num, size, key_func, priv);
+ return;
+ }
+ memcpy(pos, off, m * sizeof(*pos));
+
+ nonempty = 0;
+ for (i = 0; i < m; i++)
+ if (cnt[i])
+ nonempty++;
+
+ for (i = 0; i < num; i++) {
+ b = statsort_bucket(key_func(data + i * size, priv), min, span, m);
+ memcpy(scratch + pos[b] * size, data + i * size, size);
+ pos[b]++;
+ }
+ kfree(pos);
+
+ if (nonempty == 1) {
+ /*
+ * Every element hashed into the same bucket: the key
+ * range didn't discriminate between them, so recursing
+ * again would just repeat this bucketing forever.
+ */
+ memcpy(data, scratch, num * size);
+ statsort_fallback(data, num, size, key_func, priv);
+ kfree(off);
+ kfree(cnt);
+ return;
+ }
+
+ for (b = 0; b < m; b++) {
+ size_t bstart = off[b];
+ size_t bsize = cnt[b];
+ s64 bmin, bmax;
+
+ if (!bsize)
+ continue;
+
+ bmin = min + (s64)div_u64((u64)b * span, m);
+ bmax = min + (s64)div_u64((u64)(b + 1) * span, m);
+
+ if (bsize <= STATSORT_THRESHOLD)
+ statsort_insertion(scratch + bstart * size, bsize, size,
+ key_func, priv, tmp);
+ else
+ statsort_recurse(scratch + bstart * size, bsize, size,
+ bmin, bmax, data + bstart * size,
+ key_func, priv, tmp);
+ }
+
+ kfree(off);
+ kfree(cnt);
+ memcpy(data, scratch, num * size);
+}
+
+/**
+ * statsort - sort an array of elements by a signed 64-bit key
+ * @base: pointer to data to sort
+ * @num: number of elements
+ * @size: size of each element
+ * @key_func: pointer to a function returning each element's sort key
+ * @priv: opaque pointer passed through to @key_func
+ *
+ * Sorts @num elements of @size bytes each, in ascending order of the
+ * key returned by key_func(elem, priv). Performs best when keys are
+ * close to uniformly distributed over their range, and falls back to
+ * sort_r()'s guaranteed O(n log n) heapsort for skewed distributions
+ * or on allocation failure, so worst-case behaviour is never worse
+ * than a plain sort_r() call plus O(n) bucketing overhead.
+ *
+ * Like sort_r(), this is not a stable sort: elements that compare
+ * equal may be reordered.
+ */
+void statsort(void *base, size_t num, size_t size,
+ statsort_key_func_t key_func, const void *priv)
+{
+ void *scratch, *tmp;
+ s64 min, max;
+ size_t i;
+
+ if (num < 2 || !size)
+ return;
+
+ min = max = key_func(base, priv);
+ for (i = 1; i < num; i++) {
+ s64 key = key_func(base + i * size, priv);
+
+ if (key < min)
+ min = key;
+ if (key > max)
+ max = key;
+ }
+
+ if (min == max)
+ return; /* all keys equal: already sorted */
+
+ /*
+ * Bucketing treats the range as the half-open interval
+ * [min, max), so widen max by one to keep the true maximum
+ * key out of the (nonexistent) bucket m. Leave it alone in
+ * the vanishingly unlikely case that max is already S64_MAX.
+ */
+ if (max < S64_MAX)
+ max += 1;
+
+ scratch = kmalloc_array(num, size, GFP_KERNEL);
+ if (!scratch) {
+ statsort_fallback(base, num, size, key_func, priv);
+ return;
+ }
+
+ tmp = kmalloc(size, GFP_KERNEL);
+ if (!tmp) {
+ kfree(scratch);
+ statsort_fallback(base, num, size, key_func, priv);
+ return;
+ }
+
+ statsort_recurse(base, num, size, min, max, scratch, key_func, priv, tmp);
+
+ kfree(tmp);
+ kfree(scratch);
+}
+EXPORT_SYMBOL(statsort);
+
+static s64 statsort_identity_key(const void *elem, const void *priv)
+{
+ return *(const long *)elem;
+}
+
+/**
+ * statsort_longs - sort an array of longs
+ * @base: pointer to the array
+ * @num: number of elements
+ *
+ * Convenience wrapper around statsort() for plain "long" arrays,
+ * mirroring the relationship between sort() and sort_r() in
+ * lib/sort.c.
+ */
+void statsort_longs(long *base, size_t num)
+{
+ statsort(base, num, sizeof(*base), statsort_identity_key, NULL);
+}
+EXPORT_SYMBOL(statsort_longs);
--
2.55.0
next reply other threads:[~2026-08-04 23:35 UTC|newest]
Thread overview: 14+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-04 23:34 Peter Taraba [this message]
2026-08-04 23:58 ` [PATCH] lib: add statsort(), a distribution/bucket sort Andrew Morton
2026-08-05 0:22 ` Peter Taraba
[not found] <CAMKTnsb-PZ7_4Zd1ZAtmSWnTk8vyoP07WZUDsLuZ21j7OawQ9g@mail.gmail.com>
2026-08-05 0:35 ` Andrew Morton
2026-08-05 0:47 ` Peter Taraba
2026-08-05 14:29 ` Jonathan Corbet
2026-08-05 15:47 ` Peter Taraba
2026-08-05 16:39 ` Jonathan Corbet
2026-08-05 18:44 ` Peter Taraba
2026-08-10 10:22 ` Kuan-Wei Chiu
2026-08-10 13:24 ` Peter Taraba
2026-08-10 15:09 ` Bradley Morgan
2026-08-10 15:59 ` Peter Taraba
2026-08-10 16:07 ` Bradley Morgan
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260804233449.70815-1-peter.schuster.taraba@gmail.com \
--to=peter.schuster.taraba@gmail.com \
--cc=akpm@linux-foundation.org \
--cc=linux-kernel@vger.kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.