@epic-js/epic-sort
v1.0.15
Published
Blazing fast native sorting for Node.js - up to 20x faster than Array.sort()
Downloads
38
Maintainers
Readme
⚡ epic-sort
Native C++ sorting for Node.js — up to 50x faster than Array.sort() once your arrays get large.
Install • Quick Start • Benchmarks • Best fit • API • FAQ
Why epic-sort?
epic-sort is built for the moment sorting shows up in a CPU profile: recurring sorts over tens of thousands to millions of numeric elements — ETL jobs, real-time analytics, order books, log/telemetry pipelines, simulations. At that scale, a hybrid quicksort running in native C++ meaningfully outperforms Array.sort(), and the gap only grows as your data does.
500,000 elements, random data
Array.sort() int ██████████████████████████████████████████████████ 603 ms
epic-sort int ███ 35 ms (~17x faster)
Array.sort() float ██████████████████████████████████████████████████ 1,625 ms
epic-sort float █ 40 ms (~41x faster)And the numeric wins aren't limited to random data — structured patterns like descending-order blocks hit up to ~50x, and skewed/Gaussian-distributed floats consistently land in the 20–41x range. See the full benchmark breakdown.
- ⚡ Native C++ — a real hybrid quicksort, not micro-optimized JS tricks
- 🧠 Auto-detects input type — one
sort()call routes numbers, strings, and mixed arrays to the right path - 🔢 Typed array support — dedicated zero-copy fast paths for
Int32ArrayandFloat64Array - 💾 In-place — mutates the original array
- 🖥️ Cross-platform — prebuilt binaries for Windows, macOS, and Linux
- 📘 TypeScript-first — full type declarations included
- 📦 Zero-config — drop-in replacement, no API to learn
Installation
npm install @epic-js/epic-sortQuick Start
const { sort } = require('@epic-js/epic-sort');
const numbers = [5, 2, 9, 1];
sort(numbers);
console.log(numbers); // [1, 2, 5, 9]That's it — sort() inspects the array and routes it to the fastest matching implementation under the hood.
sort([5, 2, 9, 1]); // numeric → native int/float sort
sort(['banana', 'apple']); // strings → native string sort
sort([3, 'apple', 1]); // mixed → lexicographic sortSorting happens in-place — the original array is mutated and also returned for convenience. For
Int32Array/Float64Arraythis is true zero-copy (the native code sorts the typed array's own buffer directly). For plain number, string, or mixed arrays, epic-sort copies into a native-friendly structure to sort, then writes the result back into your original array — still in-place from the caller's point of view, just not zero-copy internally.
Benchmarks
Independently verified across two separate runs on 23 dataset shapes at 10K / 50K / 100K / 500K elements — see the raw output from both runs below. Results are consistent within normal run-to-run variance, which is exactly what you want to see from a reproducible benchmark.
🏆 Standout wins
These are the categories where epic-sort pulls furthest ahead — every number below reproduced within a few percent across both runs:
| Type (500K elements) | Array.sort() | epic-sort | Speedup |
|---|---:|---:|---:|
| Descending blocks | 186.09 ms | 5.60 ms | ~33x |
| Gaussian float | 1,624.50 ms | 39.64 ms | ~41x |
| Random float | 1,576.58 ms | 41.69 ms | ~38x |
| Exponential float | 1,560.41 ms | 40.93 ms | ~38x |
| Near-sorted int | 97.25 ms | 3.71 ms | ~26x |
| Zeros and ones | 165.11 ms | 10.68 ms | ~15x |
At 10K elements, descending blocks hits ~50x (2.48 ms → 0.05 ms) — the single largest speedup measured across the entire suite.
Numeric arrays: the clear win, across the board
| Size | Type | Array.sort() | epic-sort | Speedup |
|---:|---|---:|---:|---:|
| 10K | Random int | 8.55 ms | 0.50 ms | ~17.1x |
| 10K | Random float | 10.68 ms | 0.75 ms | ~14.2x |
| 10K | Sorted int | 0.72 ms | 0.05 ms | ~14.4x |
| 10K | Reversed int | 1.06 ms | 0.07 ms | ~15.1x |
| 500K | Random int | 602.91 ms | 34.91 ms | ~17.3x |
| 500K | Sorted int | 58.43 ms | 3.10 ms | ~18.9x |
| 500K | Reversed int | 69.02 ms | 3.32 ms | ~20.8x |
| 500K | Few-unique int | 164.24 ms | 13.53 ms | ~12.1x |
| 500K | Partially sorted int | 162.42 ms | 12.17 ms | ~13.4x |
Every numeric pattern tested — random, sorted, reversed, structured, skewed, low-cardinality — wins for epic-sort, consistently in the 10–50x range, and the advantage widens as the array grows.
Strings and mixed data: a more honest picture
This is the part most benchmark tables leave out, and it's worth showing plainly. String and mixed-type sorting route through std::string conversion on the C++ side, which narrows or reverses the advantage — and near the parity line, results get noisy:
| Type (500K) | Run 1 | Run 2 | Verdict |
|---|---:|---:|---|
| Strings | epic 1.05x slower | epic 1.19x faster | Roughly a wash — flips by run |
| Long strings | epic 1.15x faster | epic 1.07x slower | Roughly a wash — flips by run |
| Short strings | epic 1.05x faster | epic 1.09x faster | epic-sort wins narrowly, consistently |
| Repeating strings | epic 2.2x slower | epic 2.3x slower | Array.sort() clearly wins, both runs |
| Mixed (numbers + strings) | epic 2.2x faster | epic 2.1x faster | epic-sort wins consistently, moderately |
The honest takeaway: epic-sort is a numeric-array tool first. On strings it's roughly even (sometimes a hair faster, sometimes not), except on data with many repeated string values, where Array.sort() is reliably faster by more than 2x — likely because JS's sort can short-circuit on repeated comparisons in ways the current C++ path doesn't. Mixed-type arrays (numbers + strings together) win moderately and consistently. If your workload is string-heavy, benchmark both before switching.
===== ARRAY SIZE: 10000 =====
random_int builtin 14.24ms epicCPP 0.54ms
sorted_int builtin 0.93ms epicCPP 0.05ms
reversed_int builtin 0.86ms epicCPP 0.06ms
fewUnique_int builtin 2.30ms epicCPP 0.18ms
partiallySorted_int builtin 1.21ms epicCPP 0.22ms
nearSorted_int builtin 0.76ms epicCPP 0.04ms
oscillating_int builtin 2.87ms epicCPP 0.18ms
ascending_blocks builtin 0.51ms epicCPP 0.07ms
descending_blocks builtin 2.32ms epicCPP 0.05ms
zipf_int builtin 2.70ms epicCPP 0.18ms
gaussian_int builtin 2.89ms epicCPP 0.18ms
all_equal builtin 0.41ms epicCPP 0.08ms
zeros_and_ones builtin 1.83ms epicCPP 0.13ms
random_float builtin 11.37ms epicCPP 0.61ms
gaussian_float builtin 12.33ms epicCPP 0.51ms
exponential_float builtin 11.93ms epicCPP 0.50ms
strings builtin 5.50ms epicCPP 5.40ms
short_strings builtin 4.59ms epicCPP 5.25ms (built-in faster)
long_strings builtin 5.22ms epicCPP 5.20ms
repeating_strings builtin 2.38ms epicCPP 4.35ms (built-in faster)
mixed builtin 7.76ms epicCPP 5.12ms
mixed_heavy_numbers builtin 6.49ms epicCPP 4.58ms
mixed_heavy_strings builtin 5.59ms epicCPP 4.67ms
===== ARRAY SIZE: 50000 =====
random_int builtin 39.84ms epicCPP 2.58ms
sorted_int builtin 5.51ms epicCPP 0.30ms
reversed_int builtin 6.84ms epicCPP 0.32ms
fewUnique_int builtin 15.67ms epicCPP 0.97ms
partiallySorted_int builtin 9.09ms epicCPP 0.85ms
nearSorted_int builtin 5.85ms epicCPP 0.24ms
oscillating_int builtin 16.02ms epicCPP 0.99ms
ascending_blocks builtin 2.23ms epicCPP 0.29ms
descending_blocks builtin 13.80ms epicCPP 0.26ms
zipf_int builtin 13.94ms epicCPP 0.84ms
gaussian_int builtin 13.37ms epicCPP 0.91ms
all_equal builtin 1.89ms epicCPP 0.71ms
zeros_and_ones builtin 10.66ms epicCPP 0.65ms
random_float builtin 84.97ms epicCPP 2.97ms
gaussian_float builtin 91.10ms epicCPP 2.96ms
exponential_float builtin 100.68ms epicCPP 3.11ms
strings builtin 37.74ms epicCPP 32.64ms
short_strings builtin 30.15ms epicCPP 26.36ms
long_strings builtin 35.26ms epicCPP 33.60ms
repeating_strings builtin 15.20ms epicCPP 24.59ms (built-in faster)
mixed builtin 62.76ms epicCPP 34.84ms
mixed_heavy_numbers builtin 45.18ms epicCPP 25.55ms
mixed_heavy_strings builtin 35.34ms epicCPP 24.33ms
===== ARRAY SIZE: 100000 =====
random_int builtin 86.42ms epicCPP 5.75ms
sorted_int builtin 10.54ms epicCPP 0.50ms
reversed_int builtin 13.82ms epicCPP 0.54ms
fewUnique_int builtin 29.47ms epicCPP 2.01ms
partiallySorted_int builtin 18.27ms epicCPP 1.92ms
nearSorted_int builtin 13.39ms epicCPP 0.58ms
oscillating_int builtin 39.28ms epicCPP 2.65ms
ascending_blocks builtin 6.07ms epicCPP 0.82ms
descending_blocks builtin 27.43ms epicCPP 0.64ms
zipf_int builtin 27.94ms epicCPP 1.93ms
gaussian_int builtin 29.48ms epicCPP 1.93ms
all_equal builtin 3.72ms epicCPP 1.19ms
zeros_and_ones builtin 22.07ms epicCPP 1.50ms
random_float builtin 200.85ms epicCPP 7.10ms
gaussian_float builtin 198.71ms epicCPP 7.02ms
exponential_float builtin 219.10ms epicCPP 7.05ms
strings builtin 69.15ms epicCPP 56.32ms
short_strings builtin 65.09ms epicCPP 62.15ms
long_strings builtin 73.00ms epicCPP 67.98ms
repeating_strings builtin 29.40ms epicCPP 53.05ms (built-in faster)
mixed builtin 113.80ms epicCPP 67.04ms
mixed_heavy_numbers builtin 90.52ms epicCPP 64.69ms
mixed_heavy_strings builtin 76.53ms epicCPP 49.42ms
===== ARRAY SIZE: 500000 =====
random_int builtin 537.33ms epicCPP 32.70ms
sorted_int builtin 64.87ms epicCPP 2.96ms
reversed_int builtin 69.03ms epicCPP 3.46ms
fewUnique_int builtin 151.20ms epicCPP 11.80ms
partiallySorted_int builtin 164.45ms epicCPP 11.06ms
nearSorted_int builtin 73.08ms epicCPP 3.01ms
oscillating_int builtin 173.73ms epicCPP 11.49ms
ascending_blocks builtin 26.26ms epicCPP 3.31ms
descending_blocks builtin 152.40ms epicCPP 3.76ms
zipf_int builtin 162.51ms epicCPP 10.01ms
gaussian_int builtin 158.32ms epicCPP 12.35ms
all_equal builtin 20.60ms epicCPP 6.72ms
zeros_and_ones builtin 123.61ms epicCPP 9.17ms
random_float builtin 1454.04ms epicCPP 34.99ms
gaussian_float builtin 1502.22ms epicCPP 35.75ms
exponential_float builtin 1674.15ms epicCPP 45.85ms
strings builtin 430.90ms epicCPP 453.42ms (built-in faster)
short_strings builtin 428.07ms epicCPP 409.57ms
long_strings builtin 467.48ms epicCPP 406.35ms
repeating_strings builtin 163.25ms epicCPP 363.53ms (built-in faster, ~2.2x)
mixed builtin 842.04ms epicCPP 380.81ms
mixed_heavy_numbers builtin 613.72ms epicCPP 339.81ms
mixed_heavy_strings builtin 528.97ms epicCPP 337.45ms===== ARRAY SIZE: 10000 =====
random_int builtin 8.55ms epicCPP 0.50ms
sorted_int builtin 0.72ms epicCPP 0.05ms
reversed_int builtin 1.06ms epicCPP 0.07ms
fewUnique_int builtin 2.46ms epicCPP 0.24ms
partiallySorted_int builtin 1.42ms epicCPP 0.22ms
nearSorted_int builtin 0.82ms epicCPP 0.06ms
oscillating_int builtin 2.99ms epicCPP 0.20ms
ascending_blocks builtin 0.47ms epicCPP 0.06ms
descending_blocks builtin 2.48ms epicCPP 0.05ms
zipf_int builtin 2.63ms epicCPP 0.20ms
gaussian_int builtin 2.23ms epicCPP 0.16ms
all_equal builtin 0.38ms epicCPP 0.10ms
zeros_and_ones builtin 1.86ms epicCPP 0.14ms
random_float builtin 10.68ms epicCPP 0.75ms
gaussian_float builtin 11.94ms epicCPP 0.53ms
exponential_float builtin 12.03ms epicCPP 0.50ms
strings builtin 5.70ms epicCPP 6.16ms (built-in faster)
short_strings builtin 4.65ms epicCPP 5.42ms (built-in faster)
long_strings builtin 6.21ms epicCPP 5.40ms
repeating_strings builtin 2.45ms epicCPP 4.39ms (built-in faster)
mixed builtin 8.33ms epicCPP 5.22ms
mixed_heavy_numbers builtin 6.02ms epicCPP 5.15ms
mixed_heavy_strings builtin 6.91ms epicCPP 5.58ms
===== ARRAY SIZE: 50000 =====
random_int builtin 51.21ms epicCPP 3.17ms
sorted_int builtin 5.65ms epicCPP 0.26ms
reversed_int builtin 8.01ms epicCPP 0.29ms
fewUnique_int builtin 17.53ms epicCPP 1.14ms
partiallySorted_int builtin 9.14ms epicCPP 1.00ms
nearSorted_int builtin 6.55ms epicCPP 0.31ms
oscillating_int builtin 15.66ms epicCPP 1.06ms
ascending_blocks builtin 2.67ms epicCPP 0.38ms
descending_blocks builtin 15.75ms epicCPP 0.38ms
zipf_int builtin 16.67ms epicCPP 1.19ms
gaussian_int builtin 17.30ms epicCPP 1.23ms
all_equal builtin 2.68ms epicCPP 1.17ms
zeros_and_ones builtin 12.91ms epicCPP 0.81ms
random_float builtin 115.43ms epicCPP 3.25ms
gaussian_float builtin 94.35ms epicCPP 2.90ms
exponential_float builtin 87.55ms epicCPP 3.38ms
strings builtin 33.59ms epicCPP 34.40ms (built-in faster)
short_strings builtin 34.81ms epicCPP 33.69ms
long_strings builtin 37.58ms epicCPP 28.68ms
repeating_strings builtin 19.52ms epicCPP 23.31ms (built-in faster)
mixed builtin 61.30ms epicCPP 33.25ms
mixed_heavy_numbers builtin 42.36ms epicCPP 25.12ms
mixed_heavy_strings builtin 39.46ms epicCPP 26.96ms
===== ARRAY SIZE: 100000 =====
random_int builtin 141.75ms epicCPP 8.32ms
sorted_int builtin 11.72ms epicCPP 0.80ms
reversed_int builtin 13.73ms epicCPP 0.73ms
fewUnique_int builtin 33.08ms epicCPP 2.22ms
partiallySorted_int builtin 17.28ms epicCPP 2.03ms
nearSorted_int builtin 12.61ms epicCPP 0.55ms
oscillating_int builtin 53.80ms epicCPP 2.62ms
ascending_blocks builtin 5.78ms epicCPP 0.67ms
descending_blocks builtin 29.13ms epicCPP 0.62ms
zipf_int builtin 29.71ms epicCPP 1.95ms
gaussian_int builtin 39.36ms epicCPP 2.42ms
all_equal builtin 6.04ms epicCPP 1.77ms
zeros_and_ones builtin 25.19ms epicCPP 1.61ms
random_float builtin 231.41ms epicCPP 7.42ms
gaussian_float builtin 238.26ms epicCPP 7.06ms
exponential_float builtin 234.29ms epicCPP 6.83ms
strings builtin 75.89ms epicCPP 66.86ms
short_strings builtin 70.90ms epicCPP 62.73ms
long_strings builtin 76.55ms epicCPP 58.28ms
repeating_strings builtin 48.62ms epicCPP 86.05ms (built-in faster, ~1.8x)
mixed builtin 127.65ms epicCPP 74.41ms
mixed_heavy_numbers builtin 91.97ms epicCPP 55.59ms
mixed_heavy_strings builtin 85.88ms epicCPP 60.33ms
===== ARRAY SIZE: 500000 =====
random_int builtin 602.91ms epicCPP 34.91ms
sorted_int builtin 58.43ms epicCPP 3.10ms
reversed_int builtin 69.02ms epicCPP 3.32ms
fewUnique_int builtin 164.24ms epicCPP 13.53ms
partiallySorted_int builtin 162.42ms epicCPP 12.17ms
nearSorted_int builtin 97.25ms epicCPP 3.71ms
oscillating_int builtin 197.30ms epicCPP 10.88ms
ascending_blocks builtin 32.57ms epicCPP 4.23ms
descending_blocks builtin 186.09ms epicCPP 5.60ms
zipf_int builtin 203.65ms epicCPP 15.16ms
gaussian_int builtin 211.33ms epicCPP 19.68ms
all_equal builtin 27.86ms epicCPP 8.50ms
zeros_and_ones builtin 165.11ms epicCPP 10.68ms
random_float builtin 1576.58ms epicCPP 41.69ms
gaussian_float builtin 1624.50ms epicCPP 39.64ms
exponential_float builtin 1560.41ms epicCPP 40.93ms
strings builtin 553.45ms epicCPP 465.72ms
short_strings builtin 476.77ms epicCPP 436.88ms
long_strings builtin 525.11ms epicCPP 562.39ms (built-in faster)
repeating_strings builtin 169.82ms epicCPP 398.59ms (built-in faster, ~2.3x)
mixed builtin 997.07ms epicCPP 467.71ms
mixed_heavy_numbers builtin 607.22ms epicCPP 367.41ms
mixed_heavy_strings builtin 571.61ms epicCPP 353.60msOn reproducibility: both runs above come from the full epic-sort-benchmark companion suite (23 dataset shapes × 4 sizes). The benchmark.js bundled in this repo is a smaller, quick sanity check (5 patterns × 3 sizes, no 500K) — good for confirming the addon is working on your machine, not for reproducing every row above. Clone the benchmark repo for the full suite.
API
sort(arr)
Smart entry point. Detects whether arr is numeric, string, or mixed, and dispatches to the fastest native path. Recommended for most use cases.
const { sort } = require('@epic-js/epic-sort');
sort([5, 2, 9, 1]); // [1, 2, 5, 9]sortIntArray(arr: Int32Array): Int32Array
Fastest path for 32-bit integers. Throws TypeError if arr is not an Int32Array.
const { sortIntArray } = require('@epic-js/epic-sort');
const arr = new Int32Array([5, 2, 9, 1]);
sortIntArray(arr); // Int32Array [1, 2, 5, 9]sortFloatArray(arr: Float64Array): Float64Array
Fastest path for floating-point numbers. Throws TypeError if arr is not a Float64Array.
const { sortFloatArray } = require('@epic-js/epic-sort');
const arr = new Float64Array([5.4, 2.1, 9.8, 1.3]);
sortFloatArray(arr); // Float64Array [1.3, 2.1, 5.4, 9.8]sortStringArray(arr: string[]): string[]
Native sort for plain string arrays. Throws TypeError if arr isn't an array of strings.
const { sortStringArray } = require('@epic-js/epic-sort');
sortStringArray(['banana', 'apple', 'orange']); // ['apple', 'banana', 'orange']Ordering is byte/ordinal comparison (same as
Array.sort()'s default), not locale-aware — uppercase letters sort before lowercase (e.g.'Banana'before'apple').
sortMixedArray(arr: any[]): any[]
Lexicographic sort for arrays containing mixed value types. Throws TypeError if arr isn't an array.
const { sortMixedArray } = require('@epic-js/epic-sort');
sortMixedArray([3, 'apple', 1]); // [1, 3, 'apple']
nullandundefinedare sorted as the literal strings"null"/"undefined"for comparison purposes — they land wherever those words fall alphabetically, not pinned to the start or end of the array.
Best fit
epic-sort earns its place when you're doing this at scale:
- Data pipelines and ETL jobs sorting large numeric batches (record IDs, timestamps, metric values) repeatedly
- Real-time analytics or dashboards that re-sort large numeric datasets on every update
- Order books, tick data, or other financial workloads sorting large arrays of prices/quantities on a hot path
- Log or telemetry processing sorting large numeric fields at ingestion time
- Simulations, geospatial indexing, or game engines sorting large arrays of numeric coordinates/scores per frame or per tick
- Anywhere
Array.sort()shows up in a CPU profile
Good to know before you reach for it:
- The win is biggest on numeric data (see Benchmarks) — for string-heavy workloads, especially with lots of repeated values, benchmark both first
- Like most native addons, it pays off most on repeated/hot-path sorts rather than a single one-off call
- Smaller arrays still show real speedups (see the 10K rows above), but the margin is naturally smaller than at 500K+
If your data is numeric and your arrays are big, epic-sort is built exactly for you.
Platform Support
Prebuilt native binaries are published for:
- 🪟 Windows (x64)
- 🍎 macOS (Intel x64 and Apple Silicon arm64)
- 🐧 Linux (x64)
Other platforms/architectures (ARM Linux, 32-bit, etc.) will fall back to building from source via node-gyp at install time, which requires a working C++ toolchain. Native modules are tied to your Node.js runtime and CPU architecture. Hit an install or compatibility snag? Open an issue with your Node.js version, OS, and architecture.
TypeScript
Type declarations ship with the package — no @types package needed.
import { sort } from '@epic-js/epic-sort';
const numbers: number[] = [5, 2, 9, 1];
sort(numbers);FAQ
Does it mutate my array?
Yes — sorting is in-place for performance and memory efficiency. sort() also returns the array, so you can chain if you prefer.
Why is the speedup smaller (or negative) on strings and mixed arrays?
Numeric sorting stays entirely in typed native memory. String and mixed-type sorting has to convert JS strings to std::string and back, which eats into the gain. Across two verification runs: mixed arrays win consistently (~2x), plain strings land close to parity in either direction depending on the run, and arrays with lots of repeated string values consistently sort faster with Array.sort() (~2.2–2.3x, both runs). See Benchmarks for the full breakdown. If your workload is string-heavy, benchmark both before switching.
Minimum Node.js version?
Node.js 16 or later (per engines in package.json).
Contributing
Contributions are welcome!
- Fork the repository
- Clone your fork
- Install dependencies (
npm install) - Make your changes
- Run the tests (
npm test) - Open a pull request with a clear description
Support
If epic-sort saves you CPU time, a ⭐ on the repo goes a long way — and if you'd like to support ongoing development directly:
License
MIT © Rajgowthaman Rajendran
