zufast is a header-only library of small, fast, portable C99
primitives for the hot path between external text or bytes and native
typed values, for other R packages to use through
LinkingTo: zufast:
| Header | What it provides |
|---|---|
zufast/number.h |
correctly rounded parsing of double, float
and 32/64-bit integers (any base 2–36, JSON mode); shortest round-trip
and exact fixed formatting; integer writers |
zufast/datetime.h |
ISO 8601 / RFC 3339 dates and timestamps to fields and back; calendar arithmetic |
zufast/literal.h |
booleans with per-spelling flags; literal matching; ASCII whitespace |
zufast/uuid.h |
UUID parsing and formatting |
zufast/hex.h, zufast/base64.h |
hex and strict Base64 (standard and URL alphabets) |
zufast/hash.h |
XXH3 64- and 128-bit hashing, one-shot and streaming (not cryptographic) |
zufast/bits.h |
binary16 and bfloat16 conversion; endian loads, stores and swaps |
zufast/utf8.h |
UTF-8 validation and code-point counting |
zufast/status.h, zufast/version.h |
the shared zuf_result type and status codes; the
version macros |
Every function is static inline, allocates nothing,
holds no state, uses no locale and may be called from any thread.
zufast stands on the work of others, vendored at pinned releases:
The rest is package-owned.
zufast is not yet on CRAN. Install the development version from GitHub:
# install.packages("pak")
pak::pak("pedrobtz/zufast")Once it is released, install it from CRAN with
install.packages("zufast").
This is the whole recipe; the fixture package
tools/zufasttest follows it verbatim and is built, checked
and tested on Linux, macOS and Windows on every change.
1. DESCRIPTION: add zufast to
LinkingTo, and nothing else. No Imports:, no
importFrom(), no configure, no
PKG_LIBS:
LinkingTo: zufast
zufast is needed when your package is built, not when it runs: your shared object carries its own copy of every zufast function it uses, and keeps working if zufast is later removed.
2. src/Makevars and
src/Makevars.win: hide your symbols, as the zu
family does:
PKG_CFLAGS = $(C_VISIBILITY)
It is defence in depth: every function and every vendored symbol
zufast emits is already static, so two packages that both
use zufast never bind to each other’s copies.
3. Include and call. Everything, or one area at a time:
#include <zufast.h> /* everything */
#include <zufast/number.h> /* or one area */
/* Parse a whole cell as a double. */
static int cell_to_double(const char *first, const char *last, double *out)
{
zuf_result r = zuf_parse_f64(first, last, out);
return r.status == ZUF_OK && r.ptr == last;
}The one idiom to learn: parsers follow std::from_chars.
They never require the whole span to be consumed; r.ptr is
one past the last byte used, and a caller that needs “the entire span is
a value” checks r.ptr == last. On
ZUF_ERR_INVALID, r.ptr == first.
Formatters return the length the value needs, write at most
cap bytes, never NUL-terminate, and measure when
cap == 0; the ZUF_*_MAX_CHARS macros are
capacities that always suffice:
char buf[ZUF_F64_MAX_CHARS];
size_t n = zuf_format_f64(buf, sizeof buf, x); /* "0.30000000000000004" */Any number of translation units may include zufast; there is no
implementation macro and nothing to link. A unit that includes
<zufast.h> reads about fifteen thousand lines of
headers, so include only the areas you use where build time matters. The
headers compile as C99 and as C++11, so cpp11 and Rcpp code can include
them too.
Versions. ZUFAST_VERSION_MAJOR,
_MINOR, _PATCH and ZUFAST_VERSION
say which headers you compiled against. Names with a
zuf_int_ prefix are internal. See
vignette("linking") for more.
The R functions exist to test, benchmark and demonstrate the C layer:
library(zufast)
fast_parse_double(c("0.1", "1e-7", " +inf ", "abc"))
fast_format_double(0.1 + 0.2) # "0.30000000000000004"
fast_parse_datetime("2024-02-29T12:30:00+01:00")
fast_base64_encode(charToRaw("hello"), url = TRUE)
fast_hash("hello", bits = 128)
fast_info()Two deliberate differences from base R:
fast_parse_double() is correctly rounded where
as.numeric() is not (how often they differ in the last bit
depends on the platform and on the number of digits: on Apple Silicon,
where R has no extended long double, they agree on decimals
of up to fifteen significant digits and differ on about one 17-digit
decimal in five), and fast_format_double() writes the
shortest digits that round-trip where as.character() writes
fifteen significant digits.
One million ordinary decimals, parsed by as.numeric(),
by RcppFastFloat
(the C++ fast_float behind an R wrapper) and by zufast,
timed with bench::mark():
set.seed(1)
x <- sprintf("%.6f", runif(1e6, -1000, 1000))
bench::mark(
as.numeric(x),
RcppFastFloat::as.double2(x),
fast_parse_double(x),
check = function(a, b) isTRUE(all.equal(a, b)),
min_iterations = 20
)| expression | median | iterations/sec | memory |
|---|---|---|---|
as.numeric(x) |
84.8 ms | 11.8 | 7.63 MB |
RcppFastFloat::as.double2(x) |
34.3 ms | 29.1 | 13.49 MB |
fast_parse_double(x) |
27.8 ms | 36.0 | 7.64 MB |
Apple M1, R 4.6.1, zufast 0.1.0, RcppFastFloat 0.0.6, Apple clang 21.
zufast and fast_float are the same algorithm and return
identical doubles; what separates them here is the R wrapper and the
memory it allocates. tools/benchmarks.R in the repository
measures the other areas against their usual comparisons.
Adoption is tracked in each consumer’s repository and waits on zufast reaching CRAN: zuxlsx#71 (numeric cells), zuyaml#14 (scalars), zucbor#41 (float16, Base64, dates, UUIDs, big-endian loads), zuhttp#79 (Base64, hex), zucsv#11 (UTF-8, booleans).
MIT for zufast. The vendored code keeps its own licences (MIT for
ffc.h, Boost 1.0 for Ryu, BSD 2-Clause for xxHash, MIT for Hoehrmann’s
UTF-8 table); see inst/COPYRIGHTS and
LICENSE.note.