Jakob Kummerow,
Thank you for responding back so quickly. I apologize for my delay. Thank you for being so polite to me and thank you so much for point out such good points about my suggestions. Reading your answer in retrospective, it does appear that I came across as an unthinking ignoramus who likes to flaunt his option of things he know little about. In this response, I hope that I can rectify the situation and shatter the negative image I unintentionally built.
As for exp on BigInts, I am designing a library I am going to call BigMath. (Uggggghhhh.... library, right? Something that is going to needlessly bloat the download size by several hundred megabytes?!?! Well, I hate libraries like that, so my library will encourage users to use its piecemeal versions to avoid those problems.) It will feature BigInt versions of the ordinary Math functions, and errors will be thrown to handle Infinity and NaN cases. My hope is that eventually, maybe, BigMath might make it into the specification so that everyone can enjoy the benefits of a Math library so that coders everywhere do not have to frequently reinvent the wheel with their BigInt code. I am not yet finished with my
exp function because I am working out how to approximate the convergence of continued fractions, but I have finished all the BigInt stuff that the exp function will work with. See my convergence benchmark on PasteBin. I hope that it gives you a good sense for the way people are using BigInts.
You are absolutely correct about your first point, and I suggest that we copy at the first location: assignment.
let backup = x; // copy here
x += 15n; // assignment here
console.assert(backup === 42n); // This will *now* work properly
I strongly believe that copying at variable assignment is much more performant because you never see any (smart) ones needlessly copy variables. JavaScript coders copy variables to manipulate them. If the variable is not copied at assignment, then it would have to be copied where it is used in a nearby mathematical expression. Thus, there is no extra penalty incurred by copying the variables at the location of assignment because those variables are going to be used anyway. Infact, with my suggestion for mutable-operations, there will be much performance gain because, after the variable is copied for assignment to another variable, no more copying is needed for whatever successive math follows in the code. The only problem I can think of is function arguments. The preceding paragraph is contingent upon function argument optimizations, as explained in the next paragraph, otherwise I entirely agree with you about the copy-at-assignment being less performant.
function pow2PlusPow3(inBgN) {
var x = inBgN | 0n; // one option is to optimize for `|0n`, `>>0n`, and `<<0n` indicating that inBgN must be a BigInt, else an error is thrown
var y = x;
x *= y *= x; // example operation that might be done
return x + y;
}
There's one critical performance problem I can think of with copying at assignment: function arguments. In order to dumbly copy at assignment, we would also have to dumbly copy BigInts passed as arguments to functions. Thus, for there to be a real performance gain from BigInts copied at assignment, we would have to get smart with passing BigInts to function. My limited understanding is that there are already special optimizations that optimize function calls such that functions which modify their arguments must extend the stack further to create additional references variables to hold the new values of the arguments. If this is the case, then perhaps you could modify the code handling those optimizations to include checks for BigInts such that, when BigInts passed as an argument are copied into a new variable due to the argument to the function being modified inside the function call, the BigInt variable's value is copied into a new BigInt. For example, in the pow2PlusPow3 function above, inBgN is immediately assigned to a variable, copying it right there. In the example below, there would never be any need to copy bgN.
function factorial(inBgN) {
var result = 1n;
for (var i=2n; i<inBgN; i++) result *= i;
return result;
}
I want to clarify that I am not expecting you to work magic here. Rather, I am just posing a suggestion and am wondering if you think it could be practical to implement. I value your knowledge and opinion very highly as you know a lot more about the Chromium engine than I do and you are an intelligent kind person.
For transformations of division into bit-shifts, I have a solution to the problem of negative numbers: add one to the sign if it's a negative number. I strummed up the following slow crummy bad code to confirm that (any negative number plus 1n)>>1n is the same as (any negative number) / 2n.
for (var i=-0b10101n; i<0; i++)
if (BigInt.asUintN(8, (i+1n) >> 1n) !== BigInt.asUintN(8, i / 2n))
Also, I would like to clarify the extent of this optimization because I failed to do that previously. It would be extremely unlikely to divide by a variable containing two in most circumstances, so putting this optimization into the division method would likely slow it down more than it improves it. Instead, all I suggest for this optimization is that it be applied to brain-dead cases where the variable is being divided by a constant instead of a variable. I can guarantee that there are programmers out there without a care in the world for performance who will use division all over their code and no bitshifts. So, I do believe that these optimizations are crucial for performance because I can only imagine how slow the BigInt division must be.
const dividedN = 50n / divisor; // does not get optimized into bitshiftsconst another = divisor * 32n; // does indeed get optimized into `(divisor < 0n ? divisor+1n : divisor) << 32n`const remainder = another % 16n; // does indeed get optimized into `(another < 0n ? another+1n : another) & 15n `const remainder = 2n ** divisor; // does indeed get optimized into `if (divisor >= 0n) remainder = 2n << divisor; else throw Error("…");`
const dividedN = divisor ** 7n; // does not get optimized into bitshifts
As for optimizing bitwise shifts into memory access, it's the same deal: purely a compile-time optimization and not a run-time optimization. You are absolutely correct that trying to do all the necessary checks at runtime would result in a general slowdown. However, doing them at compile-time would, in many cases, result in a general speedup. Observe.
const hugeNumber = 11n ** 27n ** 3n;
const intAt65536thByte = (hugeNumber >> 65536n) & 4294967295n; // is indeed optimized into a single memory array access at compile-time
const randShift = 40820n;
const intAtRandomShiftInt = (hugeNumber >> randShift) & 4294967295n; // partially optimized into either one or two memory array accesses at compile-time
const varBitMask = 2n ** 64n - 1n;
const sizedBitAreaAtRandShift = (hugeNumber >> randShift) & varBitMask; // does not get optimized into array access
const sizedBitAreaAt32768Bits = (hugeNumber >> 32768n) & varBitMask; // does not get optimized into array access
The speed-up from this optimization would be tremendous because, instead of shifting down the bits into a new BigInt AND THEN performing a bitwise-and to yield a second copy, there needs only to be a single copy operation into a known-size BigInt from a specific offset in memory.
As for the conversion between BigInts and Uint8Array/Uint32Array/ArrayBuffer, I have my own controversial thoughts on the matter. In the majority of cases where it would be most helpful to edit the underlying bits of the BigInt, only a small handful of bits would be modified. Thus, copying the BigInt into a new Uint8Array/Uint32Array/ArrayBuffer would do little good performance-wise and people would be forced to use ordinary means of manipulating BigInts with math operators. Something that would be massively helpful to performance is a true view of the real data and not just a copy. What I am referring to is a live typed ArrayBuffer view of the BigInt such that edits to the ArrayBuffer are directly reflected on the BigInt without having to needlessly copy any data. This would allow for lickety-fast operations on BigInts and would be a god-sent to the performance-oriented niche of programmers like me.
var bigNum = 12345n ** 12345n;
As for the bit-masks, I completely agree that they do not add any performance, but that's not their purpose. The purpose of the bit-masks is to allow for the compiler to make more optimistic assertions with more confidence once the compiler is properly trained to utilize them. Observe the example below.
BigInt.asIntN = function(){};
It's legitimate specs-compliant JavaScript that the compiler must anticipate and be ready for because JavaScript sometimes stinks. Bitmasks, on the other hand, are as constant as the North Star. The compiler is able to make assertions about these bit masks yielding 64-bit unsigned integers with 100% confidence, meaning 0 overhead and completely unobstructed native 64-bit unsigned integers for lickety-fast code (if the compiler is trained on how to utilize them). I am arguing for bitmasks not for performance, instead because I believe that it will be easier to do more better optimizations with them.
As for the reference counting, I am merely suggesting changes mostly localized to the BigInt file. Instead of sprinkling reference counting code all over the Chromium engine, all my suggestion is is to sprinkle reference counting code all over the single BigInt file. BigInts would have their own internal system of pointer counting that only code in the BigInt file would understand and code elsewhere would consider part of the BigInt payload. That being said, I think you are absolutely right that, as huge of an optimization as this could be, it would be a massive undertaking and is, if ever, several years/decades/millenia away from being attempted.
Thank you so much for all of your insights into my suggestions. I hope that my commentary in response to your clever points will be beneficial to the project. I look forward to your response. Thank you so much!
Sincerely,
Jack Giffin