Repository navigation
perf: hash-based vswitch/nswitch and duplicate-table tuning (#107) - #108
Conversation
) Add regression tests for vswitch()/nswitch() with more than 8 value/output pairs: last-duplicate-wins, NA/NaN behaviour per type, mixed encoding, factor/Date outputs, vector recycling, and thread determinism. The tests pass on the current value-major implementation and will guard the new hash path.
…erse#107) For more than 8 value/output pairs, build a value -> last pair index map once and scan x in blocks instead of comparing x against every value. Matching semantics are unchanged: integer/logical NA matches NA, doubles/complex canonicalise -0 and never match NA/NaN, strings compare CHARSXP identity. Small maps are kept sparse (at least 256 slots, 4x values up to 4096) because probe count matters more than a few cache lines; large maps use a 2x load factor. vswitch string 100 values on 2m elements: 0.063s -> 0.007s; 10m: 0.317s -> ~0.04s. nswitch 100 pairs on 2m: 0.069s -> 0.012s (int), 0.072s -> 0.018s (string). The <= 8 pair value-major path is untouched.
Use a 64-bit rolling hash for duplicate-table rows instead of mixing every cell and mixing again, and reuse the shared kit_hash_rows_* helpers in the matrix/data-frame loops. A bounded row sample estimates cardinality: when the input is mostly unique the table is allocated at its final size, while low-cardinality inputs keep the small grow-on-demand table from fastverse#74. 10m x 2 integer data.frame: uniqLen 0.435s -> 0.177s, funique 0.450s -> 0.182s. 10m mixed int/double data.frame: uniqLen 0.828s -> 0.332s, funique 0.852s -> 0.378s. 2m frames improve 1.6-1.8x; vector paths are unchanged.
Manual-only benchmark covering 2m/10m elements, 5/20/100 switch values, integer/string/factor/NA inputs, data-frame funique/uniqLen, and 1/2/4 threads. data.table reference cases are optional and skipped when the package is unavailable. No timing assertions are added to CI.
Record the measured vswitch/nswitch and data-frame unique improvements and cover the remaining hash-path output and input types in tests.
…tverse#107) Only take the hash path for the five atomic types it supports, so raw or other unsupported inputs still fail with the existing argument-'x' message.
- Hoist every LOGICAL/INTEGER/REAL/COMPLEX/STRING_PTR_RO accessor and output pointer out of the OpenMP region. ALTREP accessors are not thread-safe; the previous code could corrupt R's heap with nThread > 1 and ALTREP x/default. - Use R_alloc for the switch hash table so an allocation error cannot leak it. - Keep the old argument-'outputs' error for unsupported output types, and bound the pair count that uses the per-pair map. - Only sample cardinality for inputs of at least 262k rows, use random sample positions, and cap wide-frame samples; this removes the low-cardinality presize regression and the fixed-stride false positive. - Reuse kit_hash_rows_int_raw in countOccurDataFrameR. - Extend tests for hash-path encodings, checkEnc=FALSE, vector defaults and all output writers. Refresh benchmark references and NEWS numbers.
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #108 +/- ##
==========================================
- Coverage 98.01% 96.92% -1.09%
==========================================
Files 13 15 +2
Lines 5083 5338 +255
==========================================
+ Hits 4982 5174 +192
- Misses 101 164 +63 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
- enc2UTF8() now checks every string instead of just the first and preserves NA_STRING. Value-major nswitch() compares each pair with its own converted value instead of the first. - Double hashes canonicalise -0/0 and NaN payloads the way REQUAL does, so funique(), fduplicated() and uniqLen() agree with base R on vectors, matrices and data frames. - Add regression tests and NEWS entries. Measurement: ISNAN() is inlined and R_IsNA() is only called for actual NaNs, so scalar double hashing is 11-14% faster. The double-frame row hash pays about 4% for the special-value check, leaving the PR's speedup intact.
Follow-up: encoding and double-equality fixesPicked up the two pre-existing issues from the PR body (and the related value-major Changes
Performance checkThe first version called Medians below: 10m rows, 15 reps, 1 thread, macOS R 4.6.2, comparing the previous PR head (
Scalar double paths are faster because the old code called two R API functions per element. The double-frame row hash pays about 4% for the per-element special-value check, which still leaves the PR's speedup over 0.0.23 intact (
Verification
|
Summary
Implements the three opportunities in #107.
vswitch()/nswitch()keep the value-major path for ≤8 pairs. Above that, they build the map once and scanxin cache-sized blocks. Last-duplicate-wins, NA/NaN,-0, Inf, complex, encoding, factor/Date and output recycling are unchanged.tools/benchmark-core.Ris manual only: 2m/10m elements, 5/20/100 values, integer/string/factor/NA, data frames, 1/2/4 threads, optional base/data.table references.Evidence
Rscript tools/benchmark-core.R <library> <label> 3; macOS, R 4.6.2, OpenMP disabled for these runs, medians in seconds.vswitchint, 20 values, 10mvswitchstr, 20 values, 10mvswitchint, 100 values, 10mvswitchstr, 100 values, 10mnswitchint, 100 pairs, 10mnswitchstr, 100 pairs, 10muniqLen10m × 2 int dffunique10m × 2 int dfuniqLen10m mixed dffunique10m mixed df2m data frames improve 1.5–1.8x. Five-value switches and scalar random-int vectors are unchanged; the dedicated scalar-hash variants stayed under the 20% stop threshold.
vswitchstring 100 values on 10m is 0.059 s vsdata.table::chmatch0.029 s (chmatch is a first-match lookup, not a full switch).Verification
R CMD check --no-manual: Status: OK.-0/Inf, encodings, duplicates, scalar/vector defaults, outputs, block boundaries).nThread = 4on ALTREPx/defaults; serial and parallel results match.Pre-existing issues fixed here
These were found while fuzzing and are present on
mastertoo. They are now fixed in this PR with regression tests:enc2UTF8()mixed-encoding bug. It only checked element 1 to decide whether to convert, leaving later latin1 elements unconverted and turningNA_STRINGinto the literal"NA". It now checks every element and preservesNA_STRING. Value-majornswitch()also compares each pair against its own converted value instead ofvans[0].Real row/vector hash vs
REQUAL. Row and vector hashes used raw double bits while equality usesREQUAL, so-0vs0(and differing NaN payloads) could be reported as distinct byfunique()/uniqLen()where base R treats them equal. Double hashes now canonicalise-0/0and NaN payloads while keepingNAdistinct, across vector, matrix and data-frame paths.Merge danger
Door: two-way — performance-only, no API, output format or data-layout changes.
Blast radius: switch functions and data-frame/matrix unique paths; low. Version bumped to 0.0.24 with NEWS notes.
Not covered here: 32-bit builds, >50 switch pairs / >8k element stress, and no PR timing assertions in CI.