Home > Software engineering >  Why does array have max size 2 ^ 32 -1 in JavaScript?
Why does array have max size 2 ^ 32 -1 in JavaScript?

Time:05-31

Max native integer value in JavaScript is 2 ^ 31 - 1 = 2147483647

You could check it using the following code:

let x = 2147483647;
console.log(x << 1) // you will get -2 instead of 2,147,483,647 * 2 = 4,294,967,296 

It means that variable bigger than 2 ^ 31 - 1 = 2147483647, always has floating point type.

But according to the ECMAScript max array size is 2 ^ 32 - 1 = 4294967295.

So if I write something like

let size = 3000000000;       // float type
let array = new Array(size); // passing float variable to constructor
                             // that accepts integer only
 

It means that firstly size will have float type, and then we pass this float size as array size argument. But array size obviously must be integer.

And it does not have any sense at all.

So the question is: Why does ECMAScript says that max array size is 2 ^ 32 - 1 and not 2 ^ 31 - 1?

CodePudding user response:

Why does array have max size 2^32 - 1 in JavaScript?

Quite simply: because it's specified that way. The specification authors could have chosen any maximum value.

2^32 - 1 is the biggest value that can be represented in an unsigned 32-bit integer. This is a reasonable choice because it limits required memory for storing an array length to 32 bits while maximizing the lengths that can be stored in these 32 bits.

A JavaScript engine could therefore theoretically use an unsigned 32-bit integer to store any array length (and, by extension, any valid array index). I don't know whether any JavaScript engines actually do it that way. (V8 doesn't; it uses either a signed 31-bit(!) integer or a float64, because... reasons too long to explain here!)

Max native integer value in JavaScript is 2 ^ 31 - 1 = 2147483647. Variable bigger than 2 ^ 31 - 1 = 2147483647, always has floating point type.

No, all Numbers in JavaScript are specified to be 64-bit floating point values, there is no such thing as a "max native integer value" in JS. Some operations (namely, the bitwise operations like x << 1) convert their inputs to signed 32-bit integers. FWIW, unsigned right-shift converts its input to unsigned 32-bit integers and interprets its output as that too, hence e.g. (-1 >>> 0) === 4294967295. This observable behavior doesn't guarantee anything about how the engine chooses to represent these values on the machine level. There is no way to tell whether 1 or 2147483647 is being stored as an integer or a float inside the engine. The JavaScript spec only guarantees that it'll behave like a float.
And, to tie this back to the question, what bitwise operations do is totally unrelated to the maximum array length.

CodePudding user response:

It is in fact possible to create a holey array with exactly up to 2^32 -1 numerical slots. anything beyond will be converted to a string hash index and will not iterate the array length property beyond 2^32 -1.

one way would be this:

# node
Welcome to Node.js v16.15.0.
Type ".help" for more information.
> const a = new Array(2**31-1)
undefined
> a
[ <2147483647 empty items> ]
> a.push("foo", "bar", "asdf")
2147483650
> a
[ <2147483647 empty items>, 'foo', 'bar', 'asdf' ]
> a.length
2147483650
> a.length = 2**32 -2
4294967294
> a.push("magic", "beer", "such overflow")
Uncaught RangeError: Invalid array length
    at Array.push (<anonymous>)
> a[2**32]
'such overflow'
> a[2**32 -20] = "indexable beyond 2^31-1"
'indexable beyond 2^31-1'
> a
[
  <2147483647 empty items>,
  'foo',
  'bar',
  'asdf',
  <2147483626 empty items>,
  'indexable beyond 2^31-1',
  <17 empty items>,
  'magic',
  '4294967295': 'beer',
  '4294967296': 'such overflow'
]
> a.length
4294967295

another example without initial length:

# node
Welcome to Node.js v16.15.0.
Type ".help" for more information.
> const a = []
undefined
> a[2**32 -10] = "magic"
'magic'
> a[2**33] = "overflow"
'overflow'
> a
[ <4294967286 empty items>, 'magic', '8589934592': 'overflow' ]
>
> Object.keys(a)
[ '4294967286', '8589934592' ]
> a[5] = "foo"
'foo'
> Object.keys(a)
[ '5', '4294967286', '8589934592' ]
> a
[
  <5 empty items>,
  'foo',
  <4294967280 empty items>,
  'magic',
  '8589934592': 'overflow'
]
> a.length
4294967287

but if you fill it, you will run out of heap memory:

# node
Welcome to Node.js v16.15.0.
Type ".help" for more information.
> const a = [];
undefined
> a.length = 2**32 -1;
4294967295
> a.fill(0)

<--- Last few GCs --->
n [1537688:0x5818540]    33577 ms: Mark-sweep 238.5 (256.1) -> 238.3 (272.1) MB, 202.6 / 0.0 ms  (  0.4 ms in 42 steps since start of marking, biggest step 0.0 ms, walltime since start of marking 833 ms) (average mu = 0.971, current mu = 0.868) allocation f[1537688:0x5818540]    52935 ms: Mark-sweep 1796.1 (1833.3) -> 1796.1 (1833.3) MB, 1996.9 / 0.0 ms  (  3.3 ms in 235 steps since start of marking, biggest step 0.1 ms, walltime since start of marking 9982 ms) (average mu = 0.911, current mu = 0.897) alloc

<--- JS stacktrace --->

FATAL ERROR: invalid table size Allocation failed - JavaScript heap out of memory
 1: 0xb09c10 node::Abort() [node]
 2: 0xa1c193 node::FatalError(char const*, char const*) [node]
 3: 0xcf8dbe v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [node]
 4: 0xcf9137 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [node]
 5: 0xeb09d5  [node]
 6: 0x10dcbdd  [node]
 7: 0x10dcdb3 v8::internal::Handle<v8::internal::NumberDictionary> v8::internal::HashTable<v8::internal::NumberDictionary, v8::internal::NumberDictionaryShape>::EnsureCapacity<v8::internal::Isolate>(v8::internal::Isolate*, v8::internal::Handle<v8::internal::NumberDictionary>, int, v8::internal::AllocationType) [node]
 8: 0x10dd3f4 v8::internal::Handle<v8::internal::NumberDictionary> v8::internal::Dictionary<v8::internal::NumberDictionary, v8::internal::NumberDictionaryShape>::Add<v8::internal::Isolate>(v8::internal::Isolate*, v8::internal::Handle<v8::internal::NumberDictionary>, unsigned int, v8::internal::Handle<v8::internal::Object>, v8::internal::PropertyDetails, v8::internal::InternalIndex*) [node]
 9: 0x1005348  [node]
10: 0x108b615 v8::internal::JSObject::AddDataElement(v8::internal::Handle<v8::internal::JSObject>, unsigned int, v8::internal::Handle<v8::internal::Object>, v8::internal::PropertyAttributes) [node]
11: 0x10cf36e v8::internal::Object::AddDataProperty(v8::internal::LookupIterator*, v8::internal::Handle<v8::internal::Object>, v8::internal::PropertyAttributes, v8::Maybe<v8::internal::ShouldThrow>, v8::internal::StoreOrigin) [node]
12: 0x10d3903 v8::internal::Object::SetProperty(v8::internal::LookupIterator*, v8::internal::Handle<v8::internal::Object>, v8::internal::StoreOrigin, v8::Maybe<v8::internal::ShouldThrow>) [node]
13: 0xd5972d v8::internal::Builtin_ArrayPrototypeFill(int, unsigned long*, v8::internal::Isolate*) [node]
14: 0x15f2179  [node]
[1]    1537688 abort (core dumped)  node
node  26,77s user 21,04s system 39% cpu 2:02,24 total

So technically it is possible to use 2^32 -1 arrays in javascript.
It is undoubtedly not changable to 2^31 -1 for backwards compatibility reasons to code that might rely on this. (as many js "features")

Using .length to change array size was a common thing before the web 2.0 era and still is covered by spec to both grow and shrink arrays.

Using push, concat, splice and explicit indexing will also allow you to go beyond 2^31-1

The first published javascript spec (on page 65 to 66) had this bulletpoint:

15.4.2.2 new Array(len) The [[Prototype]] property of the newly constructed object is set to the original Array prototype object, the one that is the initial value of Array.prototype (0).The [[Class]] property of the newly constructed object is set to "Array".
If the argument len is a number, then the length property of the newly constructed object is set to ToUint32(len). If the argument len is not a number, then the length property of the newly constructed object is set to 1 and the 0 property of the newly constructed object is set to len.

specifying length to be cast into a uint32

5.1 also specifies this:

15.4.5.1 [[DefineOwnProperty]] ( P, Desc, Throw ) c. Let newLen be ToUint32(Desc.[[Value]]).
d. If newLen is not equal to ToNumber( Desc.[[Value]]), throw a RangeError exception.

as well as:

15.4.2.2 new Array (len) If the argument len is a Number and ToUint32(len) is equal to len, then the length property of the newly constructed object is set to ToUint32(len). If the argument len is a Number and ToUint32(len) is not equal to len, a RangeError exception is thrown.

6.0 also specifies this here and here

how ever, Array (len) in 6.0 explicitely states the usage of intLen, which implies, it uses the integer portion of the provided Number as truth for defining the Array size while 9.4.2.1[[DefineOwnProperty]] ( P, Desc) (setting .length) does not explicitly state the usage of it.

The better question would be: Why is 2^32 -1 not accepted as parameter for array creation in the spec?
Well.. actually it does work:

nodejs:

> new Array(2**32-1)
[ <4294967295 empty items> ]
> Array(2**32-1)
[ <4294967295 empty items> ]
> 3000000000
3000000000
> Array(3000000000)
[ <3000000000 empty items> ]

firefox:

Array(2**32-1)
(4294967295) [empty × 4294967295]
new Array(2**32-1)
(4294967295) [empty × 4294967295]

So I assume stating 2^31-1 is wrong inside the spec and should be modified.

Your question was a bit missleading since you made it appear, as if anything beyond 2^31 was simply impossible.

  • Related