From 12cbefbee7b08965788dab375de6b19018059c77 Mon Sep 17 00:00:00 2001 From: Jess Date: Wed, 19 Mar 2025 10:31:39 +1300 Subject: [PATCH] LibJS+LibCrypto: Use a bitwise approach for BigInt's as*IntN methods This speeds up expressions such as `BigInt.asIntN(0x4000000000000, 1n)` (#3615). And those involving very large bigints. --- .../BigInt/Algorithms/BitwiseOperations.cpp | 10 +++--- .../Algorithms/UnsignedBigIntegerAlgorithms.h | 2 +- .../LibCrypto/BigInt/SignedBigInteger.cpp | 11 ++++++ Libraries/LibCrypto/BigInt/SignedBigInteger.h | 1 + .../LibCrypto/BigInt/UnsignedBigInteger.cpp | 34 +++++++++++++++++- .../LibCrypto/BigInt/UnsignedBigInteger.h | 2 ++ Libraries/LibJS/Runtime/BigIntConstructor.cpp | 36 +++++++++++-------- .../Tests/builtins/BigInt/BigInt.asIntN.js | 25 +++++++++---- .../Tests/builtins/BigInt/BigInt.asUintN.js | 22 ++++++++---- 9 files changed, 110 insertions(+), 33 deletions(-) diff --git a/Libraries/LibCrypto/BigInt/Algorithms/BitwiseOperations.cpp b/Libraries/LibCrypto/BigInt/Algorithms/BitwiseOperations.cpp index 5d240970959..87ffab03e7b 100644 --- a/Libraries/LibCrypto/BigInt/Algorithms/BitwiseOperations.cpp +++ b/Libraries/LibCrypto/BigInt/Algorithms/BitwiseOperations.cpp @@ -131,7 +131,7 @@ FLATTEN void UnsignedBigIntegerAlgorithms::bitwise_xor_without_allocation( /** * Complexity: O(N) where N is the number of words */ -FLATTEN void UnsignedBigIntegerAlgorithms::bitwise_not_fill_to_one_based_index_without_allocation( +FLATTEN ErrorOr UnsignedBigIntegerAlgorithms::bitwise_not_fill_to_one_based_index_without_allocation( UnsignedBigInteger const& right, size_t index, UnsignedBigInteger& output) @@ -139,16 +139,16 @@ FLATTEN void UnsignedBigIntegerAlgorithms::bitwise_not_fill_to_one_based_index_w // If the value is invalid, the output value is invalid as well. if (right.is_invalid()) { output.invalidate(); - return; + return {}; } if (index == 0) { output.set_to_0(); - return; + return {}; } size_t size = (index + UnsignedBigInteger::BITS_IN_WORD - 1) / UnsignedBigInteger::BITS_IN_WORD; - output.m_words.resize_and_keep_capacity(size); + TRY(output.m_words.try_resize_and_keep_capacity(size)); VERIFY(size > 0); for (size_t i = 0; i < size - 1; ++i) output.m_words[i] = ~(i < right.length() ? right.words()[i] : 0); @@ -158,6 +158,8 @@ FLATTEN void UnsignedBigIntegerAlgorithms::bitwise_not_fill_to_one_based_index_w auto last_word = last_word_index < right.length() ? right.words()[last_word_index] : 0; output.m_words[last_word_index] = (NumericLimits::max() >> (UnsignedBigInteger::BITS_IN_WORD - index)) & ~last_word; + + return {}; } FLATTEN void UnsignedBigIntegerAlgorithms::shift_left_without_allocation( diff --git a/Libraries/LibCrypto/BigInt/Algorithms/UnsignedBigIntegerAlgorithms.h b/Libraries/LibCrypto/BigInt/Algorithms/UnsignedBigIntegerAlgorithms.h index bd166c807c7..5536243e6ec 100644 --- a/Libraries/LibCrypto/BigInt/Algorithms/UnsignedBigIntegerAlgorithms.h +++ b/Libraries/LibCrypto/BigInt/Algorithms/UnsignedBigIntegerAlgorithms.h @@ -20,13 +20,13 @@ public: static void bitwise_or_without_allocation(UnsignedBigInteger const& left, UnsignedBigInteger const& right, UnsignedBigInteger& output); static void bitwise_and_without_allocation(UnsignedBigInteger const& left, UnsignedBigInteger const& right, UnsignedBigInteger& output); static void bitwise_xor_without_allocation(UnsignedBigInteger const& left, UnsignedBigInteger const& right, UnsignedBigInteger& output); - static void bitwise_not_fill_to_one_based_index_without_allocation(UnsignedBigInteger const& left, size_t, UnsignedBigInteger& output); static void shift_left_without_allocation(UnsignedBigInteger const& number, size_t bits_to_shift_by, UnsignedBigInteger& temp_result, UnsignedBigInteger& temp_plus, UnsignedBigInteger& output); static void shift_right_without_allocation(UnsignedBigInteger const& number, size_t num_bits, UnsignedBigInteger& output); static void multiply_without_allocation(UnsignedBigInteger const& left, UnsignedBigInteger const& right, UnsignedBigInteger& temp_shift_result, UnsignedBigInteger& temp_shift_plus, UnsignedBigInteger& temp_shift, UnsignedBigInteger& output); static void divide_without_allocation(UnsignedBigInteger const& numerator, UnsignedBigInteger const& denominator, UnsignedBigInteger& quotient, UnsignedBigInteger& remainder); static void divide_u16_without_allocation(UnsignedBigInteger const& numerator, UnsignedBigInteger::Word denominator, UnsignedBigInteger& quotient, UnsignedBigInteger& remainder); + static ErrorOr bitwise_not_fill_to_one_based_index_without_allocation(UnsignedBigInteger const& left, size_t, UnsignedBigInteger& output); static ErrorOr try_shift_left_without_allocation(UnsignedBigInteger const& number, size_t bits_to_shift_by, UnsignedBigInteger& temp_result, UnsignedBigInteger& temp_plus, UnsignedBigInteger& output); static void extended_GCD_without_allocation(UnsignedBigInteger const& a, UnsignedBigInteger const& b, UnsignedBigInteger& x, UnsignedBigInteger& y, UnsignedBigInteger& gcd, UnsignedBigInteger& temp_quotient, UnsignedBigInteger& temp_1, UnsignedBigInteger& temp_2, UnsignedBigInteger& temp_shift_result, UnsignedBigInteger& temp_shift_plus, UnsignedBigInteger& temp_shift, UnsignedBigInteger& temp_r, UnsignedBigInteger& temp_s, UnsignedBigInteger& temp_t); diff --git a/Libraries/LibCrypto/BigInt/SignedBigInteger.cpp b/Libraries/LibCrypto/BigInt/SignedBigInteger.cpp index f1f89069c05..1edba05336a 100644 --- a/Libraries/LibCrypto/BigInt/SignedBigInteger.cpp +++ b/Libraries/LibCrypto/BigInt/SignedBigInteger.cpp @@ -291,6 +291,17 @@ FLATTEN SignedBigInteger SignedBigInteger::shift_right(size_t num_bits) const return SignedBigInteger { m_unsigned_data.shift_right(num_bits), m_sign }; } +FLATTEN ErrorOr SignedBigInteger::mod_power_of_two(size_t power_of_two) const +{ + auto const lower_bits = m_unsigned_data.as_n_bits(power_of_two); + + if (is_positive()) + return SignedBigInteger(lower_bits); + + // twos encode lower bits + return SignedBigInteger(TRY(lower_bits.try_bitwise_not_fill_to_one_based_index(power_of_two)).plus(1).as_n_bits(power_of_two)); +} + FLATTEN SignedBigInteger SignedBigInteger::multiplied_by(SignedBigInteger const& other) const { bool result_sign = m_sign ^ other.m_sign; diff --git a/Libraries/LibCrypto/BigInt/SignedBigInteger.h b/Libraries/LibCrypto/BigInt/SignedBigInteger.h index 73771567774..75203588257 100644 --- a/Libraries/LibCrypto/BigInt/SignedBigInteger.h +++ b/Libraries/LibCrypto/BigInt/SignedBigInteger.h @@ -120,6 +120,7 @@ public: [[nodiscard]] SignedBigInteger multiplied_by(SignedBigInteger const& other) const; [[nodiscard]] SignedDivisionResult divided_by(SignedBigInteger const& divisor) const; + [[nodiscard]] ErrorOr mod_power_of_two(size_t power_of_two) const; [[nodiscard]] ErrorOr try_shift_left(size_t num_bits) const; [[nodiscard]] SignedBigInteger plus(UnsignedBigInteger const& other) const; diff --git a/Libraries/LibCrypto/BigInt/UnsignedBigInteger.cpp b/Libraries/LibCrypto/BigInt/UnsignedBigInteger.cpp index 73054427599..b556c5936f5 100644 --- a/Libraries/LibCrypto/BigInt/UnsignedBigInteger.cpp +++ b/Libraries/LibCrypto/BigInt/UnsignedBigInteger.cpp @@ -473,10 +473,15 @@ FLATTEN UnsignedBigInteger UnsignedBigInteger::bitwise_xor(UnsignedBigInteger co } FLATTEN UnsignedBigInteger UnsignedBigInteger::bitwise_not_fill_to_one_based_index(size_t size) const +{ + return MUST(try_bitwise_not_fill_to_one_based_index(size)); +} + +FLATTEN ErrorOr UnsignedBigInteger::try_bitwise_not_fill_to_one_based_index(size_t size) const { UnsignedBigInteger result; - UnsignedBigIntegerAlgorithms::bitwise_not_fill_to_one_based_index_without_allocation(*this, size, result); + TRY(UnsignedBigIntegerAlgorithms::bitwise_not_fill_to_one_based_index_without_allocation(*this, size, result)); return result; } @@ -506,6 +511,33 @@ FLATTEN UnsignedBigInteger UnsignedBigInteger::shift_right(size_t num_bits) cons return output; } +FLATTEN UnsignedBigInteger UnsignedBigInteger::as_n_bits(size_t n) const +{ + if (auto const num_bits = one_based_index_of_highest_set_bit(); n >= num_bits) + return *this; + + UnsignedBigInteger output; + output.set_to(*this); + + auto const word_index = n / BITS_IN_WORD; + + auto const bits_to_keep = n % BITS_IN_WORD; + auto const bits_to_discard = BITS_IN_WORD - bits_to_keep; + + output.m_words.resize(word_index + 1); + + auto const last_word = output.m_words[word_index]; + Word new_last_word = 0; + + // avoid UB from a 32 bit shift on a u32 + if (bits_to_keep != 0) + new_last_word = last_word << bits_to_discard >> bits_to_discard; + + output.m_words[word_index] = new_last_word; + + return output; +} + FLATTEN UnsignedBigInteger UnsignedBigInteger::multiplied_by(UnsignedBigInteger const& other) const { UnsignedBigInteger result; diff --git a/Libraries/LibCrypto/BigInt/UnsignedBigInteger.h b/Libraries/LibCrypto/BigInt/UnsignedBigInteger.h index 0e354a51161..7a9997f18b6 100644 --- a/Libraries/LibCrypto/BigInt/UnsignedBigInteger.h +++ b/Libraries/LibCrypto/BigInt/UnsignedBigInteger.h @@ -117,9 +117,11 @@ public: [[nodiscard]] UnsignedBigInteger bitwise_not_fill_to_one_based_index(size_t) const; [[nodiscard]] UnsignedBigInteger shift_left(size_t num_bits) const; [[nodiscard]] UnsignedBigInteger shift_right(size_t num_bits) const; + [[nodiscard]] UnsignedBigInteger as_n_bits(size_t n) const; [[nodiscard]] UnsignedBigInteger multiplied_by(UnsignedBigInteger const& other) const; [[nodiscard]] UnsignedDivisionResult divided_by(UnsignedBigInteger const& divisor) const; + [[nodiscard]] ErrorOr try_bitwise_not_fill_to_one_based_index(size_t) const; [[nodiscard]] ErrorOr try_shift_left(size_t num_bits) const; [[nodiscard]] u32 hash() const; diff --git a/Libraries/LibJS/Runtime/BigIntConstructor.cpp b/Libraries/LibJS/Runtime/BigIntConstructor.cpp index 07f81a009e1..5ebea4b6873 100644 --- a/Libraries/LibJS/Runtime/BigIntConstructor.cpp +++ b/Libraries/LibJS/Runtime/BigIntConstructor.cpp @@ -18,6 +18,7 @@ namespace JS { GC_DEFINE_ALLOCATOR(BigIntConstructor); static Crypto::SignedBigInteger const BIGINT_ONE { 1 }; +static Crypto::SignedBigInteger const BIGINT_ZERO { 0 }; BigIntConstructor::BigIntConstructor(Realm& realm) : NativeFunction(realm.vm().names.BigInt.as_string(), realm.intrinsics().function_prototype()) @@ -72,20 +73,25 @@ JS_DEFINE_NATIVE_FUNCTION(BigIntConstructor::as_int_n) // 2. Set bigint to ? ToBigInt(bigint). auto bigint = TRY(vm.argument(1).to_bigint(vm)); + // OPTIMIZATION: mod = bigint (mod 2^0) = 0 < 2^(0-1) = 0.5 + if (bits == 0) + return BigInt::create(vm, BIGINT_ZERO); + // 3. Let mod be ℝ(bigint) modulo 2^bits. - // FIXME: For large values of `bits`, this can likely be improved with a SignedBigInteger API to - // drop the most significant bits. - auto bits_shift_left = TRY_OR_THROW_OOM(vm, BIGINT_ONE.try_shift_left(bits)); - auto mod = modulo(bigint->big_integer(), bits_shift_left); + auto const mod = TRY_OR_THROW_OOM(vm, bigint->big_integer().mod_power_of_two(bits)); - // 4. If mod ≥ 2^(bits-1), return ℤ(mod - 2^bits); otherwise, return ℤ(mod). - // NOTE: Some of the below conditionals are non-standard, but are to protect SignedBigInteger from - // allocating an absurd amount of memory if `bits - 1` overflows to NumericLimits::max. - if ((bits == 0) && (mod >= BIGINT_ONE)) - return BigInt::create(vm, mod.minus(bits_shift_left)); - if ((bits > 0) && (mod >= BIGINT_ONE.shift_left(bits - 1))) - return BigInt::create(vm, mod.minus(bits_shift_left)); + // OPTIMIZATION: mod < 2^(bits-1) + if (mod.is_zero()) + return BigInt::create(vm, BIGINT_ZERO); + // 4. If mod ≥ 2^(bits-1), return ℤ(mod - 2^bits); ... + if (auto top_bit_index = mod.unsigned_value().one_based_index_of_highest_set_bit(); top_bit_index >= bits) { + // twos complement decode + auto decoded = TRY_OR_THROW_OOM(vm, mod.unsigned_value().try_bitwise_not_fill_to_one_based_index(bits)).plus(1); + return BigInt::create(vm, Crypto::SignedBigInteger { std::move(decoded), true }); + } + + // ... otherwise, return ℤ(mod). return BigInt::create(vm, mod); } @@ -98,10 +104,10 @@ JS_DEFINE_NATIVE_FUNCTION(BigIntConstructor::as_uint_n) // 2. Set bigint to ? ToBigInt(bigint). auto bigint = TRY(vm.argument(1).to_bigint(vm)); - // 3. Return the BigInt value that represents ℝ(bigint) modulo 2bits. - // FIXME: For large values of `bits`, this can likely be improved with a SignedBigInteger API to - // drop the most significant bits. - return BigInt::create(vm, modulo(bigint->big_integer(), TRY_OR_THROW_OOM(vm, BIGINT_ONE.try_shift_left(bits)))); + // 3. Return the BigInt value that represents ℝ(bigint) modulo 2^bits. + auto const mod = TRY_OR_THROW_OOM(vm, bigint->big_integer().mod_power_of_two(bits)); + + return BigInt::create(vm, mod); } } diff --git a/Libraries/LibJS/Tests/builtins/BigInt/BigInt.asIntN.js b/Libraries/LibJS/Tests/builtins/BigInt/BigInt.asIntN.js index 4bc14a14b0b..eb4424a0b61 100644 --- a/Libraries/LibJS/Tests/builtins/BigInt/BigInt.asIntN.js +++ b/Libraries/LibJS/Tests/builtins/BigInt/BigInt.asIntN.js @@ -22,12 +22,6 @@ describe("errors", () => { BigInt.asIntN(1, "foo"); }).toThrowWithMessage(SyntaxError, "Invalid value for BigInt: foo"); }); - - test("large allocation", () => { - expect(() => { - BigInt.asIntN(0x4000000000000, 1n); - }).toThrowWithMessage(InternalError, "Out of memory"); - }); }); describe("correct behavior", () => { @@ -82,4 +76,23 @@ describe("correct behavior", () => { expect(BigInt.asIntN(128, -extremelyBigInt)).toBe(99061374399389259395070030194384019691n); expect(BigInt.asIntN(256, -extremelyBigInt)).toBe(-extremelyBigInt); }); + + test("large bit values", () => { + expect(BigInt.asIntN(0x4000000000000, 1n)).toBe(1n); + expect(BigInt.asIntN(0x8ffffffffffff, 1n)).toBe(1n); + expect(BigInt.asIntN(2 ** 53 - 1, 2n)).toBe(2n); + + // These incur large intermediate values that 00M. For now, ensure they don't crash + expect(() => { + BigInt.asIntN(0x4000000000000, -1n); + }).toThrowWithMessage(InternalError, "Out of memory"); + + expect(() => { + BigInt.asIntN(0x8ffffffffffff, -1n); + }).toThrowWithMessage(InternalError, "Out of memory"); + + expect(() => { + BigInt.asIntN(2 ** 53 - 1, -2n); + }).toThrowWithMessage(InternalError, "Out of memory"); + }); }); diff --git a/Libraries/LibJS/Tests/builtins/BigInt/BigInt.asUintN.js b/Libraries/LibJS/Tests/builtins/BigInt/BigInt.asUintN.js index 059d575f557..1d8252f0e96 100644 --- a/Libraries/LibJS/Tests/builtins/BigInt/BigInt.asUintN.js +++ b/Libraries/LibJS/Tests/builtins/BigInt/BigInt.asUintN.js @@ -22,12 +22,6 @@ describe("errors", () => { BigInt.asUintN(1, "foo"); }).toThrowWithMessage(SyntaxError, "Invalid value for BigInt: foo"); }); - - test("large allocation", () => { - expect(() => { - BigInt.asUintN(0x4000000000000, 1n); - }).toThrowWithMessage(InternalError, "Out of memory"); - }); }); describe("correct behavior", () => { @@ -80,4 +74,20 @@ describe("correct behavior", () => { 115792089237316195423570861551898784396480861208851440582668460551124006183147n ); }); + + test("large bit values", () => { + const INDEX_MAX = 2 ** 53 - 1; + const LAST_8_DIGITS = 10n ** 8n; + + expect(BigInt.asUintN(0x400000000, 1n)).toBe(1n); + expect(BigInt.asUintN(0x4000, -1n) % LAST_8_DIGITS).toBe(64066815n); + + expect(BigInt.asUintN(0x400000000, 2n)).toBe(2n); + expect(BigInt.asUintN(0x4000, -2n) % LAST_8_DIGITS).toBe(64066814n); + + expect(BigInt.asUintN(INDEX_MAX, 2n)).toBe(2n); + expect(() => { + BigInt.asUintN(INDEX_MAX, -2n); + }).toThrowWithMessage(InternalError, "Out of memory"); + }); });