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.
This commit is contained in:
Jess 2025-03-19 10:31:39 +13:00 committed by Jelle Raaijmakers
parent 92d0cd3c7c
commit 12cbefbee7
Notes: github-actions[bot] 2025-03-20 08:45:14 +00:00
9 changed files with 110 additions and 33 deletions

View file

@ -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<void> 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<UnsignedBigInteger::Word>::max() >> (UnsignedBigInteger::BITS_IN_WORD - index)) & ~last_word;
return {};
}
FLATTEN void UnsignedBigIntegerAlgorithms::shift_left_without_allocation(

View file

@ -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<void> bitwise_not_fill_to_one_based_index_without_allocation(UnsignedBigInteger const& left, size_t, UnsignedBigInteger& output);
static ErrorOr<void> 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);

View file

@ -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> 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;

View file

@ -120,6 +120,7 @@ public:
[[nodiscard]] SignedBigInteger multiplied_by(SignedBigInteger const& other) const;
[[nodiscard]] SignedDivisionResult divided_by(SignedBigInteger const& divisor) const;
[[nodiscard]] ErrorOr<SignedBigInteger> mod_power_of_two(size_t power_of_two) const;
[[nodiscard]] ErrorOr<SignedBigInteger> try_shift_left(size_t num_bits) const;
[[nodiscard]] SignedBigInteger plus(UnsignedBigInteger const& other) const;

View file

@ -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> 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;

View file

@ -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<UnsignedBigInteger> try_bitwise_not_fill_to_one_based_index(size_t) const;
[[nodiscard]] ErrorOr<UnsignedBigInteger> try_shift_left(size_t num_bits) const;
[[nodiscard]] u32 hash() const;

View file

@ -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<size_t>::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);
}
}

View file

@ -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");
});
});

View file

@ -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");
});
});