Benchmark: JSCBigInt vs. attaswift/BigInt vs. swift-bignum

August 15, 2026 · View on GitHub

How does delegating to JavaScriptCore compare to pure-Swift bignums? Short answer: the winner depends entirely on operand size. The pure-Swift libraries dominate when operations are small and frequent (the Swift ⇄ JSC bridge costs ~2 µs per call); JSCBigInt dominates once the numbers grow past a few thousand digits, because the actual arithmetic runs in WebKit's optimized C++ BigInt core.

Environment

MachineApple M1
OSmacOS 26.6.1
Swift6.3.3 (release build, -c release)
attaswift/BigInt5.7.0
dankogai/swift-bignum6.3.1
Date2026-08-15

Method

The harness lives in Benchmarks/ — a standalone package depending on all three libraries. Each case runs the identical algorithm on identical inputs on every side, takes the best of 3 runs, and all libraries' outputs are checked for agreement before timing. Results are fed to a @inline(never) sink to prevent dead-code elimination. (attaswift/BigInt and swift-bignum both name their type BigInt, so the harness imports each in its own file and races them under typealiases.)

cd Benchmarks && swift run -c release bench

Results

Bold marks the fastest per row; parenthesized ratios are relative to it.

BenchmarkJSCBigIntattaswift/BigIntswift-bignum
sum 1...100_000 (100k small ops)307.78 ms (19.5×)15.75 ms25.49 ms (1.6×)
fib(10_000) (2,090 digits)11.12 ms (4.1×)4.34 ms (1.6×)2.73 ms
factorial(1_000) (2,568 digits)2.75 ms (7.9×)0.39 ms (1.1×)0.35 ms
2.power(100_000) (30,103 digits)0.01 ms0.03 ms (3.8×)0.19 ms (24.1×)
10k-digit × 10k-digit, 100 times23.93 ms182.51 ms (7.6×)61.84 ms (2.6×)
20k-digit ÷ 10k-digit, 100 times66.01 ms325.26 ms (4.9×)179.64 ms (2.7×)
2060-bit modPow (power(_:mod:))16.90 ms76.03 ms (4.5×)57.06 ms (3.4×)
gcd of two 1,000-digit numbers, 10 times3.42 ms6.01 ms (1.8×)3.75 ms (1.1×)
isqrt of 20k-digit number, 10 times115.80 ms536.82 ms (4.6×)291.67 ms (2.5×)
toString(21002^{100}_000), decimal20.39 ms (1.1×)26.78 ms (1.4×)19.31 ms
parse 10,000 decimal digits, 100 times62.42 ms (1.9×)93.82 ms (2.9×)32.13 ms

Analysis

The bridge tax is fixed; the arithmetic advantage scales. Every JSCBigInt operation crosses from Swift into JavaScriptCore through a cached JSValue function call, costing roughly 1.5–2 µs regardless of operand size (the 100k-small-ops case is almost pure bridge: ~340 ms for ~200k crossings). For small numbers that overhead is the whole story, and the pure-Swift libraries — which add machine words in nanoseconds — win by 13–21×.

As operands grow, the crossing cost stays constant while the work per operation grows, so the balance tips:

  • fib(10_000) and factorial(1_000) are transitional: thousands of operations on merely-thousands-of-digits numbers. The pure-Swift libraries still lead (swift-bignum fastest on both), but the gap narrows from 20× to 4–8×.
  • Multiplication and division of 10k+-digit numbers flip the table: JSCBigInt beats attaswift by 7.6× / 4.9× and swift-bignum by 2.6× / 2.7×. WebKit's JSBigInt core uses asymptotically better algorithms and hand-tuned C++ limb arithmetic that pure Swift has to chase.
  • RSA-sized modPow is JSCBigInt's best case among the practical workloads: power(_:mod:) runs its entire square-and-multiply loop inside JavaScriptCore in a single bridge crossing — ~2060 squarings of 2060-bit numbers with zero per-operation bridge tax — beating swift-bignum 3.4× and attaswift 4.6×.
  • gcd and isqrt follow the same pattern: both run their entire loop inside JSC in one crossing. squareRoot() of a 20k-digit number (Newton's method, division-dominated) wins by the division margin, 2.5–4.6×. Euclid gcd on 1,000-digit operands is a near-tie with swift-bignum's binary gcd — the operands are small enough that algorithm choice, not limb speed, decides. A cautionary tale from writing this row: the first input pair (x and (x>>3)*5+7$) \text{was} \text{linearly} \text{related}, \text{collapsing} \text{Euclid} \text{to} 8 \text{iterations} \text{and} "\text{winning}" 217 \times — \text{benchmark} \text{inputs} \text{for} \text{gcd} \text{must} \text{be} \text{genuinely} \text{unrelated}, \text{so} \text{the} \text{harness} \text{uses} \text{coprime} \text{prime} \text{powers} ($7^1183, 3^2093, Euclid chain 1922 ≈ the 1.94/digit random average).
  • Radix-10 conversion is a close race between JSCBigInt and swift-bignum on printing, while swift-bignum's parser is the clear winner — reminding us that algorithm choice matters more than language: attaswift, also pure Swift, trails both.
  • 2.power(100_000) is a curiosity: JSC recognizes the power-of-two case and effectively reduces it to a shift, finishing in microseconds.

Between the two pure-Swift libraries, swift-bignum is the faster bignum across the board — up to ~3× on big multiplication, division, and parsing — while attaswift takes the many-tiny-ops case where per-value allocation dominates.

Takeaway

If your workload is many operations on modest numbers (exponent loops, counters, hashes), a native-Swift bignum is the right tool. If it is fewer operations on genuinely huge numbers (10⁴+ digits), JSCBigInt's borrowed WebKit engine is the fastest of the three — not bad for a wrapper whose entire arithmetic layer is a dozen one-line JavaScript functions.