BigInt Shipping in Firefox
wingolog.org
wingolog.org
There are important real use cases for this. I authored an implementation of the FNV64 hash function[1] at my last job. I needed to use the BigInteger.js[2] library. It truly surprised me that Javascript in 2018 did not support something so simple as 64-bit integers.
[0]: https://tc39.github.io/proposal-bigint/
The weirdness of WebAssembly with regards to control flow constructs is rooted in Emscripten's "relooper", which was developed in a naive manner ignoring previous (superior) research on irreducible control flow.
I'm less certain on my memory but I recall that Spidermonkey was more easily able to do fancier things with control flow.
While SpiderMonkey itself might support fancier things with control-flow, they were writing a separate compiler for Asm.js that relied on structured control flow for SSA construction, but I have no experience with how things evolved after that. Reading the thread at https://github.com/WebAssembly/design/issues/796, it seems that V8 and SpiderMonkey have roughly equivalent design constraints.
https://pdfs.semanticscholar.org/7dd2/b5359ab507fec1b1223db6...
The cause of this is that the dataflow analysis performs nontrivial merging of dataflow facts, requiring a reanalysis. In the WebAssembly design there is no merging; any divergence of stack effects is an error.
There are other deeper issues with the Java design: bytecode subroutines, the fact that the Java subtype relation doesn't form a semilattice, and way in which Java bytecode models object initialization.
Packing as much as possible into a single cons cell, or a JS value, was (and is) important, so bit masking to distinguish between integers, floats, strings, functions, objects, was an inevitable approach.
JavaScript tried to make things artificially "simpler" by implicit conversions. As usual, the lack of consistency only lead to more eventual complexity than a sound solution would have in the first place.
Is 2 between 1.9 and 2.1? What about between 1.9 and 2.1111111111111111111111111111111111111111?
What about over/underflow? Do you wrap, clamp, throw an exception? Do you round or set +-inf? Can you divide by zero? Can you divide an integer by a floating point number, and if so what would the result be?
What happens if, say, you multiply an 8-bit and 16-bit integer and the result (using twos complement) doesn't fit in 8 bits?
"Use IEEE-754 doubles for everything" answers all these questions. I think JavaScript is basically junk, but I find at least this aspect to be rather elegant (to the point that I suspect it came from somewhere else, haha).
I'm not so sure about that. There is an alternative implementation of Lua called LuaJIT, which uses a similar NaN-tagging trick, and it also happens to be incredibly fast. LuaJIT uses the 5.1 version of the language, with some backported 5.2 features for compatibility, but it will likely never backport 64-bit integers from 5.3.
It's resulted in something like the Python 2/3 split. Except it's even worse because Lua has relatively little penetration as a general purpose scripting language, but is quite popular as a language that can be embedded in a program to add scripting capabilities. This means it's up to the developer of said program which version of Lua they choose to embed, and from what I can tell, most developers choose speed over 64-bit integer support.
It's not ideal to presume such a thing when there are ways to find out the answer, either from the link in a sibling comment or by observing JavaScript code that uses the Google Maps API or similar mapping APIs. How would you represent a latitude or longitude with a 32-bit integer?
I don't mean to pick on you. I have made these same kinds of assumptions too many times and it always got me in trouble.
Here's a particularly embarrassing example. When I did my first election results map for Google, I needed a way to represent the outline of a state. I saw that the Maps API supported polygons, so I thought "that's great, I can use a polygon for each state!"
Until the map went live and someone asked me "What happened to the northern part of Michigan [which isn't connected to the rest of the state]? And where are the rest of the Hawaiian Islands?"
It turned out that a single polygon wasn't enough to represent the outline of a state. Who would have guessed?
Don't let this happen to you. :-)
https://github.com/thlorenz/v8-perf/blob/master/data-types.m...
EDIT: The more I think about it (and the more comments I get), the more I think this might actually be a good thing. I think bigint will probably not be used in places where integer numbers are currently used, and it might be unreasonable to expect code to work with numbers or bigints. In that case, it's actually probably good that you can easily tell the difference using typeof, without requiring some hacky fix like `Array.isArray`.
It does mean that "don't mix numbers and bigints" is going to become very popular JS advice soon.
Do these guys assemble their application as hundreds (or thousands) of NPM packages containing single functions and then import them together? Because now I actually believe they are capable of it.
And this: https://dev.to/leoat12/the-nodemodules-problem-29dc
Bitwise operators also coerce to integer, so they should roughly always be OK.
But that's not how you write software. In an application, you have a canonical representation that you pass through your business logic.
But I dunno, maybe it shouldn't be expected to work with bigints either. It's not a regular int, so I doubt people will start using these as array indices in for loops or what have you, so maybe it should be a different type...
You need to make a deliberate decision about your data, not deck the halls with just-in-case programming. The software you write will be much better for it.
Besides, even if you could justify a bunch of typeof checks and you were doing it everywhere, Javascript already has a way to spare you from repetitive code: a function.
"Regular int" is not a distinct type in javascript. There are now two numeric types: 64-bit floating point, and bigint.
Math.pow(2,53)
// 9007199254740992
Math.pow(2,53)+1
// 9007199254740992
Math.pow(2,53)+2
// 9007199254740994
0.1 + 0.2
// 0.30000000000000004Not if your code works with arbitrary inputs.
I have some collation sequences that I’ll need to tweak.
But there's no reason that fixed-size ints and IEEE floats can't be intermixed with arbitrary-size/precision numbers, even in a small language, such as Scheme:
https://schemers.org/Documents/Standards/R5RS/HTML/r5rs-Z-H-...
Racket adds a bit more:
https://docs.racket-lang.org/reference/numbers.html
IIRC, Brendan Eich was aware of Scheme when he defined what's now known as JavaScript, but on a crazy-tight schedule, and JS wasn't intended to be an applications language.
[0] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
[1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
["array", [...]]
;-)But it would be nice to have literal 8n to convert to a bigint 8n. That's not in the JSON standard though.
If you only decoded big ones as BigInt, you would have the same problem of updating the code but now you have 2 codepaths at every callsite!
It's perhaps a defect in the JSON spec that it doesn't have a provision to support BigInt, but it's not really clear what should happen!
Quickest thing I can think of is storing it as power of 2 + the difference ending with character n as good measure. It is easy to serialize/de-serialize and you can verify the number by re-serializing to be a proper BigInt and not for instance a String. Confusion still would be possible, but rare. So 1 would be stored as 0+1n, 42 would be stored as 5+10n, etc.
Object.defineProperty(BigInt.prototype, "toJSON", {
get() {
"use strict";
return () => String(this);
}
});
Of course you could also serialize to a Number, but that risks losing precision. For example, the Twitter API often returns IDs as both a Number ("id") and a String ("id_str") to safely handle cases where the value might fall outside of a normal double-precision float: https://developer.twitter.com/en/docs/tweets/data-dictionary...forty years past K&R.
Wish I knew Tcl.
But stay away from TCL...
It is such a mess.
> I love TCL.
> It's my favorite language
> to play around in.
This does make me curious how they're pronouncing TCL in two syllables, though.
And you can get some of the patching effect in any language with uniform function/method calls, though typically with a little more scoping and thus less ability to inflict unexpected side effects: https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax
https://tc39.github.io/proposal-bigint/#prod-BigIntLiteralSu...
I suspect they didn't want to conflate the two types.
It actually stands for "not numeric"
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
In Chrome, but not Safari or Edge.
Sorry for the drama. I know its off topic, but its all I can contribute since I know very little about the implementation of base data types.
> censoring my comment will not resolve the inherent dishonesty of this article!!!
Even more amusingly, the commenter's name links to disney.com. Or perhaps it is a strange new astroturfing campaign.
I wonder if the interpreters will convert between BigInts and small ints automatically now? The backend side was PHP and it did so we never noticed the problem there.
Also, I'm assuming there are performance issues with using bigints.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
I once wrote a replacement for the + operator in javascript because computers can't do proper addition (0.1+0.2!=0.3). It was basically remembering the sign and handling some other notation like 1e100, splitting on the dot, and adding them up the normal way (as integers). To merge the result of the two additions, take care of the carry (if any) and concatenate the two numbers again with a dot.
So from my primitive understanding, if you're already storing a variable-length number (a space win in most cases, compared to the previous 4-byte float), you might as well store two variable-length numbers. Something like ABxx xxxx for the first byte, where if A=0 there is no decimal part (no second variable-length number) and if B=0 there is no next byte (the variable length part of it), then use Bxxx xxxx for each following byte. Then you have arbitrary size and decimals with perfect addition (no floating point approximations anymore).
Would it be that much slower, that it's an awful idea to do by default? Like, would it be noticeable on an average website with the usual godawful amount of javascript? Python gets away with it, so that would be weird. And you could still introduce special syntax (like, I don't know, suffixing an n maybe) to use old and faster primitive types for those who really need that.
For example, you can’t represent 1/3 in decimal, for the exact same reason you can’t represent 1/5 in binary. (1/3 is the infinitely repeating decimal 0.3333..., whereas 1/5 is the infinitely repeating binary 0.00110011001100...)
In general if you need to perfectly represent rational arithmetic you shouldn’t be using decimal or binary; you should have a type with an integer numerator and integer denominator. I don’t see the value of making arithmetic dramatically slower without actually solving the approximation issue.
If you are ever calling == on floating point numbers, you are doing something seriously wrong. Floating point numbers are supposed to be used for scientific and numerical computations where there is a notion of measurement error, and “exactly equal” is nonsense, so yes, speed is the entire point.
That’s why making a language without integers is such a serious mistake.
Do you really expect I mentioned this example, mentioned I wrote some code that solves this issue, and still never looked up or came across an explanation of why most programming languages behave this way?
> If you are ever calling == on floating point numbers, you are doing something seriously wrong.
Not sure if the 'you' is actually directed to me or if it could be replaced with 'one', but since I mention that it would be nice to do so, I guess I should feel addressed. Thanks for saying I'm doing things seriously wrong, that really helps.
Your comment completely steers any further comments down this thread towards explaining to me why floating point addition is fast but imprecise, rather than what I mentioned that I am actually wondering about: is it that much slower to do arbitrarily large integers by default (separate from the decimal issue), and secondarily solve the decimal issue at the same time (given the example I mention of the method that solves it, at least for addition, in roughly O(2))?
Well, what you described does not actually solve the issue despite you claiming that it has, so I thought you might be confused. Which is not an insult -- many people are confused about this issue.
And you appear to have misunderstood my comment, which is not about explaining to you why binary arithmetic is imprecise, which you obviously already know. It is about explaining that decimal arithmetic is also imprecise, for the exact same reason, which is something that much fewer people understand.
Your "fix" makes it so that 1/10 + 2/10 == 3/10, but it still doesn't make it so that 1/3 + 1/3 == 2/3. So how is it actually "precise"?
To answer your question about speed: yes, doing things with arbitrarily-sized integers is much slower than doing them with floats (or normal integers for that matter). In the best case, you add at least one branch to every arithmetic operation. And a binary-coded decimal scheme like you described would be even slower still.
It doesn't really matter whether it would make the average website slower, since the average website should not be using floats (OR binary-coded decimals like your scheme) in the first place except for calculating layouts or other numeric calculations where asking whether 0.1 + 0.2 == 0.3 would never come up. For discrete computations they should be using integers -- that's what integers are for.
Fair point! I don't think anyone ever put it quite this way. I mean, I knew that 1/3 cannot be represented in decimal and that decimal, like binary, is imprecise for the exact same reason, but I don't think anyone asked me about the definition of precise and why I think my version of addition fits that definition of precise better :). I think the answer is that, in code, we type in decimal: 0.1+0.2 and not 0b0.1+0b0.10 (if that would even be valid syntax). We work in base 10 most of the time, so we know that operations on 1/3 can not have infinite precision. But that's just something I came up with on the spot, I'm not sure that this is the true reason why it feels more correct.
> It doesn't really matter whether it would make the average website slower, since the average website should not be using floats
Fair enough about floats, but why about arbitrarily large integers? The feature being introduced could have been introduced as 'works out of the box' instead of 'opt in using the n suffix'.
Actually, I just realized it would probably break code that does bit shifts. Maybe that's why bigint is not the default?
I think you are exactly right. People think of decimals as being the "actual", "primary", "fundamental" numbers, and binary as being an imperfect representation of those. Whereas in reality, both binary and decimal are imperfect representations of rational numbers, and we only think of decimal as being more fundamental because of our writing system.
> Fair enough about floats, but why about arbitrarily large integers
How exactly would you represent them? The best way I can think of is:
struct BigInt {
int64_t first_64;
char *data; // pointer to extra, dynamically allocated data
int data_len;
};
This would allow you to avoid doing a dynamic allocation for the most common case of being under 64 bits. And the addition algorithm would probably special case that too, and look something like this: BigInt add(BigInt x, BigInt y) {
if (x.data_len == 0 && y.data_len == 0) {
int64_t new_val = x.first_64 + y.first_64;
if (overflow_signaled()) {
return add_slow_path(x, y);
}
return {new_val, 0, nullptr};
} else {
return add_slow_path(x, y);
}
}
As you can see, there is a ton of complexity here, even just for the simplest possible case. Replacing what was before literally just one instruction, e.g. addq %rbx, %rcx . Also, each number is represented by a 20-byte struct now instead of 8. So now each 64-byte cache line can fit only 3 values instead of 8. Because of all this, it would be dramatically slower.This is just for the easiest case of no overflow! If you overflow and have to then go allocate memory dynamically and loop over it, it would of course be even worse.
This is not true.
>>> type(1)
<class 'int'>
>>> type(1.5)
<class 'float'>Still though, any int can get as large as you like by default, no weird -n suffix (that I never saw in any other language -- just like most of Javascript's other recently added syntax, by the way, it's the new Perl).
I do wonder where I got this notion of Number. Is there some other language that has this?
However, I don't know a single mainstream application programming language that has a single numeric type that can handle: arbitrarily large integers, floating point values, and correct decimal arithmetic (0.1 + 0.2 == 0.3). I have at least a passing familiarity with probably about a dozen general purpose programming languages, and none of them can do it. If anyone knows of one, I'd be interested to learn about it.
Python 3 has mostly same approach to number types.
...this is a terrible, bad-faith way of making a complaint that isn't even valid. Computers do do proper addition. And they handle the exact example you give perfectly, if that's what you want. JavaScript might have difficulty with that problem, but that's because, unlike the computers it's implemented for, it has no integers.
What do you think "proper addition" would involve if I asked you to tell me 1/7 + 1/3, on paper?
If you mean how should the machine do that, it can find the least common multiple of 3 and 7 (which is 21) and then convert both fractions to be in that denominator, then simplify if possible. This is, as I said, annoyingly slow, but if you want "proper" answers that's what you got.
I wouldn't bother because I'm aware that _Almost All Real Numbers are Normal_ and so they're usually completely impossible to express in this fashion anyway and we should stop our foolish pretence that you can add non-integers together and expect to get "correct" answers just because it can be done for some easy cases.
> a replacement for the + operator in javascript because computers can't do proper addition (0.1+0.2!=0.3).
Just saying they can't do it (I edited this: first I said that JS doesn't do it properly, but I thought that was rather too narrow. I guess 'computers' is too broad again. Pick a name, you know what I mean)
> [an explanation of what worked for me some years ago] Then you have arbitrary size and decimals with perfect addition (no floating point approximations anymore).
Again, just mentioning that this would solve it for addition, not saying this is the perfect way for life, the universe, and everything.
> Would it be that much slower, that it's an awful idea to do by default?
See, I'm not insisting on anything, I'm wondering and asking.
> Python gets away with it,
It doesn't do correct decimal addition either, so I'm not even focusing on resolving floating point inaccuracies, I'm more interested in "if it would be so slow to do arbitrary precision integers---oh and by the way, wouldn't it also solve this addition thing?"
Now, you also didn't exactly say that I was insisting, you said "someone who insists". So maybe this only applies to your parent comment. But every time I bring it up, people stumble over each other to tell me why it is this way. I already know why it is this way. There is a lot other words in the comment that one could reply to, and it's rather frustrating that it's completely overshadowed - every time - by people ignoring everything except those twelve magic characters: 0.1+0.2!=0.3.
> Just saying they can't do it (I edited this: first I said that JS doesn't do it properly, but I thought that was rather too narrow. I guess 'computers' is too broad again. Pick a name, you know what I mean)
You can say it, but that won't make it true. They can do it, and they do do it. Your comment is so much nonsense. The algorithm computers use to add 0.1 and 0.2 is the same algorithm that you use, which is, unsurprisingly, why they produce correct results.
> There is a lot other words in the comment that one could reply to, and it's rather frustrating that it's completely overshadowed - every time - by people ignoring everything except those twelve magic characters: 0.1+0.2!=0.3.
I'll point out again that I'm focusing on your completely unjustified claim that "computers can't do proper addition", which you didn't bother to include in "those twelve magic characters" that everyone is complaining about.
Are you aware of floating point math? That's what most languages, and almost all languages aimed at performance, use. It's defined in the IEEE 754 standard and supported on a hardware level in many devices.
Here is a listing of the result of 0.1 + 0.2 in various languages: https://0.30000000000000004.com/
As the URL already indicates, most languages, including C, Rust, C++, Java, Javascript, Clojure, FORTRAN, Python, etc. evaluate this to 0.30000000000000004. I'm fine with this, I need fast rather than precise math. But it's not "correct".
That's normal floating point math behaviour and fine in most use cases. You can't fix this for add, sub, mul and div without serious performance implications.
> Would it be that much slower, that it's an awful idea to do by default?
Yes. Easily an order of magnitude slower, perhabs two. The fixed size of uint32, float, double, etc. is an important property that allows computations to be fast. And since the size is fixed, you have to accept trade-offs in how or which numbers can be represented. Also, as someone else already mentioned, you can't even store 1 / 3 as a single numeric value. You'd have to store it as a rational number. Things are going to get super complicated once you combine rational numbers in computations and complex formulas.
I'm doing lot's of number crunching tasks with javascript, with performances of up to almost ~50% of equivalent C++ code. If js would have used a non-natively supported number format by default, it would have been useless for me.
a = Math.pow(2, 1023)
t = new Date().getTime()
for (var i = 0; i < 1e7; i++) {
a += i;
}
console.log(new Date().getTime() - t);
vs a = pow(2, 1024)
t = time.time()
for i in range(int(1e7)):
a += i
print(time.time() - t)
Both ran a bunch of times: pypy does it in 279ms and nodejs in 250. I chose 1024 for Python because that is where JS starts to return infinity, so the JS code does operations on a number just below that. The time seems to be spent in the loop, as an empty loop or a loop doing a+=0 is 20x faster.Lowering the exponent to 100, JS spends 269ms and pypy 142. Not sure why that is, but having arbitrarily large integers doens't seem to make this arithmetic any faster.
I don't know how to quickly toy around with fraction-based floats, but at least for arbitrarily large integers, I'm not sure why we're going to have to put up with new syntax.
I tried in C:
#include <math.h>
#include <stdio.h>
int main()
{
double a = pow(2, 100);
for (double i = 0; i < 1e7; ++i) {
a += i;
}
printf("%f\n", a);
}
and timed it. The time taken was 17ms. {
let a = Math.pow(2, 1023)
let t = performance.now();
let max = 1e7;
for (let i = 0; i < max; i++) {
a += i;
}
console.log(performance.now() - t);
}
Main problem was, that you should declare a with let.That benchmark is a bit strange/flawed anyway. You're initializing a as pow(2, 1023), then adding numbers in the loop. But since a is already such a large double value, the result won't change. The numbers you add are too small to make a dent in the value of a, likely because a isn't an integer. It's a double with a limited precision for large integer values.
Everyone thought that was a really bad stupid idea.
IE. If we provide a decimal type as a default. Then everyone will use it and it'll be 'slow'. Thus this is a bad idea. So we should not make that even an option. Because IEEE 754 is fast and fast is 'good'
Frankly that decision was the worst language decision ever made bar none.
We see BigInt as a relatively uncontroversial but important extension of the numeric type system of JavaScript. Once the door is open for a type that is not immediately compatible with Number, potentially new numeric types can be introduced. Perhaps BigDecimal (or Rational?) can follow.
https://developers.google.com/web/updates/2018/05/bigint#pol...
That doesn't sound like a recommendation.
For this to work, you need a library whose bigint behavior either exactly matches the built in implementation, or whose deviations are known and can reasonably be account for when translating to built in bigints.
The library they recommend is a JavaScript translation of their implementation of built in bigints, so should match numerically what the built in implementation does.
There may be other libraries that would be suitable, but it would be nice if everyone agreed on just one for this so that when the time comes to use Babel or similar to replace the library calls with use of the built ins, it only has to deal with one library.
JS already has a Uint8Array, so bignum libraries are easy. (Let me tell you though, there were JS bignum libraries before Uint8Array, and they were, ahem, “interesting.” I think a popular one built a byte-array abstraction on top of hex-encoded strings, and then built bignums on that.)
Actually supporting the literal syntax or the operators via a plain library is impossible; but you can just do it the other way around: encourage developers to use a bignum library (for now), and have said library just “bake down” to native BigInts when they’re available.
Since everybody until now who was using bignums in JS was using them through a library, this won’t be a hard change to make.
(And it’s already been done! https://www.npmjs.com/package/big-integer now bakes down to native BigInts when it can.)
As for the brazen developers who start writing code to directly use the native BigInt... I suppose they’ll have to have two versions of their compiled code units in their minified blob, with a loader shim that evals out the version of the class/module that does/doesn’t use bigints. Maybe we’ll see an JS-pipeline pass to generate this. (I’m not a JS dev, so I’m not sure if there’s already support for this kind of thing.)
Can't you rewrite the syntax into something usable at runtime? Not that it were a good idea.
I’m guessing this has been done, to let the browser run—without backend compilation—<script type=“foo”> where foo is CoffeeScript or TypeScript or ClojureScript or what-have-you; but this one would be especially bad, because the thing it’d be targeting would still be Javascript, and so you’d have to rewrite all Javascript sources you encounter, with most of them likely never using native bigints but suffering the double-parsing overhead anyway.
Oh, and you’d have to override the ES6 module loader with one that also rewrites what it loads. Ugly.
Or if your String type doesn't do any of those. Javascript strings will happily hold an arbitrary series of 16 bit numbers.
Even when something requires valid code points, you can still easily store arbitrary 8 bit values. Or 15/16/17/20 depending on how fussy you are. Unless you want it to be hacky, it shouldn't be notably worse than an array.
[1]: https://github.com/GoogleChromeLabs/jsbi
[2]: https://github.com/GoogleChromeLabs/babel-plugin-transform-j...
JS's Number can represent way more integers than just those between -2⁵³ and 2⁵³. There's a great StackOverflow answer[1] that answers a similar question, ("What is the biggest "no-floating" integer that can be stored in an IEEE 754 double type without losing precision?") which also applies here (A JS Number is an IEEE double):
> The biggest/largest integer that can be stored in a double without losing precision is the same as the largest possible value of a double. That is, DBL_MAX or approximately 1.8 × 10³⁰⁸ (if your double is an IEEE 754 64-bit double). It's an integer. It's represented exactly. What more do you want?
> Go on, ask me what the largest integer is, such that it and all smaller integers can be stored in IEEE 64-bit doubles without losing precision. An IEEE 64-bit double has 52 bits of mantissa, so I think it's 2⁵³:
More seriously, this is great news, and I think it'll be nice to have such a numeric type in JS.