Core concepts
Kwker calls work like the NumPy, pandas and PyTorch calls you already use, and return the same results. This page covers what you control: whether a call changes your array, how many threads it uses, the sort order, memory and errors. Edge cases come last.
In place or a copy
sort,select,partial_sort,sort_kv,sort_rowsandsort_segmentschange the array in place, views and strided arrays included. Nothing is copied.sortedreturns a sorted copy.argsort,top_k,rank,searchsortedand the other calls that return positions never change their input, so they work on read-only and memory-mapped arrays too.
import numpy as np
import kwker
a = np.array([5, 3, 9, 1], dtype=np.uint32)
b = kwker.sorted(a) # a sorted copy; a is unchanged
kwker.sort(a) # a itself is sorted now
print(a, b)
keys = np.array([3.0, 1.0, 2.0])
values = np.array([30, 10, 20])
kwker.sort_kv(keys, values) # values move with their keys
print(keys, values)
[1 3 5 9] [1 3 5 9]
[1. 2. 3.] [10 20 30]
fn main() {
let mut a: [u32; 4] = [5, 3, 9, 1];
let mut b = a.to_vec(); // a copy to sort; a is unchanged
kwker::sort(&mut b);
kwker::sort(&mut a); // a itself is sorted now
println!("{a:?} {b:?}");
let mut keys = [3.0, 1.0, 2.0];
let mut values = [30, 10, 20];
kwker::sort_kv(&mut keys, &mut values); // values move with their keys
println!("{keys:?} {values:?}");
}
[1, 3, 5, 9] [1, 3, 5, 9] [1.0, 2.0, 3.0] [10, 20, 30]
#include <stdio.h>
#include <kwker.h>
int main(void) {
uint32_t a[] = {5, 3, 9, 1};
kwker_u32_sort(a, 4); /* a itself is sorted now */
printf("%u %u %u %u\n", a[0], a[1], a[2], a[3]);
double keys[] = {3.0, 1.0, 2.0};
int32_t values[] = {30, 10, 20};
kwker_f64_sort_kv(keys, values, sizeof(int32_t), 3, KWKER_ASCENDING); /* values move with their keys */
printf("%g %g %g / %d %d %d\n", keys[0], keys[1], keys[2], values[0], values[1], values[2]);
return 0;
}
1 3 5 9 1 2 3 / 10 20 30
#include <iostream>
#include <vector>
#include <kwker.hpp>
int main() {
std::vector<unsigned> a{5, 3, 9, 1};
std::vector<unsigned> b = a; // a copy to sort; a is unchanged
kwker::sort(b);
kwker::sort(a); // a itself is sorted now
for (auto v : a) std::cout << v << ' ';
std::cout << "/ ";
for (auto v : b) std::cout << v << ' ';
std::cout << '\n';
std::vector<double> keys{3.0, 1.0, 2.0};
std::vector<int> values{30, 10, 20};
kwker::sort_kv(keys.data(), values.data(), keys.size()); // values move with their keys
for (size_t i = 0; i < keys.size(); i++) std::cout << keys[i] << ':' << values[i] << ' ';
std::cout << '\n';
}
1 3 5 9 / 1 3 5 9 1:10 2:20 3:30
const kwk = require("kwker");
const a = new Uint32Array([5, 3, 9, 1]);
const b = kwk.sort(a.slice()); // a sorted copy; a is unchanged
kwk.sort(a); // a itself is sorted now
console.log(a, b);
const keys = new Float64Array([3.0, 1.0, 2.0]);
const values = new Int32Array([30, 10, 20]);
kwk.sortKV(keys, values); // values move with their keys
console.log(keys, values);
Uint32Array(4) [ 1, 3, 5, 9 ] Uint32Array(4) [ 1, 3, 5, 9 ]
Float64Array(3) [ 1, 2, 3 ] Int32Array(3) [ 10, 20, 30 ]
package main
import (
"fmt"
"slices"
"kwker.io/go/kwker"
)
func main() {
a := []uint32{5, 3, 9, 1}
b := slices.Clone(a) // a copy to sort; a is unchanged
kwker.Sort(b)
kwker.Sort(a) // a itself is sorted now
fmt.Println(a, b)
keys := []float64{3.0, 1.0, 2.0}
values := []int{30, 10, 20}
kwker.SortKV(keys, values, kwker.Ascending) // values move with their keys
fmt.Println(keys, values)
}
[1 3 5 9] [1 3 5 9] [1 2 3] [10 20 30]
import io.kwker.Kwker;
import java.util.Arrays;
public class Example {
public static void main(String[] args) {
int[] a = {5, 3, 9, 1};
int[] b = a.clone(); // a copy to sort; a is unchanged
Kwker.sort(b);
Kwker.sort(a); // a itself is sorted now
System.out.println(Arrays.toString(a) + " " + Arrays.toString(b));
double[] keys = {3.0, 1.0, 2.0};
int[] values = {30, 10, 20};
Kwker.sortKV(keys, values, Kwker.ASCENDING); // values move with their keys
System.out.println(Arrays.toString(keys) + " " + Arrays.toString(values));
}
}
[1, 3, 5, 9] [1, 3, 5, 9] [1.0, 2.0, 3.0] [10, 20, 30]
using Kwker;
var a = new uint[] { 5, 3, 9, 1 };
var b = (uint[])a.Clone(); // a copy to sort; a is unchanged
Sorter.Sort(b);
Sorter.Sort(a); // a itself is sorted now
Console.WriteLine(string.Join(" ", a) + " | " + string.Join(" ", b));
var keys = new[] { 3.0, 1.0, 2.0 };
var values = new[] { 30, 10, 20 };
Sorter.SortKV(keys, values); // values move with their keys
Console.WriteLine(string.Join(" ", keys) + " | " + string.Join(" ", values));
1 3 5 9 | 1 3 5 9 1 2 3 | 10 20 30
Threads
A call runs on the calling thread. Kwker starts no threads unless you ask:
threads=4uses four cores for that call;threads=0uses the default set byset_default_threads(1 at start).- All calls in a process share one limit,
set_max_threads(the process's CPUs at start), so concurrent callers never oversubscribe the machine. - Under PyTorch, Kwker runs on PyTorch's thread pool.
Orders
Calls that sort, rank or pick values take an order: smallest first (the default) or largest first.
| Python | Rust | C | C++ | JavaScript | Meaning |
|---|---|---|---|---|---|
descending=False (default) |
Order::ASCENDING |
KWKER_ (0) |
Order::ascending |
{} |
smallest first |
descending=True |
Order::DESCENDING |
KWKER_ |
Order::descending |
{ descending: true } |
largest first |
nans_ |
Order { nans: NanPlacement::First, .. } |
KWKER_ |
Order::nans_ |
{ nansFirst: true } |
NaN values first |
NaN values go last in both directions, as in NumPy, unless you ask for them first.
Supported types
Integers of 8 to 64 bits (128 in Rust and C), float16, bfloat16, FP8 (E4M3, E5M2), float32, float64, packed int4 and byte
strings. In Python: every NumPy integer and float dtype, bfloat16 and FP8 through ml_dtypes, datetime64 /
timedelta64 (NaT last) and NumPy string arrays.
Engines
Kwker picks the fastest code for your CPU when the program starts: avx512, avx2 or sse42 on x86, neon (with
sve where the CPU has it) on ARM, portable elsewhere. Every engine returns the same results; you never choose one.
import kwker
print(kwker.isa()) # the engine in use
kwker.set_isa("portable") # cap it at run time (debugging, A/B timings)
kwker.set_isa(None) # back to the best
To try a slower engine, set KWKER_ISA=avx2 (or portable) before the program starts.
Memory
Most calls need little extra memory, and each call's bound is documented (in Rust, scratch_bound() returns it).
set_scratch_limit(n)caps the extra memory a thread's calls may use.0makes in-place calls allocation-free, with the same results.- A
Plankeeps its buffers between repeated calls:
import numpy as np, kwker
plan = kwker.Plan(descending=True)
for _ in range(3):
idx = plan.argsort(np.random.default_rng(0).integers(0, 1000, 5000)) # buffers reused across calls
Errors
A rejected call changes nothing. Typical causes: k out of range, keys and values of different lengths, an unsupported
type, a null pointer. Each language reports them its usual way:
| Language | Invalid arguments | Other failures |
|---|---|---|
| Python | ValueError, TypeError (an unsupported dtype) |
kwker.Cancelled when a progress callback cancels |
| Rust | a panic, only on the contract violations each function documents | std::io::Error from the file sorts |
| C | return code -1 (0 on success); the C API reference lists each function's codes |
the same codes |
| C++ | std::invalid_ |
std::runtime_ for I/O errors |
| Go | a panic, as for an out-of-range slice index | an error from SortFile, SetISA and Observe |
| JavaScript (Node.js) | TypeError (not a supported typed array), RangeError (k out of range) |
Error |
| JavaScript (WebAssembly) | TypeError, RangeError |
RangeError when WebAssembly memory runs out |
| Java | IllegalArgumentException (NullPointerException for null) |
|
| C# | ArgumentException, ArgumentOutOfRangeException, ArgumentNullException |
|
| Ruby | ArgumentError, TypeError (an unsupported array), FrozenError (a frozen array) |
NoMemoryError |
| PHP | ValueError, TypeError (an array of both ints and floats) |
|
| Perl | croak (a die with the message) |
|
| R | an R error (stop()) |
|
| Swift | a precondition failure (the program stops) |
|
| Zig | error.InvalidArgument |
|
| Fortran | error stop with the message |
|
| COBOL | RETURNING -1 (the C codes) |
Equal values
Equal values matter only when a call returns positions or carries other data along:
| Calls | What happens to equal values |
|---|---|
sort, sorted, select, partial_ |
Not observable: equal values are identical |
argsort, top_, argselect, argpartition, rank(method="ordinal"), lexsort |
Input order (stable) |
sort_ (the default) |
The values of equal keys in any order |
sort_, kway_, group_ |
Input order |
The same input gives the same output on every CPU and with any number of threads.
Special float values
Floats sort by value, with -0.0 before +0.0, -inf and inf at the ends, and NaN values together at the end you
choose. NaN values come back bit for bit as they went in. Calls that compare values (ranks, groups, set operations,
unique) treat -0.0 and +0.0 as one value, as NumPy does. Subnormal values are compared exactly, even when another
library has switched the CPU to flush-to-zero mode. The same holds for every float type, from FP8 to float64.
import numpy as np
import kwker
a = np.array([2.0, np.nan, -0.0, 0.0, -1.0, np.inf])
print(kwker.sorted(a))
print(kwker.sorted(a, descending=True))
print(kwker.sorted(a, descending=True, nans_first=True))
[-1. -0. 0. 2. inf nan]
[inf 2. 0. -0. -1. nan]
[nan inf 2. 0. -0. -1.]
use kwker::{Direction, NanPlacement, Order};
fn main() {
let a = [2.0, f64::NAN, -0.0, 0.0, -1.0, f64::INFINITY];
let mut x = a;
kwker::sort(&mut x);
println!("{x:?}");
let mut y = a;
kwker::sort_descending(&mut y);
println!("{y:?}");
let mut z = a;
kwker::sort_by_order(&mut z, Order { direction: Direction::Descending, nans: NanPlacement::First });
println!("{z:?}");
}
[-1.0, -0.0, 0.0, 2.0, inf, NaN] [inf, 2.0, 0.0, -0.0, -1.0, NaN] [NaN, inf, 2.0, 0.0, -0.0, -1.0]
#include <math.h>
#include <stdio.h>
#include <string.h>
#include <kwker.h>
int main(void) {
const double a[] = {2.0, NAN, -0.0, 0.0, -1.0, INFINITY};
const uint32_t orders[] = {KWKER_ASCENDING, KWKER_DESCENDING, KWKER_DESCENDING | KWKER_NANS_FIRST};
for (int k = 0; k < 3; k++) {
double x[6];
memcpy(x, a, sizeof a);
kwker_f64_sort_order(x, 6, orders[k]);
for (int i = 0; i < 6; i++) printf(i ? " %g" : "%g", x[i]);
printf("\n");
}
return 0;
}
-1 -0 0 2 inf nan inf 2 0 -0 -1 nan nan inf 2 0 -0 -1
#include <cmath>
#include <iostream>
#include <vector>
#include <kwker.hpp>
int main() {
const std::vector<double> a{2.0, NAN, -0.0, 0.0, -1.0, INFINITY};
using O = kwker::Order;
for (O order : {O::ascending, O::descending, O::descending_nans_first}) {
std::vector<double> x = a;
kwker::sort(x, order);
for (double v : x) std::cout << v << ' ';
std::cout << '\n';
}
}
-1 -0 0 2 inf nan inf 2 0 -0 -1 nan nan inf 2 0 -0 -1
const kwk = require("kwker");
const a = new Float64Array([2.0, NaN, -0.0, 0.0, -1.0, Infinity]);
console.log(kwk.sort(a.slice()));
console.log(kwk.sort(a.slice(), { descending: true }));
console.log(kwk.sort(a.slice(), { descending: true, nansFirst: true }));
Float64Array(6) [ -1, -0, 0, 2, Infinity, NaN ]
Float64Array(6) [ Infinity, 2, 0, -0, -1, NaN ]
Float64Array(6) [ NaN, Infinity, 2, 0, -0, -1 ]
package main
import (
"fmt"
"math"
"slices"
"kwker.io/go/kwker"
)
func main() {
a := []float64{2.0, math.NaN(), math.Copysign(0, -1), 0.0, -1.0, math.Inf(1)}
for _, order := range []kwker.Order{kwker.Ascending, kwker.Descending, kwker.Descending | kwker.NaNsFirst} {
x := slices.Clone(a)
kwker.SortOrder(x, order)
fmt.Println(x)
}
}
[-1 -0 0 2 +Inf NaN] [+Inf 2 0 -0 -1 NaN] [NaN +Inf 2 0 -0 -1]
import io.kwker.Kwker;
import java.util.Arrays;
public class Example {
public static void main(String[] args) {
double[] a = {2.0, Double.NaN, -0.0, 0.0, -1.0, Double.POSITIVE_INFINITY};
for (int order : new int[] {Kwker.ASCENDING, Kwker.DESCENDING, Kwker.DESCENDING | Kwker.NANS_FIRST}) {
double[] x = a.clone();
Kwker.sort(x, order);
System.out.println(Arrays.toString(x));
}
}
}
[-1.0, -0.0, 0.0, 2.0, Infinity, NaN] [Infinity, 2.0, 0.0, -0.0, -1.0, NaN] [NaN, Infinity, 2.0, 0.0, -0.0, -1.0]
using Kwker;
var a = new double[] { 2.0, double.NaN, -0.0, 0.0, -1.0, double.PositiveInfinity };
foreach (var order in new[] { Order.Ascending, Order.Descending, Order.Descending | Order.NansFirst })
{
var x = (double[])a.Clone();
Sorter.Sort(x, order);
Console.WriteLine(string.Join(" ", x));
}
-1 -0 0 2 ∞ NaN ∞ 2 0 -0 -1 NaN NaN ∞ 2 0 -0 -1
Related
- Behavior specification: every rule above stated exactly, each with the test that checks it.
- Sorting: the order options at work on real arrays.
- Runtime controls: engines, threads and memory, set at run time.
- Known limitations: what 0.1.0 does not do yet.