Skip to content

Commit 4da0455

Browse files
author
Can Gokmen
committed
Renamed PARALLEL to BRANCHLESS
1 parent 6b0dfde commit 4da0455

4 files changed

Lines changed: 37 additions & 37 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ endif()
4747

4848
# ── Targets ─────────────────────────────────────────────────────────────────────
4949
# All executables are tuned for the build host with -march=native. This enables
50-
# the wider instruction set the QuART_kfp Parallel findSlot and the Node16 SSE
50+
# the wider instruction set the QuART_kfp Branchless findSlot and the Node16 SSE
5151
# compares benefit from. Note: it applies to the WHOLE binary, so every tree type
5252
# is built for this CPU and the result may SIGILL on an older/different machine.
5353
#

sequential_findslot_report.txt

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
================================================================================
2-
QuART k-fp: Why the Sequential findSlot is Slower than the Parallel findSlot
2+
QuART k-fp: Why the Sequential findSlot is Slower than the Branchless findSlot
33
A profiling report + branch-misprediction explanation
44
================================================================================
55

@@ -18,7 +18,7 @@ Benchmark : test_kfp, K=3, three sorted BoDS streams interleaved uniformly at
1818
* The Sequential findSlot is slower because of BRANCH MISPREDICTION, not because
1919
of data copying or cache misses.
2020

21-
* perf proves it: the Sequential and Parallel modes have an IDENTICAL L1 data-
21+
* perf proves it: the Sequential and Branchless modes have an IDENTICAL L1 data-
2222
cache miss rate (0.14% vs 0.15%), but Sequential suffers ~35M MORE branch
2323
mispredictions (55.2M vs 19.7M). Each misprediction is a ~15-20 cycle pipeline
2424
flush.
@@ -28,7 +28,7 @@ Benchmark : test_kfp, K=3, three sorted BoDS streams interleaved uniformly at
2828
uniformly at random, each key matches a random one of the 3 slots, so that
2929
branch is fundamentally unpredictable (~1/3 taken, no pattern).
3030

31-
* The Parallel mode is faster because it converts K unpredictable branches into
31+
* The Branchless mode is faster because it converts K unpredictable branches into
3232
K branchless comparisons + ONE highly-predictable branch ("did ANY slot
3333
match?", ~96% taken) + one ctz instruction to extract which slot.
3434

@@ -39,7 +39,7 @@ Benchmark : test_kfp, K=3, three sorted BoDS streams interleaved uniformly at
3939
REGRESSES this uniform-random benchmark.
4040

4141
* Decision: leave the Sequential findSlot as the clean, honest baseline. The
42-
Parallel mode is the answer when speed matters.
42+
Branchless mode is the answer when speed matters.
4343

4444

4545
--------------------------------------------------------------------------------
@@ -58,7 +58,7 @@ upper bytes:
5858
findSlot has two implementations selected at compile time by the SearchMode
5959
template parameter:
6060

61-
Parallel (default) - branchless K-way compare, collapsed to one branch.
61+
Branchless (default) - K-way compare, collapsed to one branch.
6262
Sequential - the classic early-exit scan: test slots one at a
6363
time, stop at the first match.
6464

@@ -84,7 +84,7 @@ SEQUENTIAL (the early-exit scan; trees/QuART_kfp.h):
8484
}
8585
return {-1, MatchType::NO_MATCH};
8686

87-
PARALLEL (branchless K-way match; trees/QuART_kfp.h):
87+
BRANCHLESS (K-way match; trees/QuART_kfp.h):
8888

8989
unsigned match = 0;
9090
for (int i = 0; i < K; i++)
@@ -103,7 +103,7 @@ compiler.
103103

104104
To measure each mode in isolation, test_kfp gained dedicated run modes:
105105

106-
./build/test_kfp par <maxkeys> # only the Parallel QuART_kfp<3>
106+
./build/test_kfp par <maxkeys> # only the Branchless QuART_kfp<3>
107107
./build/test_kfp seq <maxkeys> # only the Sequential QuART_kfp<3,FIFO,Sequential>
108108
./build/test_kfp ff <maxkeys> # only the FREQ_FILTER variant
109109
./build/test_kfp kfp <maxkeys> # all three
@@ -132,9 +132,9 @@ Hotspot attribution:
132132
4. RESULTS (perf stat, isolated, 20M keys/stream)
133133
--------------------------------------------------------------------------------
134134

135-
Metric Sequential Parallel Delta
135+
Metric Sequential Branchless Delta
136136
----------------------- ------------ ----------- ----------------
137-
Throughput (timed) 39.4 M/s 54.1 M/s Parallel 1.37x
137+
Throughput (timed) 39.4 M/s 54.1 M/s Branchless 1.37x
138138
task-clock 1528 ms 1114 ms
139139
instructions 12.05 B 11.80 B seq +2%
140140
cycles 5.58 B 3.94 B seq +42%
@@ -163,7 +163,7 @@ early-exit comparison branches.
163163
The two telling facts:
164164

165165
(a) The L1 data-cache miss RATE is identical (0.14% vs 0.15%). The Sequential
166-
scan does NOT copy structs and does NOT miss cache more than Parallel. The
166+
scan does NOT copy structs and does NOT miss cache more than Branchless. The
167167
hypothesis that "copying instead of references" causes the slowdown is
168168
DISPROVEN by the data. (The only real copies in the insert path are in
169169
loadSlot/saveToSlot, and the FP_INSERT hot path already uses a reference
@@ -234,10 +234,10 @@ THE SEQUENTIAL BRANCH HAS NO PATTERN
234234

235235

236236
--------------------------------------------------------------------------------
237-
7. HOW THE PARALLEL DESIGN SOLVES IT
237+
7. HOW THE BRANCHLESS DESIGN SOLVES IT
238238
--------------------------------------------------------------------------------
239239

240-
The Parallel version never branches per slot. Conceptually, for K=3:
240+
The Branchless version never branches per slot. Conceptually, for K=3:
241241

242242
bit0 = (slots[0].cached_upper == keyUpper); // branchless: cmp + sete
243243
bit1 = (slots[1].cached_upper == keyUpper); // branchless
@@ -253,7 +253,7 @@ Three things change:
253253
with NO jump and NO control-flow change. There is nothing to mispredict --
254254
the CPU computes all three comparisons unconditionally. Because the three
255255
are independent (no data dependency), they issue on separate execution ports
256-
in the same cycles. That parallelism is what gives the mode its name.
256+
in the same cycles -- and none of them is a branch, which names the mode.
257257
Result for a key from stream B: match = 0b010.
258258

259259
2. THE ONE REMAINING BRANCH IS PREDICTABLE.
@@ -267,7 +267,7 @@ Three things change:
267267
__builtin_ctz(match) (count-trailing-zeros), a single instruction returning
268268
the index of the lowest set bit (0b010 -> 1). No guessing.
269269

270-
Intuition: Parallel shows the package to all three people AT ONCE; they answer
270+
Intuition: Branchless shows the package to all three people AT ONCE; they answer
271271
simultaneously and you read off who raised a hand. The only thing you "predict"
272272
is "did somebody raise a hand?", and the answer is almost always yes.
273273

@@ -278,7 +278,7 @@ THE CORE IDEA
278278

279279
Sequential : in CONTROL FLOW (a chain of data-dependent branches)
280280
-> the predictor must guess random outcomes -> ~55M flushes.
281-
Parallel : in the DATAPATH (branchless sete compares + ctz), leaving only
281+
Branchless : in the DATAPATH (branchless sete compares + ctz), leaving only
282282
one PREDICTABLE branch ("any match?") -> ~20M misses, 1.37x.
283283

284284
You cannot make the random "which slot" question predictable; that is the
@@ -350,7 +350,7 @@ regresses the uniform benchmark.
350350
NOT cache, NOT instruction count.
351351

352352
* On THIS benchmark the Sequential scan is already at its floor. No change that
353-
keeps it sequential improves it; the parallel restructuring is the only thing
353+
keeps it sequential improves it; the branchless restructuring is the only thing
354354
that helps, and that mode already exists.
355355

356356
* The one realistic lever is the temporal-locality hint, which only pays off if
@@ -359,7 +359,7 @@ regresses the uniform benchmark.
359359
construction, so it cannot show that win and in fact regresses ~2-6%.
360360

361361
* DECISION: leave the Sequential findSlot as the clean, honest baseline. Use the
362-
Parallel mode when throughput matters (1.37x here). If a future experiment uses
362+
Branchless mode when throughput matters (1.37x here). If a future experiment uses
363363
clustered/bursty interleaves, revisit the opt-in hint as a third SearchMode.
364364

365365

@@ -372,7 +372,7 @@ regresses the uniform benchmark.
372372
cmake --build build --target test_kfp
373373

374374
# Isolated timing
375-
./build/test_kfp par 20000000 # Parallel ~54 M/s
375+
./build/test_kfp par 20000000 # Branchless ~54 M/s
376376
./build/test_kfp seq 20000000 # Sequential ~39 M/s
377377
./build/test_kfp kfp 20000000 # both + FREQ_FILTER, prints 1.37x speedup
378378

@@ -384,7 +384,7 @@ regresses the uniform benchmark.
384384
L1-dcache-loads,L1-dcache-load-misses ./build-prof/test_kfp seq 20000000
385385
perf stat -e ... ./build-prof/test_kfp par 20000000
386386

387-
Code under study: trees/QuART_kfp.h, function findSlot (Parallel branchless
387+
Code under study: trees/QuART_kfp.h, function findSlot (Branchless
388388
block and the Sequential early-exit scan).
389389

390390
================================================================================

test_kfp_grid.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
// (seed 42 → identical sequence for every tree), pre-loads PRELOAD_FRAC
88
// untimed, then times the remaining inserts into plain ART and into
99
// QuART_kfp<K> for K in {1,2,4,8,16,32,64}, in both findSlot SearchModes
10-
// (Parallel and SIMD). Emits one CSV row per (mode,K) to stdout:
10+
// (Branchless and SIMD). Emits one CSV row per (mode,K) to stdout:
1111
//
1212
// series,streams,keys_per_stream,mode,K,art_ns,kfp_ns,speedup
1313
//
@@ -140,7 +140,7 @@ static long long run_one(TreeT& tree, const vector<key_int_t>& vals,
140140

141141
// Emit one CSV row (machine-readable) on stdout. A crashed run is recorded
142142
// with kfp_ns = -1 and speedup = -1 so the cell is preserved (not lost) in the
143-
// grid. `mode` is the findSlot SearchMode ("parallel"/"simd"), or "art" for
143+
// grid. `mode` is the findSlot SearchMode ("branchless"/"simd"), or "art" for
144144
// the baseline row.
145145
static void emit_csv(const string& series, size_t streams, size_t kps,
146146
const char* mode, int K, long long art_ns,
@@ -216,16 +216,16 @@ static void sweep_k_mode(const string& series, size_t streams, size_t kps,
216216
emit_csv(series, streams, kps, mode, K, art_ns, ns);
217217
}
218218

219-
// Sweep one K value across both findSlot SearchModes (Parallel vs SIMD) so the
219+
// Sweep one K value across both findSlot SearchModes (Branchless vs SIMD) so the
220220
// grid carries a direct A/B of the SoA+AVX2 classifier against the scalar
221221
// branchless one.
222222
template <int K>
223223
static void sweep_k(const string& series, size_t streams, size_t kps,
224224
const vector<key_int_t>& vals,
225225
const vector<array<uint8_t, keyBytes>>& enc, size_t preload,
226226
long long art_ns) {
227-
sweep_k_mode<K, SearchMode::Parallel>(series, streams, kps, vals, enc,
228-
preload, art_ns, "parallel");
227+
sweep_k_mode<K, SearchMode::Branchless>(series, streams, kps, vals, enc,
228+
preload, art_ns, "branchless");
229229
sweep_k_mode<K, SearchMode::SIMD>(series, streams, kps, vals, enc, preload,
230230
art_ns, "simd");
231231
}

trees/QuART_kfp.h

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ enum class EvictionPolicy { FIFO, FREQ_FILTER };
2424

2525
// Strategy used by findSlot to classify a key against the K active slots.
2626
//
27-
// Parallel – data-parallel branchless FP_INSERT pre-pass: all K
27+
// Branchless – data-parallel FP_INSERT pre-pass: all K
2828
// upper-byte equality checks are computed with no inter-check
2929
// data dependency, so the CPU issues them together and the
3030
// whole classification collapses to a single, well-predicted
@@ -36,7 +36,7 @@ enum class EvictionPolicy { FIFO, FREQ_FILTER };
3636
// offered one) the lowest-index BRIDGE. Simpler, but every
3737
// per-slot comparison is a hard-to-predict branch.
3838
//
39-
// SIMD – like Parallel, but the K upper-byte equality checks are done
39+
// SIMD – like Branchless, but the K upper-byte equality checks are done
4040
// over a packed, struct-of-arrays `cached_uppers[K]` companion
4141
// array (one machine word per slot, ~K/8 cache lines, L1-
4242
// resident) using AVX2 (_mm256_cmpeq_epi32, 8 lanes per
@@ -46,18 +46,18 @@ enum class EvictionPolicy { FIFO, FREQ_FILTER };
4646
// loop in insert() is also dropped for this mode — only the one
4747
// matched slot's fp is touched. This keeps the per-insert tax
4848
// ~flat as K grows, so k-fp stays >1x vs ART even at high stream
49-
// counts. Same classifier semantics as Parallel (lowest-index
49+
// counts. Same classifier semantics as Branchless (lowest-index
5050
// FP_INSERT wins once all K slots are occupied).
5151
//
5252
// All three SearchModes are exact equivalents. Each returns the lowest-index
5353
// FP_INSERT among all active slots, and only when no slot offers an FP_INSERT
54-
// does it return the lowest-index BRIDGE. Parallel/SIMD compute the FP_INSERT
54+
// does it return the lowest-index BRIDGE. Branchless/SIMD compute the FP_INSERT
5555
// pass branchlessly / with AVX2 once all K slots are full; Sequential walks the
5656
// slots in two scalar passes (all FP_INSERT checks, then all BRIDGE checks).
5757
// They differ only in instruction mix, never in the slot or match type they
5858
// pick — in particular a BRIDGE in a low-index slot can never pre-empt an exact
5959
// FP_INSERT in a higher-index slot in any mode.
60-
enum class SearchMode { Sequential, Parallel, SIMD };
60+
enum class SearchMode { Sequential, Branchless, SIMD };
6161

6262
// QuART_kfp<K, Policy, Search>: Maintains up to K independent fast-path slots,
6363
// one per workload. Uses the stail key-classification scheme (FP_INSERT /
@@ -75,9 +75,9 @@ enum class SearchMode { Sequential, Parallel, SIMD };
7575
// (eviction policy selected by the Policy template parameter)
7676
//
7777
// The Search template parameter selects how findSlot scans the K slots
78-
// (Parallel branchless vs. Sequential early-exit); see SearchMode above.
78+
// (Branchless vs. Sequential early-exit); see SearchMode above.
7979
template <int K, EvictionPolicy Policy = EvictionPolicy::FIFO,
80-
SearchMode Search = SearchMode::Parallel>
80+
SearchMode Search = SearchMode::Branchless>
8181
class QuART_kfp : public QuART {
8282
public:
8383
QuART_kfp() : QuART(), num_active(0), next_evict(0), active_slot(-1)
@@ -349,8 +349,8 @@ class QuART_kfp : public QuART {
349349
std::pair<int, MatchType> findSlot(uint8_t key[]) const {
350350
key_int_t keyUpper = getKeyUpperBytes(key);
351351

352-
if constexpr (Search == SearchMode::Parallel) {
353-
// Parallel branchless FP_INSERT classification.
352+
if constexpr (Search == SearchMode::Branchless) {
353+
// Branchless FP_INSERT classification.
354354
// All K equality checks are emitted as branchless sete/cmov
355355
// instructions with no data dependencies between them, so the CPU can
356356
// issue them in parallel. The single resulting branch (match != 0) has
@@ -380,7 +380,7 @@ class QuART_kfp : public QuART {
380380
// _mm256_cmpeq_epi32 tests 8 slots; movemask_ps packs the 8 lane
381381
// results into bits, accumulated into a 64-bit `match` (bit i == slot
382382
// i, so ctzll picks the lowest-index match — same tie-break as
383-
// Parallel). Only runs once all K slots are full, mirroring Parallel.
383+
// Branchless). Only runs once all K slots are full, mirroring Branchless.
384384
if (__builtin_expect(num_active == K, 1)) {
385385
uint64_t match = 0;
386386
if constexpr (sizeof(key_int_t) == 4) {
@@ -414,11 +414,11 @@ class QuART_kfp : public QuART {
414414
// GLOBAL FP_INSERT: scan every active slot and return the lowest-index
415415
// exact upper-byte match. Only if no slot offers an FP_INSERT do we make a
416416
// second pass for the lowest-index BRIDGE. Splitting the passes is what
417-
// makes Sequential agree with the Parallel/SIMD FP_INSERT pre-pass — a
417+
// makes Sequential agree with the Branchless/SIMD FP_INSERT pre-pass — a
418418
// BRIDGE in a low-index slot can no longer pre-empt an exact FP_INSERT in a
419419
// higher-index slot.
420420
//
421-
// In Parallel/SIMD mode, when all K slots are occupied the branchless/AVX2
421+
// In Branchless/SIMD mode, when all K slots are occupied the branchless/AVX2
422422
// FP_INSERT pre-pass above already returned any exact match, so the first
423423
// pass here finds nothing and this loop is just the BRIDGE / warm-up path.
424424
// In Sequential mode it is the entire classifier.

0 commit comments

Comments
 (0)