<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shreyas</title>
    <description>The latest articles on DEV Community by Shreyas (@dev-shreyas).</description>
    <link>https://dev.to/dev-shreyas</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3980948%2F70a0cc57-95d7-4dd6-b667-220c54fa9f60.png</url>
      <title>DEV Community: Shreyas</title>
      <link>https://dev.to/dev-shreyas</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dev-shreyas"/>
    <language>en</language>
    <item>
      <title>I Built a Stable Sorting Algorithm That Beats Java's Dual-Pivot Quicksort and TimSort</title>
      <dc:creator>Shreyas</dc:creator>
      <pubDate>Fri, 12 Jun 2026 09:38:33 +0000</pubDate>
      <link>https://dev.to/dev-shreyas/i-built-a-stable-sorting-algorithm-that-beats-javas-dual-pivot-quicksort-fck</link>
      <guid>https://dev.to/dev-shreyas/i-built-a-stable-sorting-algorithm-that-beats-javas-dual-pivot-quicksort-fck</guid>
      <description>&lt;p&gt;&lt;em&gt;Update: This post has been substantially revised from the original. Since first publishing, I re-tuned the algorithm's parameters through a proper JMH sweep, re-ran every benchmark on a single consistent machine, added an adversarial-input stress test, measured actual peak memory footprint (not just allocation rate), and added a full worked example below so you can trace exactly what the algorithm does by hand. The core idea is unchanged — the numbers and rigor behind them are not.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A while back I finished benchmarking something I've been building — a cache-aware, stable, histogram-based sorting algorithm I'm calling &lt;strong&gt;BusSort&lt;/strong&gt;. The results still surprise me.&lt;/p&gt;

&lt;p&gt;At 100 million elements, it runs &lt;strong&gt;up to 2.75x faster than Java's Dual-Pivot Quicksort&lt;/strong&gt; on distribution-heavy &lt;code&gt;int[]&lt;/code&gt; data — while being &lt;strong&gt;stable&lt;/strong&gt;, which DPQ is not. The generic variant beats &lt;strong&gt;TimSort by up to 2.87x&lt;/strong&gt; on random &lt;code&gt;T[]&lt;/code&gt; data.&lt;/p&gt;

&lt;p&gt;It also loses in a few honest, specific ways — on reverse-sorted input, and (for the generic variant) on nearly-sorted input. I'll show those numbers too, not just the wins.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem With Quicksort at Scale
&lt;/h2&gt;

&lt;p&gt;Quicksort-based algorithms partition elements with random writes across the entire array. At large scales this causes &lt;strong&gt;cache thrashing&lt;/strong&gt; — elements get written to memory locations all over the place, constantly missing L1 and L2 cache.&lt;/p&gt;

&lt;p&gt;The larger the array, the worse it gets.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Idea
&lt;/h2&gt;

&lt;p&gt;Instead of scattering elements globally, BusSort processes data in &lt;strong&gt;L1 cache-sized chunks&lt;/strong&gt; — 4096 integers (~16KB) — using a histogram to know exactly where every element belongs before moving it.&lt;/p&gt;

&lt;p&gt;For each chunk, it does 4 passes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PASS 1&lt;/strong&gt; — Scan left-to-right, compute the bucket for each element, build a &lt;em&gt;local&lt;/em&gt; histogram for this chunk only&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PASS 2&lt;/strong&gt; — Compute local prefix sums (each bucket's position &lt;em&gt;within the chunk&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PASS 3&lt;/strong&gt; — Scatter into a local &lt;code&gt;grouped&lt;/code&gt; buffer — the "bus." Because this buffer is L1-sized, all random writes stay &lt;strong&gt;in cache&lt;/strong&gt; ✅&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PASS 4&lt;/strong&gt; — Copy each bucket's portion from the local buffer to its correct global position&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After one pass, each bucket is recursed into using the same four passes, until a bucket is small enough to finish with &lt;strong&gt;insertion sort&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Walking Through It By Hand
&lt;/h2&gt;

&lt;p&gt;Numbers alone don't build intuition, so here's the entire algorithm traced on a small array — every variable, every pass. To keep this on one page, I'm using small illustrative constants (&lt;code&gt;BUCKETS=4&lt;/code&gt;, &lt;code&gt;BUS_SIZE=8&lt;/code&gt;, &lt;code&gt;THRESHOLD=5&lt;/code&gt;) instead of the real tuned values (&lt;code&gt;256&lt;/code&gt;/&lt;code&gt;4096&lt;/code&gt;/&lt;code&gt;64&lt;/code&gt;) — the logic executed is identical either way, just at a size you can follow by hand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input (20 elements):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;idx:   0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19
val:  15  3  9  1 18  7 12  4 19  0 11  6 16  2  8 14  5 17 10 13
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Level 1: the whole array
&lt;/h3&gt;

&lt;p&gt;First, find &lt;code&gt;min=0&lt;/code&gt;, &lt;code&gt;max=19&lt;/code&gt;. Since &lt;code&gt;min ≠ max&lt;/code&gt;, we distribute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bucket range:&lt;/strong&gt; &lt;code&gt;rangeL = (19-0+1+4-1)/4 = 22/4 = 5&lt;/code&gt; — so bucket 0 covers values &lt;code&gt;[0,4]&lt;/code&gt;, bucket 1 covers &lt;code&gt;[5,9]&lt;/code&gt;, bucket 2 covers &lt;code&gt;[10,14]&lt;/code&gt;, bucket 3 covers &lt;code&gt;[15,19]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Global histogram&lt;/strong&gt; (one pass over all 20 elements, computing &lt;code&gt;value/5&lt;/code&gt; for each, capped at bucket 3):&lt;/p&gt;

&lt;p&gt;&lt;code&gt;globalCount = [5, 5, 5, 5]&lt;/code&gt; — exactly 5 elements per bucket, by construction of this example.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefix sums:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;globalNext   = [0, 5, 10, 15]   (mutable — advances as chunks get written)
bucketStarts = [0, 5, 10, 15]   (frozen — used later to define each bucket's boundaries)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the chunk loop. With &lt;code&gt;BUS_SIZE=8&lt;/code&gt;, 20 elements split into three chunks: &lt;code&gt;[0-7]&lt;/code&gt;, &lt;code&gt;[8-15]&lt;/code&gt;, &lt;code&gt;[16-19]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chunk 1 — indices 0-7:&lt;/strong&gt; values &lt;code&gt;15,3,9,1,18,7,12,4&lt;/code&gt; → buckets &lt;code&gt;3,0,1,0,3,1,2,0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Pass 1&lt;/em&gt; builds &lt;code&gt;localCount = [3,2,1,2]&lt;/code&gt; (3 elements landed in bucket 0, 2 in bucket 1, 1 in bucket 2, 2 in bucket 3).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Pass 2&lt;/em&gt; prefix-sums that into &lt;code&gt;localStart = [0,3,5,6]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Pass 3&lt;/em&gt; scatters into the local &lt;code&gt;grouped&lt;/code&gt; buffer, respecting each element's original left-to-right position within its bucket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;grouped = [3, 1, 4, 9, 7, 12, 15, 18]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look closely: bucket 0's slice is &lt;code&gt;[3,1,4]&lt;/code&gt; — in the &lt;em&gt;original array&lt;/em&gt;, these appeared at indices 1, 3, 7, in that order. They land in &lt;code&gt;grouped&lt;/code&gt; in that same order. That's the entire stability mechanism, visible in one line.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Pass 4&lt;/em&gt; copies each bucket's slice from &lt;code&gt;grouped&lt;/code&gt; into the real output array at &lt;code&gt;globalNext[b]&lt;/code&gt;, then advances &lt;code&gt;globalNext[b]&lt;/code&gt; by however many elements just got written:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;bucket&lt;/th&gt;
&lt;th&gt;count&lt;/th&gt;
&lt;th&gt;written to output at&lt;/th&gt;
&lt;th&gt;globalNext becomes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;positions 0-2&lt;/td&gt;
&lt;td&gt;0+3 = 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;positions 5-6&lt;/td&gt;
&lt;td&gt;5+2 = 7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;position 10&lt;/td&gt;
&lt;td&gt;10+1 = 11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;positions 15-16&lt;/td&gt;
&lt;td&gt;15+2 = 17&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Chunk 2 — indices 8-15:&lt;/strong&gt; values &lt;code&gt;19,0,11,6,16,2,8,14&lt;/code&gt; → buckets &lt;code&gt;3,0,2,1,3,0,1,2&lt;/code&gt;. Same four passes, &lt;code&gt;localCount=[2,2,2,2]&lt;/code&gt;. After this chunk, &lt;code&gt;globalNext = [5, 9, 13, 19]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chunk 3 — indices 16-19:&lt;/strong&gt; values &lt;code&gt;5,17,10,13&lt;/code&gt; → buckets &lt;code&gt;1,3,2,2&lt;/code&gt;. &lt;code&gt;localCount=[0,1,2,1]&lt;/code&gt; — bucket 0 gets nothing from this chunk, so its copy step is simply skipped. After this chunk, &lt;code&gt;globalNext = [5, 10, 15, 20]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output after level 1:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;idx:  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19
val:  3  1  4  0  2  9  7  6  8  5 12 11 14 10 13 15 18 19 16 17
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every position filled exactly once — the three chunks' contributions to each bucket landed in separate, non-overlapping slices, in original left-to-right order.&lt;/p&gt;

&lt;h3&gt;
  
  
  Level 2: recurse into each bucket
&lt;/h3&gt;

&lt;p&gt;Using the &lt;em&gt;frozen&lt;/em&gt; &lt;code&gt;bucketStarts = [0,5,10,15]&lt;/code&gt; (not the now-moved-on &lt;code&gt;globalNext&lt;/code&gt;), the four buckets are &lt;code&gt;[0-4]&lt;/code&gt;, &lt;code&gt;[5-9]&lt;/code&gt;, &lt;code&gt;[10-14]&lt;/code&gt;, &lt;code&gt;[15-19]&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bucket 0: &lt;code&gt;[3,1,4,0,2]&lt;/code&gt; — 5 elements, &lt;code&gt;n ≤ THRESHOLD(5)&lt;/code&gt; → straight to insertion sort → &lt;code&gt;[0,1,2,3,4]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Bucket 1: &lt;code&gt;[9,7,6,8,5]&lt;/code&gt; → insertion sort → &lt;code&gt;[5,6,7,8,9]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Bucket 2: &lt;code&gt;[12,11,14,10,13]&lt;/code&gt; → insertion sort → &lt;code&gt;[10,11,12,13,14]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Bucket 3: &lt;code&gt;[15,18,19,16,17]&lt;/code&gt; → insertion sort → &lt;code&gt;[15,16,17,18,19]&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Final result:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;idx:  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19
val:  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sorted. At production scale (&lt;code&gt;BUCKETS=256&lt;/code&gt;, &lt;code&gt;BUS_SIZE=4096&lt;/code&gt;, &lt;code&gt;THRESHOLD=64&lt;/code&gt;), the mechanism is identical — just more buckets, bigger chunks, more insertion-sort headroom.&lt;/p&gt;




&lt;h2&gt;
  
  
  Benchmark Results
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Machine:&lt;/strong&gt; Intel Core i5-1035G1 @ 1.00GHz (4 cores / 8 threads), 8GB RAM | &lt;strong&gt;Java:&lt;/strong&gt; 21&lt;br&gt;
&lt;strong&gt;Methodology:&lt;/strong&gt; JMH 1.37 — 5 warmup iterations, 10 measurement iterations, 3 JVM forks, &lt;code&gt;-prof gc&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;int[]&lt;/code&gt; vs Dual-Pivot Quicksort (&lt;code&gt;n = 100,000,000&lt;/code&gt;, tuned: BUCKETS=256, BUS_SIZE=4096, THRESHOLD=64)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input Type&lt;/th&gt;
&lt;th&gt;BusSort (ms)&lt;/th&gt;
&lt;th&gt;DPQ (ms)&lt;/th&gt;
&lt;th&gt;Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Random&lt;/td&gt;
&lt;td&gt;3981&lt;/td&gt;
&lt;td&gt;10606&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;2.66x&lt;/strong&gt; ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicates&lt;/td&gt;
&lt;td&gt;954&lt;/td&gt;
&lt;td&gt;2622&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;2.75x&lt;/strong&gt; ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Few Duplicates&lt;/td&gt;
&lt;td&gt;2108&lt;/td&gt;
&lt;td&gt;4113&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1.95x&lt;/strong&gt; ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clustered&lt;/td&gt;
&lt;td&gt;1897&lt;/td&gt;
&lt;td&gt;2707&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1.43x&lt;/strong&gt; ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nearly Sorted&lt;/td&gt;
&lt;td&gt;3306&lt;/td&gt;
&lt;td&gt;3783&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1.14x&lt;/strong&gt; ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adversarial*&lt;/td&gt;
&lt;td&gt;3725&lt;/td&gt;
&lt;td&gt;3908&lt;/td&gt;
&lt;td&gt;1.05x — near tie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sorted&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;td&gt;0.92x — trivial cost either way&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All Same&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;td&gt;0.89x — trivial cost either way&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reverse&lt;/td&gt;
&lt;td&gt;333&lt;/td&gt;
&lt;td&gt;111&lt;/td&gt;
&lt;td&gt;0.33x ❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;T[]&lt;/code&gt; vs TimSort (&lt;code&gt;n = 10,000,000&lt;/code&gt;, tuned: BUCKETS=80, BUS_SIZE=4096, THRESHOLD=64)
&lt;/h3&gt;

&lt;p&gt;Sorting &lt;code&gt;Record&lt;/code&gt; objects — an int key plus a field, not boxed &lt;code&gt;Integer&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input Type&lt;/th&gt;
&lt;th&gt;BusSort (ms)&lt;/th&gt;
&lt;th&gt;TimSort (ms)&lt;/th&gt;
&lt;th&gt;Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Random&lt;/td&gt;
&lt;td&gt;1864&lt;/td&gt;
&lt;td&gt;5346&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;2.87x&lt;/strong&gt; ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Few Duplicates&lt;/td&gt;
&lt;td&gt;1015&lt;/td&gt;
&lt;td&gt;2135&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;2.10x&lt;/strong&gt; ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clustered&lt;/td&gt;
&lt;td&gt;882&lt;/td&gt;
&lt;td&gt;1478&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1.68x&lt;/strong&gt; ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicates&lt;/td&gt;
&lt;td&gt;998&lt;/td&gt;
&lt;td&gt;1637&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1.64x&lt;/strong&gt; ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adversarial*&lt;/td&gt;
&lt;td&gt;1723&lt;/td&gt;
&lt;td&gt;2093&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1.21x&lt;/strong&gt; ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All Same&lt;/td&gt;
&lt;td&gt;19.8&lt;/td&gt;
&lt;td&gt;22.5&lt;/td&gt;
&lt;td&gt;1.14x — trivial cost either way&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sorted&lt;/td&gt;
&lt;td&gt;22.5&lt;/td&gt;
&lt;td&gt;21.7&lt;/td&gt;
&lt;td&gt;0.96x — trivial cost either way&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reverse&lt;/td&gt;
&lt;td&gt;185&lt;/td&gt;
&lt;td&gt;104&lt;/td&gt;
&lt;td&gt;0.56x ❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nearly Sorted&lt;/td&gt;
&lt;td&gt;673&lt;/td&gt;
&lt;td&gt;358&lt;/td&gt;
&lt;td&gt;0.53x ❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;* &lt;strong&gt;Adversarial:&lt;/strong&gt; 99% of elements packed into a 1000-wide key range, 1% spread across the full &lt;code&gt;int&lt;/code&gt; range — deliberately stresses histogram skew. More on this below.&lt;/p&gt;

&lt;p&gt;Full benchmark code, including this adversarial case, is in the &lt;a href="https://github.com/dev-shreyaspatil/BusSort/tree/main/benchmarks" rel="noopener noreferrer"&gt;&lt;code&gt;benchmarks/&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/dev-shreyaspatil/BusSort/tree/main/generics/benchmarks" rel="noopener noreferrer"&gt;&lt;code&gt;generics/benchmarks/&lt;/code&gt;&lt;/a&gt; folders.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Adversarial Case
&lt;/h2&gt;

&lt;p&gt;Bucket-based sorts don't get a free logarithmic recursion-depth guarantee the way balanced comparison sorts do — the split is data-dependent, not count-dependent. If a value distribution is skewed enough that most elements land in one bucket at every recursion level, depth can approach &lt;code&gt;O(n)&lt;/code&gt; instead of &lt;code&gt;O(log n)&lt;/code&gt;, and the whole algorithm's total work approaches &lt;code&gt;O(n²)&lt;/code&gt; — the same theoretical worst case Quicksort has always had.&lt;/p&gt;

&lt;p&gt;I built a test specifically to stress this (99% of values crammed into a narrow range) rather than wait for someone else to find it. The result: BusSort's advantage &lt;strong&gt;collapses to roughly a tie&lt;/strong&gt; for &lt;code&gt;int[]&lt;/code&gt; (1.05x) under this skew, though it still holds a real edge for &lt;code&gt;T[]&lt;/code&gt; (1.21x). It doesn't get &lt;em&gt;worse&lt;/em&gt; than the JDK's own sort at the scales I tested — but the margin that makes it worth using elsewhere mostly disappears.&lt;/p&gt;

&lt;p&gt;I deliberately haven't added a fallback mechanism into the algorithm itself — I wanted the core logic to stay simple and auditable, even at the cost of this one known weak spot. A natural fix would be a lightweight health-check after the histogram pass: if one bucket holds a disproportionate share of elements, hand that sub-range to &lt;code&gt;Arrays.sort()&lt;/code&gt; instead of recursing further. I'd genuinely like input on whether that's worth building in, and what threshold would make sense.&lt;/p&gt;




&lt;h2&gt;
  
  
  Peak Memory — Not Just Allocation Rate
&lt;/h2&gt;

&lt;p&gt;JMH's &lt;code&gt;-prof gc&lt;/code&gt; reports &lt;em&gt;cumulative&lt;/em&gt; allocation (bytes/op), which isn't the same as &lt;em&gt;peak live memory&lt;/em&gt; at any instant — a more relevant number for a memory-conscious comparison. I measured this separately using &lt;code&gt;MemoryPoolMXBean&lt;/code&gt; peak-usage tracking around a single sort call.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;BusSort (&lt;code&gt;int[]&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;DPQ&lt;/th&gt;
&lt;th&gt;BusSortGenerics&lt;/th&gt;
&lt;th&gt;TimSort&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1,000,000&lt;/td&gt;
&lt;td&gt;13.1 MB&lt;/td&gt;
&lt;td&gt;13.1 MB&lt;/td&gt;
&lt;td&gt;36.4 MB&lt;/td&gt;
&lt;td&gt;41.0 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10,000,000&lt;/td&gt;
&lt;td&gt;121.1 MB&lt;/td&gt;
&lt;td&gt;81.1 MB&lt;/td&gt;
&lt;td&gt;350.1 MB&lt;/td&gt;
&lt;td&gt;370.2 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100,000,000&lt;/td&gt;
&lt;td&gt;1153.1 MB&lt;/td&gt;
&lt;td&gt;769.1 MB&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two different stories here, worth stating plainly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;int[]&lt;/code&gt; vs DPQ:&lt;/strong&gt; at scale, BusSort uses &lt;strong&gt;~50% more peak memory&lt;/strong&gt; than DPQ. Expected — DPQ sorts in-place, BusSort allocates a full auxiliary buffer every level. This is a genuine tradeoff: &lt;strong&gt;faster, but not free.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;T[]&lt;/code&gt; vs TimSort:&lt;/strong&gt; BusSortGenerics uses &lt;strong&gt;less&lt;/strong&gt; peak memory than TimSort at every scale I measured. TimSort isn't in-place either — its merge machinery needs its own auxiliary array — so for the generic variant, BusSort wins on speed &lt;em&gt;and&lt;/em&gt; memory.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why It's Stable
&lt;/h2&gt;

&lt;p&gt;Chunks are processed left-to-right, and within each chunk, elements are scattered left-to-right — &lt;code&gt;globalNext[b]&lt;/code&gt; only ever advances, never resets, so earlier chunks always land before later chunks in the output. No index tagging, no extra bookkeeping. You can see this directly in the worked example above: bucket 0's elements landed in &lt;code&gt;grouped&lt;/code&gt; in exactly the order they appeared in the original array.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;int[]&lt;/code&gt;, equal integers are identical by value, so stability is technically unobservable — but the mechanism is identical for the generic variant, where it matters for real objects with equal keys.&lt;/p&gt;




&lt;h2&gt;
  
  
  Known Trade-offs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reverse-sorted input is a real weak spot&lt;/strong&gt; (0.33x for &lt;code&gt;int[]&lt;/code&gt;, 0.56x for &lt;code&gt;T[]&lt;/code&gt;) — the reverse-detection path preserves stability for equal-key runs, which costs more than a simple swap-based reversal would. Known, understood, not fixed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nearly-sorted input, generic variant only&lt;/strong&gt; (0.53x) — TimSort's run-detection is specifically built to exploit existing order and wins outright here. BusSort's histogram approach doesn't detect or exploit "already mostly sorted" at all, by design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No pure &lt;code&gt;Comparator&amp;lt;T&amp;gt;&lt;/code&gt; support&lt;/strong&gt; — BusSortGenerics requires a &lt;code&gt;ToIntFunction&amp;lt;T&amp;gt;&lt;/code&gt; numeric key extractor. Arithmetic bucketing needs a numeric key; comparison-based sorting doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not in-place&lt;/strong&gt; — see peak memory above.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[x] Sequential parameter tuning via JMH (bucket count, chunk size, insertion-sort threshold)&lt;/li&gt;
&lt;li&gt;[x] Adversarial-input stress testing&lt;/li&gt;
&lt;li&gt;[x] GC pressure and peak-memory measurement&lt;/li&gt;
&lt;li&gt;[ ] Health-check fallback for adversarial distribution collapse (documented above, not yet implemented — genuinely open, I'd like feedback on this one)&lt;/li&gt;
&lt;li&gt;[ ] Dynamic stack sizing&lt;/li&gt;
&lt;li&gt;[ ] Auto-tune &lt;code&gt;BUS_SIZE&lt;/code&gt; based on runtime L1 cache size&lt;/li&gt;
&lt;li&gt;[ ] Parallel / multi-threaded variant&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  GitHub
&lt;/h2&gt;

&lt;p&gt;Full implementation, benchmarks (including the adversarial and peak-memory ones above), and the generic variant are all published.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://github.com/dev-shreyaspatil/BusSort" rel="noopener noreferrer"&gt;github.com/dev-shreyaspatil/BusSort&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;I'm sharing this as independent work, hoping for feedback and scrutiny from people who know sorting and the JVM better than I do. Curious what the Java community thinks — especially the adversarial-case fallback question above, and whether there's published cache-aware bucket/radix sort literature I should be comparing against.&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>java</category>
      <category>performance</category>
      <category>sorting</category>
    </item>
  </channel>
</rss>
