-
Notifications
You must be signed in to change notification settings - Fork 58
JSBigInt: sub-quadratic multiply/divide/toString/fromString, interruption, and a 1 << 30 bit limit #507
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
JSBigInt: sub-quadratic multiply/divide/toString/fromString, interruption, and a 1 << 30 bit limit #507
Changes from all commits
abe2ae4
0e929ad
a78edf1
c372d1f
7dcfbce
25287d0
4d4e09b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| function test(xs, ys, count) { | ||
| let acc = 0n; | ||
| for (let i = 0; i < count; i++) { | ||
| const j = i & 7; | ||
| acc ^= xs[j] / ys[j]; | ||
| } | ||
| return acc; | ||
| } | ||
| noInline(test); | ||
|
|
||
| const DIVIDEND_DIGITS = 512; | ||
| const DIVISOR_DIGITS = 256; | ||
|
|
||
| const xs = []; | ||
| const ys = []; | ||
| let mix = 0x9e3779b97f4a7c15n; | ||
| function next(digits) { | ||
| let value = 0n; | ||
| for (let digit = 0; digit < digits; digit++) { | ||
| mix = (mix * 6364136223846793005n + 1442695040888963407n) & 0xffffffffffffffffn; | ||
| value |= mix << BigInt(64 * digit); | ||
| } | ||
| return value | (1n << BigInt(64 * digits - 1)); | ||
| } | ||
| for (let i = 0; i < 8; i++) { | ||
| xs.push(next(DIVIDEND_DIGITS)); | ||
| ys.push(next(DIVISOR_DIGITS)); | ||
| } | ||
|
|
||
| let result = 0n; | ||
| for (let i = 0; i < 10; i++) | ||
| result = test(xs, ys, 300); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| function test(strings, count) { | ||
| let acc = 0n; | ||
| for (let i = 0; i < count; i++) | ||
| acc ^= BigInt(strings[i & 7]); | ||
| return acc; | ||
| } | ||
| noInline(test); | ||
|
|
||
| const DIGITS = 256; | ||
|
|
||
| const strings = []; | ||
| let mix = 0x9e3779b97f4a7c15n; | ||
| function next() { | ||
| let value = 0n; | ||
| for (let digit = 0; digit < DIGITS; digit++) { | ||
| mix = (mix * 6364136223846793005n + 1442695040888963407n) & 0xffffffffffffffffn; | ||
| value |= mix << BigInt(64 * digit); | ||
| } | ||
| return value | (1n << BigInt(64 * DIGITS - 1)); | ||
| } | ||
| for (let i = 0; i < 8; i++) | ||
| strings.push("0x" + next().toString(16)); | ||
|
|
||
| let result = 0n; | ||
| for (let i = 0; i < 10; i++) | ||
| result = test(strings, 400); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| function test(strings, count) { | ||
| let acc = 0n; | ||
| for (let i = 0; i < count; i++) | ||
| acc ^= BigInt(strings[i & 7]); | ||
| return acc; | ||
| } | ||
| noInline(test); | ||
|
|
||
| const DIGITS = 256; | ||
|
|
||
| const strings = []; | ||
| let mix = 0x9e3779b97f4a7c15n; | ||
| function next() { | ||
| let value = 0n; | ||
| for (let digit = 0; digit < DIGITS; digit++) { | ||
| mix = (mix * 6364136223846793005n + 1442695040888963407n) & 0xffffffffffffffffn; | ||
| value |= mix << BigInt(64 * digit); | ||
| } | ||
| return value | (1n << BigInt(64 * DIGITS - 1)); | ||
| } | ||
| for (let i = 0; i < 8; i++) | ||
| strings.push(next().toString()); | ||
|
|
||
| let result = 0n; | ||
| for (let i = 0; i < 10; i++) | ||
| result = test(strings, 200); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| function test(xs, ys, count) { | ||
| let acc = 0n; | ||
| for (let i = 0; i < count; i++) { | ||
| const j = i & 7; | ||
| acc ^= xs[j] % ys[j]; | ||
| } | ||
| return acc; | ||
| } | ||
| noInline(test); | ||
|
|
||
| const DIVIDEND_DIGITS = 512; | ||
| const DIVISOR_DIGITS = 256; | ||
|
|
||
| const xs = []; | ||
| const ys = []; | ||
| let mix = 0x9e3779b97f4a7c15n; | ||
| function next(digits) { | ||
| let value = 0n; | ||
| for (let digit = 0; digit < digits; digit++) { | ||
| mix = (mix * 6364136223846793005n + 1442695040888963407n) & 0xffffffffffffffffn; | ||
| value |= mix << BigInt(64 * digit); | ||
| } | ||
| return value | (1n << BigInt(64 * digits - 1)); | ||
| } | ||
| for (let i = 0; i < 8; i++) { | ||
| xs.push(next(DIVIDEND_DIGITS)); | ||
| ys.push(next(DIVISOR_DIGITS)); | ||
| } | ||
|
|
||
| let result = 0n; | ||
| for (let i = 0; i < 10; i++) | ||
| result = test(xs, ys, 300); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| function test(xs, ys, count) { | ||
| let acc = 0n; | ||
| for (let i = 0; i < count; i++) { | ||
| const j = i & 7; | ||
| acc ^= xs[j] * ys[j]; | ||
| } | ||
| return acc; | ||
| } | ||
| noInline(test); | ||
|
|
||
| const LARGE_DIGITS = 1024; | ||
| const SMALL_DIGITS = 96; | ||
|
|
||
| const xs = []; | ||
| const ys = []; | ||
| let mix = 0x9e3779b97f4a7c15n; | ||
| function next(digits) { | ||
| let value = 0n; | ||
| for (let digit = 0; digit < digits; digit++) { | ||
| mix = (mix * 6364136223846793005n + 1442695040888963407n) & 0xffffffffffffffffn; | ||
| value |= mix << BigInt(64 * digit); | ||
| } | ||
| return value | (1n << BigInt(64 * digits - 1)); | ||
| } | ||
| for (let i = 0; i < 8; i++) { | ||
| xs.push(next(LARGE_DIGITS)); | ||
| ys.push(next(SMALL_DIGITS)); | ||
| } | ||
|
|
||
| let result = 0n; | ||
| for (let i = 0; i < 10; i++) | ||
| result = test(xs, ys, 1000); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| function test(xs, ys, count) { | ||
| let acc = 0n; | ||
| for (let i = 0; i < count; i++) { | ||
| const j = i & 7; | ||
| acc ^= xs[j] * ys[j]; | ||
| } | ||
| return acc; | ||
| } | ||
| noInline(test); | ||
|
|
||
| const DIGITS = 256; | ||
|
|
||
| const xs = []; | ||
| const ys = []; | ||
| let mix = 0x9e3779b97f4a7c15n; | ||
| function next() { | ||
| let value = 0n; | ||
| for (let digit = 0; digit < DIGITS; digit++) { | ||
| mix = (mix * 6364136223846793005n + 1442695040888963407n) & 0xffffffffffffffffn; | ||
| value |= mix << BigInt(64 * digit); | ||
| } | ||
| return value | (1n << BigInt(64 * DIGITS - 1)); | ||
| } | ||
| for (let i = 0; i < 8; i++) { | ||
| xs.push(next()); | ||
| ys.push(next()); | ||
| } | ||
|
|
||
| let result = 0n; | ||
| for (let i = 0; i < 10; i++) | ||
| result = test(xs, ys, 1000); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| function test(xs, count) { | ||
| let acc = 0; | ||
| for (let i = 0; i < count; i++) | ||
| acc += xs[i & 7].toString().length; | ||
| return acc; | ||
| } | ||
| noInline(test); | ||
|
|
||
| const DIGITS = 256; | ||
|
|
||
| const xs = []; | ||
| let mix = 0x9e3779b97f4a7c15n; | ||
| function next() { | ||
| let value = 0n; | ||
| for (let digit = 0; digit < DIGITS; digit++) { | ||
| mix = (mix * 6364136223846793005n + 1442695040888963407n) & 0xffffffffffffffffn; | ||
| value |= mix << BigInt(64 * digit); | ||
| } | ||
| return value | (1n << BigInt(64 * DIGITS - 1)); | ||
| } | ||
| for (let i = 0; i < 8; i++) | ||
| xs.push(next()); | ||
|
|
||
| let result = 0; | ||
| for (let i = 0; i < 10; i++) | ||
| result = test(xs, 100); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,7 +5,8 @@ | |
| throw new Error(message); | ||
| } | ||
|
|
||
| let a = (1n << 1048575n) - 1n; | ||
| // maxLengthBits is 1 << 30; build an all-ones value of exactly that many bits. | ||
| let a = (1n << 1073741823n) - 1n; | ||
|
Check warning on line 9 in JSTests/stress/big-int-out-of-memory-tests.js
|
||
|
Comment on lines
+8
to
+9
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 With Extended reasoning...What the issue isThree stress tests updated in this PR now allocate BigInt cells at or near the new
Step-by-step: how it manifestsTake
The same reasoning applies to the other two files. Why nothing prevents itBefore this PR each of these tests topped out at the old Why it looks like an oversightThis same PR does add memory directives to comparable tests:
So the convention is clearly known and applied elsewhere in the PR; these three appear to have been missed. ImpactTest-infrastructure only. On desktop CI nothing changes; on FixAdd one line at the top of each file, e.g.: //@ skip if $memoryLimitedFor |
||
| a = (a << 1n) | 1n; | ||
|
|
||
| try { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| //@ slow! | ||
| // Exercises the Burnikel-Ziegler and Barrett division paths around their divisor-size thresholds | ||
| // (57 and 13000 digits), with dividends of one to many divisor lengths. Each quotient and | ||
| // remainder is checked against x == q * y + r with 0 <= r < y, which relies on the multiplication | ||
| // paths but shares no division code, and the quotient of exact multiples is checked directly. | ||
|
|
||
| function shouldBe(actual, expected, message) { | ||
| if (actual !== expected) | ||
| throw new Error(`${message}: expected ${expected.toString(16).slice(0, 40)}... but got ${actual.toString(16).slice(0, 40)}...`); | ||
| } | ||
|
|
||
| function makeOperand(digits, seed, shape) { | ||
| const parts = new Array(digits); | ||
| let mix = BigInt.asUintN(64, 0x9e3779b97f4a7c15n * BigInt(seed + 1)); | ||
| for (let i = 0; i < digits; i++) { | ||
| mix = BigInt.asUintN(64, mix * 6364136223846793005n + 1442695040888963407n); | ||
| let digit; | ||
| switch (shape) { | ||
| case "random": | ||
| digit = mix; | ||
| break; | ||
| case "ones": | ||
| digit = 0xffffffffffffffffn; | ||
| break; | ||
| case "sparse": | ||
| digit = (i * 7 + seed) % 5 === 0 ? mix : 0n; | ||
| break; | ||
| case "top": | ||
| // Only the top digit is set, with its high bit, so the divisor needs no normalization | ||
| // shift and the dividend's top block is maximal. | ||
| digit = i ? 0n : 0x8000000000000000n; | ||
| break; | ||
| case "low": | ||
| // A small top digit forces the largest normalization shift. | ||
| digit = i ? mix : 1n; | ||
| break; | ||
| } | ||
| parts[i] = digit.toString(16).padStart(16, "0"); | ||
| } | ||
| if (shape === "random" || shape === "sparse") | ||
| parts[0] = "8" + parts[0].slice(1); | ||
| return BigInt("0x" + parts.join("")); | ||
| } | ||
|
|
||
| function check(x, y, message) { | ||
| const q = x / y; | ||
| const r = x % y; | ||
| if (r < 0n || r >= y) | ||
| throw new Error(`${message}: remainder out of range`); | ||
| shouldBe(q * y + r, x, `${message} identity`); | ||
| shouldBe((-x) / (-y), q, `${message} negative operands quotient`); | ||
| shouldBe((-x) % y, -r, `${message} negative dividend remainder`); | ||
| } | ||
|
|
||
| const shapes = ["random", "ones", "sparse", "top", "low"]; | ||
|
|
||
| // Divisor sizes around the Burnikel-Ziegler threshold and its power-of-two block rounding, with | ||
| // dividends from one digit longer up to many blocks. | ||
| for (const divisorSize of [56, 57, 58, 113, 114, 115, 127, 128, 129, 228, 229, 456, 457]) { | ||
| for (const extra of [1, 2, 57, 58, 114, 115, 500]) { | ||
| const dividendSize = divisorSize + extra; | ||
| for (const shape of shapes) { | ||
| const x = makeOperand(dividendSize, dividendSize, shape); | ||
| const y = makeOperand(divisorSize, divisorSize * 3 + 1, shapes[(shapes.indexOf(shape) + 1) % shapes.length]); | ||
| check(x, y, `${dividendSize} / ${divisorSize} ${shape}`); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Divisor sizes around the Barrett threshold, where the dividend is at most twice the divisor, | ||
| // exactly twice, and chunked beyond that. | ||
| for (const [divisorSize, extra, shape] of [[12999, 13000, "random"], [13000, 1, "low"], [13000, 13001, "ones"], [13001, 27000, "random"]]) { | ||
| const dividendSize = divisorSize + extra; | ||
| const x = makeOperand(dividendSize, dividendSize, shape); | ||
| const y = makeOperand(divisorSize, divisorSize * 3 + 1, shapes[(shapes.indexOf(shape) + 2) % shapes.length]); | ||
| check(x, y, `${dividendSize} / ${divisorSize} ${shape}`); | ||
| } | ||
|
|
||
| // Exact multiples, and the remainders 1 and y - 1, with quotients of various sizes. | ||
| for (const divisorSize of [57, 128, 13001]) { | ||
| const y = makeOperand(divisorSize, divisorSize, "random"); | ||
| for (const quotientSize of [1, 2, 57, 300]) { | ||
| const q = makeOperand(quotientSize, quotientSize * 7, "sparse"); | ||
| for (const r of [0n, 1n, y - 1n]) { | ||
| const x = q * y + r; | ||
| shouldBe(x / y, q, `${quotientSize} x ${divisorSize} + ${r === 0n ? "0" : r === 1n ? "1" : "y - 1"} quotient`); | ||
| shouldBe(x % y, r, `${quotientSize} x ${divisorSize} + ${r === 0n ? "0" : r === 1n ? "1" : "y - 1"} remainder`); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Powers of two as divisors and dividends. | ||
| for (const bits of [64 * 57, 64 * 1000 + 1]) { | ||
| const p = 1n << BigInt(bits); | ||
| const x = makeOperand(Math.ceil(bits / 64) * 2 + 3, bits, "random"); | ||
| shouldBe(x / p, x >> BigInt(bits), `${bits} bit power of two divisor`); | ||
| shouldBe(x % p, x & (p - 1n), `${bits} bit power of two remainder`); | ||
| shouldBe(x / (p - 1n) * (p - 1n) + x % (p - 1n), x, `${bits} bit all ones divisor`); | ||
| shouldBe((p * p) / p, p, `${bits} bit power of two dividend`); | ||
| shouldBe((p * p - 1n) / p, p - 1n, `${bits} bit all ones dividend`); | ||
| shouldBe((p * p - 1n) % p, p - 1n, `${bits} bit all ones dividend remainder`); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Cover the actual maximum-length value.
Line 9 constructs a value with 1,073,741,823 bits. It does not construct the claimed
1 << 30-bit value. Build the all-ones value from operands that remain within the allowed boundary.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents