English | 한국어
🤖 AI Collaboration
Let me be 100% transparent about what AI did—and what I did:
- Heavy AI Assistance for Code & Low-Level Tuning:
I am an undergraduate student still learning algorithms and computer architecture. Because I am not yet skilled enough to write low-level C++ optimizations on my own, I heavily relied on Generative AI (ChatGPT and Gemini) to write the C++20 code, apply low-level hardware tricks (e.g., branchlesscmov, cache-line alignment, L1 tiling), and build the benchmarking/profiling suite.- Independent Hypotheses:
I came up with the initial hypotheses myself while studying sorting algorithms, rather than taking them from an existing paper or sorting implementation.
PLEASE roast my code. Seriously. I'm begging you. 😭🙏
I'm just a university student trying to figure things out, and I still have a lot to learn (and probably a lot of terrible code to fix).
If you find anything wrong, inefficient, ugly, confusing, or just plain stupid in my code, PLEASE let me know.
I don't care if you're a beginner or a veteran. Any feedback is welcome. Literally ANYTHING.
- Found a bug? Please tell me.
- Know a better approach? PLEASE tell me.
- Think my code is a mess? You're probably right. Tell me why. 💀
Open an Issue, start a Discussion, or submit a Pull Request. Whatever works for you!
Every bit of criticism helps me grow as a developer. Please don't hold back. ❤️
While studying sorting algorithms, a fundamental question crossed my mind: "Quick Sort has a worst-case time complexity of $O(N^2)$ and shares the same $O(N \log N)$ average complexity as Heap Sort or Merge Sort. Why can it be faster in practice?"
Quicksort's practical performance depends on memory access patterns, comparison and movement costs, branch behavior, and implementation details. This led me to investigate small-block sorting and cache-aware merging.
- Splitting Down to the 64-Byte CPU Cache Line: On the target CPU, a 64-byte cache line holds 16 int32_t elements. I wanted to test whether sorting micro-blocks of this size before merging could reduce overhead.
- Improving Predictability in Partitioning & Merging: Could partition statistics improve pivot choices, and could opposite-direction boundary searches reduce unnecessary merge work? I also wanted to measure how these changes affected branch behavior.
This repository documents the journey of turning these raw ideas into working C++20 code and benchmarking them against std::sort and std::stable_sort.
Compared against
std::sorton the same machine and workload ($N = 1,000,000$ ,int32_t).
Note: Values inA / BdenoteOurs / std::sort. ↓ = lower is better (-n%) · ↑ = higher is better (+n%,+n%p).
| Scenario | Runtime ↓ | IPC ↑ | L1D Read Hit Rate ↑ | L1D Read Misses ↓ | Branch Hit Rate ↑ |
|---|---|---|---|---|---|
0_Random_3D_Distances |
21.32 ms ( -60.2%) |
2.19 / 0.63 ( +248%) |
95.11% ( -2.72%p) |
2.02M / 662K ( +205%) |
95.02% ( +19.48%p) |
1_Frame_Jitter |
13.43 ms ( -53.7%) |
2.01 / 0.85 ( +136%) |
99.47% ( +1.43%p) |
128K / 497K ( -74.2%) |
93.78% ( +10.99%p) |
2_Append_5Pct_Tail |
2.99 ms ( -84.2%) |
2.37 / 1.66 ( +42.8%) |
95.48% ( -2.42%p) |
289K / 655K ( -55.9%) |
98.22% ( +4.26%p) |
5_Gaussian_Like |
21.35 ms ( -52.5%) |
2.19 / 0.62 ( +253%) |
95.08% ( -2.77%p) |
2.03M / 588K ( +246%) |
95.11% ( +19.22%p) |
6_Organ_Pipe |
8.86 ms ( -43.5%) |
3.00 / 1.57 ( +91.1%) |
95.20% ( -1.01%p) |
1.15M / 2.75M ( -58.3%) |
99.71% ( +6.73%p) |
| Scenario | Runtime ↓ | IPC ↑ | L1D Read Hit Rate ↑ | L1D Read Misses ↓ | Branch Hit Rate ↑ |
|---|---|---|---|---|---|
0_Random_3D_Distances |
20.40 ms ( -61.9%) |
2.21 / 0.63 ( +251%) |
96.93% ( -0.90%p) |
1.26M / 662K ( +89.7%) |
94.89% ( +19.35%p) |
1_Frame_Jitter |
15.30 ms ( -47.2%) |
1.83 / 0.85 ( +115%) |
94.66% ( -3.38%p) |
1.26M / 497K ( +154%) |
93.12% ( +10.33%p) |
2_Append_5Pct_Tail |
4.19 ms ( -77.8%) |
2.37 / 1.66 ( +42.8%) |
86.24% ( -11.66%p) |
1.31M / 655K ( +101%) |
98.57% ( +4.61%p) |
5_Gaussian_Like |
20.37 ms ( -54.7%) |
2.21 / 0.62 ( +256%) |
96.93% ( -0.92%p) |
1.26M / 588K ( +114%) |
94.88% ( +18.99%p) |
6_Organ_Pipe |
8.49 ms ( -45.8%) |
2.70 / 1.57 ( +72.0%) |
94.28% ( -1.93%p) |
1.26M / 2.75M ( -54.3%) |
99.50% ( +6.52%p) |
Lower runtime did not consistently coincide with higher L1D read hit rates or fewer L1D read misses. These measurements alone do not establish improved cache behavior as the cause of the speedup.
0_Random_3D_Distances: Squared-distance keys computed from uniformly sampled 3D coordinates. The resulting keys are not uniformly distributed.1_Frame_Jitter: Pre-sorted array with small local random noise applied to every element (nearly sorted).2_Append_5Pct_Tail: A 95% sorted prefix followed by 5% unsorted new distance keys.3_Distance_Buckets: Squared-distance keys quantized into ~300 discrete buckets (key /= 1,000,000), creating heavy key duplication.4_Mostly_Descending: Reverse-sorted array (descending order) perturbed by 1% random adjacent swaps.5_Gaussian_Like: A bell-shaped distribution generated by summing four uniform integer samples.6_Organ_Pipe: Symmetric mountain-shaped array (ascending first half, descending second half).
This project grew out of a series of "what if" experiments (Idea 1 Idea 4). While the core algorithmic concepts came from my own hypotheses, I actively leaned on AI to translate those ideas into low-level C++ optimizations (such as branchless cmov loops, galloping boundary search, and L1 cache tiling) that I wouldn't have known how to write on my own as an undergraduate.
- My Idea: I tested switching from Quicksort partitioning to insertion sort for partitions of at most 16 int32_t elements. This cutoff aims to reduce partitioning overhead; it does not guarantee cache-line alignment or L1 residency.
- Outcome: It was slightly faster than basic Quick Sort, but since the overall structure was still top-down Quick Sort, the improvement was pretty modest.
- My Idea: The current implementation first performs Hoare partitioning, then scans the left and right partitions again to compute their sums and minimum/maximum keys. These statistics help choose subsequent pivots and identify all-equal ranges. This introduces an additional scan rather than accumulating statistics during partitioning.
- Outcome: It worked nicely on symmetric distributions and skipped all-duplicate ranges (
min == max) quickly, but maintaining running sums added overhead and still couldn't solve Quick Sort's heavy branch mispredictions on random data.
-
My Idea: Seeing the limits of top-down Quick Sort, I flipped the approach bottom-up—starting from Merge Sort instead:
- Instead of recursively splitting the array down to 1 element like standard Merge Sort, slice the array directly into 64-byte (16-element) blocks and sort each block with Insertion Sort.
- When merging two adjacent sorted blocks, instead of blindly merging the whole range from start to end, start two pointers at the meeting boundary (
mid) and walk them outward in opposite directions (left$\leftarrow$ mid$\rightarrow$ right) to trim away outer elements that are already in order, merging only the overlapping middle section.
- AI-Assisted Low-Level Optimization: AI helped implement bidirectional merge loops using conditional selection, exponential boundary searches, and half-buffer merging. These techniques aim to reduce data-dependent branching and memory copying. The generated instructions depend on the compiler and target, and loop and boundary branches remain.
- Outcome: Among the ideas I tried, this was the one that finally showed a genuinely meaningful jump in the benchmarks, especially on partially ordered or jittered data.
- Thought & AI Collaboration: Both current merge variants use 8 KiB tiles. This variant additionally alternates source and destination buffers between merge passes to reduce copy-back operations. Ordered ranges still need to be copied to the destination buffer when required by the next pass.
- Outcome: On the random 3D-distance workload, this variant recorded slightly lower runtime and fewer L1D read misses than boundary_trimmed_merge.
Looking back at the progression from Idea 1 Idea 4, Idea 3 (boundary_trimmed_merge)—along with Idea 4 as its random-data variant—turned out to be the most meaningful outcome among my attempts.
Of course, much smarter people out there have already designed far better and faster sorting algorithms than this. Still, seeing this rough idea actually hold its own against std::sort in the benchmarks made me think: "At least Idea 3 seems decent enough—maybe it could actually be somewhat useful in the right situation?"
Note
🚧 This README is a work in progress and will be updated regularly.
This project is licensed under the MIT License.