From 4b51ec680f1f99eef839cb1d1ab228bf3f27e86e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 26 Jul 2026 07:59:46 +0000 Subject: [PATCH] JSBigInt::parseInt: O(n) fast path for power-of-two radix The generic parse loop calls multiplyAdd over the full preallocated digit vector for every lengthLimitForBigInt32-sized group of input characters (n/k groups times O(n) multiplyAdd = O(n^2)). For a power-of-two radix (0b/0o/0x string prefixes, and literal parsing for radix 2/4/8/16/32) no multiplication is needed at all: each character is a fixed number of bits that can be packed directly into the digit vector. This mirrors toStringBasePowerOfTwo, which is already linear in the output direction. 200k hex chars: ~360ms -> ~1ms (release), ~17s -> ~6ms (debug+ASAN). Short inputs that fit in an int32 fall through to the existing path so they keep producing BigInt32 where applicable. --- Source/JavaScriptCore/runtime/JSBigInt.cpp | 58 ++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/Source/JavaScriptCore/runtime/JSBigInt.cpp b/Source/JavaScriptCore/runtime/JSBigInt.cpp index c0aeb7213a3e..1c763404e47f 100644 --- a/Source/JavaScriptCore/runtime/JSBigInt.cpp +++ b/Source/JavaScriptCore/runtime/JSBigInt.cpp @@ -3605,6 +3605,64 @@ JSValue JSBigInt::parseInt(JSGlobalObject* nullOrGlobalObjectForOOM, VM& vm, std unsigned limitA = 'A' + (static_cast(radix) - 10); unsigned initialLength = length - p; Vector resultVector; + +#if USE(BUN_JSC_ADDITIONS) + // Fast path for power-of-two radixes (2, 4, 8, 16, 32): pack characters + // directly into the digit vector. The generic path below calls multiplyAdd + // over the full-length digit vector for every lengthLimitForBigInt32-sized + // group of characters, which is O(n^2). This path is O(n) and mirrors + // toStringBasePowerOfTwo in the other direction. + // Short inputs that fit in a single int32 fall through to the generic path + // so they keep returning BigInt32 where applicable. + if (hasOneBitSet(radix) && initialLength > lengthLimitForBigInt32) { + const unsigned bitsPerChar = ctz(radix); + uint64_t totalBits = static_cast(initialLength) * bitsPerChar; + uint64_t numDigits = (totalBits + digitBits - 1) / digitBits; + if (numDigits > maxLength) [[unlikely]] { + if (nullOrGlobalObjectForOOM) { + auto scope = DECLARE_THROW_SCOPE(vm); + throwOutOfMemoryError(nullOrGlobalObjectForOOM, scope, "BigInt generated from this operation is too big"_s); + } + return JSValue(); + } + resultVector.fill(0, static_cast(numDigits)); + + Digit current = 0; + unsigned bitsInDigit = 0; + unsigned digitIndex = 0; + for (size_t i = length; i > p;) { + --i; + Digit c; + if (data[i] >= '0' && data[i] < limit0) + c = static_cast(data[i] - '0'); + else if (data[i] >= 'a' && data[i] < limita) + c = static_cast(data[i] - 'a' + 10); + else if (data[i] >= 'A' && data[i] < limitA) + c = static_cast(data[i] - 'A' + 10); + else { + if (errorParseMode == ErrorParseMode::ThrowExceptions) { + auto scope = DECLARE_THROW_SCOPE(vm); + ASSERT(nullOrGlobalObjectForOOM); + throwVMError(nullOrGlobalObjectForOOM, scope, createSyntaxError(nullOrGlobalObjectForOOM, "Failed to parse String to BigInt"_s)); + } + return JSValue(); + } + current |= c << bitsInDigit; + bitsInDigit += bitsPerChar; + if (bitsInDigit >= digitBits) { + resultVector[digitIndex++] = current; + bitsInDigit -= digitBits; + current = bitsInDigit ? c >> (bitsPerChar - bitsInDigit) : 0; + } + } + if (bitsInDigit) + resultVector[digitIndex++] = current; + ASSERT(digitIndex <= resultVector.size()); + + return tryCreateFromImpl(nullOrGlobalObjectForOOM, vm, sign == ParseIntSign::Signed, resultVector.span()); + } +#endif + while (p < length) { Checked digit = 0; Checked multiplier = 1;