memory allocation in javascript [duplicate] - javascript

That is, if I use the current time as an index into the array:
array[Date.getTime()] = value;
will the interpreter instantiate all the elements from 0 to now? Do different browsers do it differently?
I remember there used to be a bug in the AIX kernel, which would create pseudo-ttys on request, but if you did, say, "echo > /dev/pty10000000000" it would create /dev/pty0, /dev/pty1, .... and then fall over dead. It was fun at trade shows, but I don't want this to happen to my customers.

Yes, they are. They are actually hash tables internally, so you can use not only large integers but also strings, floats, or other objects. All keys get converted to strings via toString() before being added to the hash. You can confirm this with some test code:
<script>
var array = [];
array[0] = "zero";
array[new Date().getTime()] = "now";
array[3.14] = "pi";
for (var i in array) {
alert("array["+i+"] = " + array[i] + ", typeof("+i+") == " + typeof(i));
}
</script>
Displays:
array[0] = zero, typeof(0) == string
array[1254503972355] = now, typeof(1254503972355) == string
array[3.14] = pi, typeof(3.14) == string
Notice how I used for...in syntax, which only gives you the indices that are actually defined. If you use the more common for (var i = 0; i < array.length; ++i) style of iteration then you will obviously have problems with non-standard array indices.

How exactly JavaScript arrays are implemented differs from browser to browser, but they generally fall back to a sparse implementation - most likely the same one used for property access of regular objects - if using an actual array would be inefficient.
You'll have to ask someone with more knowledge about specific implementations to answer what excatly triggers the shift from dense to sparse, but your example should be perfectly safe. If you want to get a dense array, you should call the constructor with an explicit length argument and hope you'll actually get one.
See this answer for a more detailed description by olliej.

You could avoid the issue by using a javascript syntax designed for this sort of thing. You can treat it as a dictionary, yet the "for ... in ... " syntax will let you grab them all.
var sparse = {}; // not []
sparse["whatever"] = "something";

Javascript objects are sparse, and arrays are just specialized objects with an auto-maintained length property (which is actually one larger than the largest index, not the number of defined elements) and some additional methods. You are safe either way; use an array if you need it's extra features, and an object otherwise.

The answer, as is usually true with JavaScript, is "it's a bit wierder...."
Memory usage is not defined and any implementation is allowed to be stupid. In theory, const a = []; a[1000000]=0; could burn megabytes of memory, as could const a = [];. In practice, even Microsoft avoids those implementations.
Justin Love points out, the length attribute is the highest index set. BUT its only updated if the index is an integer.
So, the array is sparse. BUT built-in functions like reduce(), Math.max(), and "for ... of" will walk through the entire range of possible integer indices form 0 to the length, visiting many that return 'undefined'. BUT 'for ... in' loops might do as you expect, visiting only the defined keys.
Here's an example using Node.js:
"use strict";
const print = console.log;
let a = [0, 10];
// a[2] and a[3] skipped
a[4] = 40;
a[5] = undefined; // which counts towards setting the length
a[31.4] = 'ten pi'; // doesn't count towards setting the length
a['pi'] = 3.14;
print(`a.length= :${a.length}:, a = :${a}:`);
print(`Math.max(...a) = :${Math.max(a)}: because of 'undefined values'`);
for (let v of a) print(`v of a; v=:${v}:`);
for (let i in a) print(`i in a; i=:${i}: a[i]=${a[i]}`);
giving:
a.length= :6:, a = :0,10,,,40,:
Math.max(...a) = :NaN: because of 'undefined values'
v of a; v=:0:
v of a; v=:10:
v of a; v=:undefined:
v of a; v=:undefined:
v of a; v=:40:
v of a; v=:undefined:
i in a; i=:0: a[i]=0
i in a; i=:1: a[i]=10
i in a; i=:4: a[i]=40
i in a; i=:5: a[i]=undefined
i in a; i=:31.4: a[i]=ten pi
i in a; i=:pi: a[i]=3.14
But. There are more corner cases with Arrays not yet mentioned.

Sparseness (or denseness) can be confirmed empirically for NodeJS with the non-standard process.memoryUsage().
Sometimes node is clever enough to keep the array sparse:
Welcome to Node.js v12.15.0.
Type ".help" for more information.
> console.log(`The script is using approximately ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024 * 100) / 100} MB`)
The script is using approximately 3.07 MB
undefined
> array = []
[]
> array[2**24] = 2**24
16777216
> array
[ <16777216 empty items>, 16777216 ]
> console.log(`The script is using approximately ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024 * 100) / 100} MB`)
The script is using approximately 2.8 MB
undefined
Sometimes node chooses to make it dense (this behavior might well be optimized in future):
> otherArray = Array(2**24)
[ <16777216 empty items> ]
> console.log(`The script is using approximately ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024 * 100) / 100} MB`)
The script is using approximately 130.57 MB
undefined
Then sparse again:
> yetAnotherArray = Array(2**32-1)
[ <4294967295 empty items> ]
> console.log(`The script is using approximately ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024 * 100) / 100} MB`)
The script is using approximately 130.68 MB
undefined
So perhaps using a dense array to get a feel for the original AIX kernel bug might need to be forced with a range-alike:
> denseArray = [...Array(2**24).keys()]
[
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71,
72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83,
84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95,
96, 97, 98, 99,
... 16777116 more items
]
> console.log(`The script is using approximately ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024 * 100) / 100} MB`);
The script is using approximately 819.94 MB
undefined
Because why not make it fall over?
> tooDenseArray = [...Array(2**32-1).keys()]
<--- Last few GCs --->
[60109:0x1028ca000] 171407 ms: Scavenge 1072.7 (1090.0) -> 1056.7 (1090.0) MB, 0.2 / 0.0 ms (average mu = 0.968, current mu = 0.832) allocation failure
[60109:0x1028ca000] 171420 ms: Scavenge 1072.7 (1090.0) -> 1056.7 (1090.0) MB, 0.2 / 0.0 ms (average mu = 0.968, current mu = 0.832) allocation failure
[60109:0x1028ca000] 171434 ms: Scavenge 1072.7 (1090.0) -> 1056.7 (1090.0) MB, 0.2 / 0.0 ms (average mu = 0.968, current mu = 0.832) allocation failure
<--- JS stacktrace --->
==== JS stack trace =========================================
0: ExitFrame [pc: 0x100931399]
1: StubFrame [pc: 0x1008ee227]
2: StubFrame [pc: 0x100996051]
Security context: 0x1043830808a1 <JSObject>
3: /* anonymous */ [0x1043830b6919] [repl:1] [bytecode=0x1043830b6841 offset=28](this=0x104306fc2261 <JSGlobal Object>)
4: InternalFrame [pc: 0x1008aefdd]
5: EntryFrame [pc: 0x1008aedb8]
6: builtin exit frame: runInThisContext(this=0x104387b8cac1 <ContextifyScript map = 0x1043...
FATAL ERROR: invalid array length Allocation failed - JavaScript heap out of memory
Writing Node.js report to file: report.20200220.220620.60109.0.001.json
Node.js report completed
1: 0x10007f4b9 node::Abort() [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
2: 0x10007f63d node::OnFatalError(char const*, char const*) [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
3: 0x100176a27 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
4: 0x1001769c3 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
5: 0x1002fab75 v8::internal::Heap::FatalProcessOutOfMemory(char const*) [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
6: 0x1005f3e9b v8::internal::Runtime_FatalProcessOutOfMemoryInvalidArrayLength(int, unsigned long*, v8::internal::Isolate*) [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
7: 0x100931399 Builtins_CEntry_Return1_DontSaveFPRegs_ArgvOnStack_NoBuiltinExit [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
8: 0x1008ee227 Builtins_IterableToList [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
Abort trap: 6

They can be but they don't always have to be, and they can perform better when they're not.
Here's a discussion about how to test for index sparseness in an array instance:
https://benmccormick.org/2018/06/19/code-golf-sparse-arrays/
This code golf (fewest characters) winner is:
let isSparse = a => !!a.reduce(x=>x-1,a.length)
Basically walking the array for indexed entries while decrementing the length value and returning the hardened !! boolean of the falsy/truthy numerical result (if the accumulator is decremented all the way to zero, the index is fully populated and not sparse). Charles Merriam's caveats above should be considered as well and this code doesn't address them, but they apply to hashed string entries which can happen when assigning elements with arr[var]= (something) where var wasn't an integer.
On reason to care about index sparseness is its effects on performance, which can differ between script engines, there's a great discussion about array creation/.initialization here:
What’s the difference between "Array()" and "[]" while declaring a JavaScript array?
A recent answer to that post has a link to this deep dive into how V8 tries to optimize arrays by tagging them to avoid (re-)testing for characteristics like sparseness: https://v8.dev/blog/elements-kinds. The blog post is from Sept '17 and the material is subject to some change, but the breakdown to implications for day-to-day development is useful and clear.

Related

Greedy Algorithm with Maximum Salary Problem?

I am trying to solve the below problem. I think my solution is working well. But, the system in which I am trying to upload the solutions, is not accepting my solution. Probably some tests are failing. May I know what am I missing?
The problem is:
As the last question of a successful interview, your boss gives you a few pieces of paper with numbers on it and asks you to compose a largest number from these numbers. The resulting number is going to be your salary, so you are very much interested in maximizing this number. How can you do this?
Sample 1
Input:
2
21 2
Output: 221
Sample 2
Input:
3
23 39 92
Output: 923923
My solution is:
function MaxSallary(nums) {
let maxSize = Math.max(...nums).toString().length;
let newArr = [];
nums.map((num) => {
while (num.toString().length < maxSize) {
num = num.toString().concat(num.toString().split("").slice(-1)[0]);
}
newArr.push(Number(num));
});
finalArr = [];
while (newArr.length > 0) {
let minIndex = newArr.indexOf(Math.max(...newArr));
newArr.splice(minIndex, 1);
finalArr.push(...nums.splice(minIndex, 1));
}
return finalArr.join("");
}
console.log(MaxSallary([2, 12, 34, 11, 43, 21, 5]));
You want to know in which order the numbers should be concatenated, so that, once parsed back to a number, the result is the highest possible. Reworded this way, it looks like we should sort the array first.
When comparing two numbers a and b, to know which one should come first, we need to know which one is higher between ${a}${b} and ${b}${a}:
.sort((a, b) => parseInt(`${b}${a}`, 10) - parseInt(`${a}${b}`, 10)))
.sort mutates the array (and returns it), so I'm cloning it first.
function MaxSallary(nums) {
const salary = [...nums]
.sort((a, b) => parseInt(`${b}${a}`, 10) - parseInt(`${a}${b}`, 10))
.join("");
return salary;
}
console.log(MaxSallary([21, 2]));
console.log(MaxSallary([23, 39, 92]));
console.log(MaxSallary([2, 12, 34, 11, 43, 21, 5]));

Improve performance of this combination algorithm?

I'm working on this kata from Codewars. The task is:
Given a certain number, how many multiples of three could you obtain with its digits?
Supose that you have the number 362. The numbers that can be generated from it are:
362 ----> 3, 6, 2, 36, 63, 62, 26, 32, 23, 236, 263, 326, 362, 623, 632
I've written the following recursive function to calculate all possiblities:
const findMult_3 = (num) => {
const powerset = (set) => {
const combinations = []
const combine = (prefix, chars) => {
for (let i = 0; i < chars.length; i++) {
const newPrefix = parseInt(prefix + chars[i])
if (!combinations.includes(newPrefix)) {
combinations.push(newPrefix)
} else {
console.log('encountered duplicate')
}
combine(newPrefix, chars.filter((x, ind) => ind !== i))
}
}
combine('', set)
return combinations.sort((a, b) => a - b)
}
const allCombinations = powerset(num.toString().split(''))
const factorsOfThree = allCombinations.filter(x => x % 3 === 0).filter(x => x !== 0)
return [factorsOfThree.length, factorsOfThree.pop()]
}
findMult_3(43522283000229)
I noticed early on that I was encountered a lot of duplicate cases, hence the console.log('encountered duplicate') flag.
Execution of this algorithm is taking an extremely long time for large numbers, eg 43522283000229.
How can I improve the performance of this code, or should it be scrapped entirely?
With most coding katas, the choice of algorithm is far more important that implementation details, but before we get to that, let me point out the most glaring flaw of your implementation:
if (!combinations.includes(newPrefix)) {
combinations.push(newPrefix)
} else {
console.log('encountered duplicate')
}
combine(newPrefix, chars.filter((x, ind) => ind !== i))
combinations is an array, and includes works by iterating over the array and checking every element. That is, to check whether an element is a duplicate, you are comparing it with every previously encountered combination. Since there are exponentially many of those, this is going to be very slow. If you used a dictionary object or Map instead, your code would be far faster.
Also, did you notice you are proceeding with generating combination even if the combination is a duplicate? That's redundant.
So the cheap improvement would be:
const combinations = {};
if (combinations[prefix]) {
// duplicate, do nothing
} else {
combinations[prefix] = true;
combine(...);
}
The real improvement however is choosing a better algorithm. If you make use of the mathematical structure of the problem, you may be able to find the number of solutions without iterating over them all.
The following insights might help:
a number is divisible by three if and only if the sum of its digits is.
a sum of digits is divisible by 3 if and only if the sum of their remainders when divided by 3 is 0.
the order of digits in the input does not matter
One (first) optimization would be to only check or generate numbers where the sum of the digits is divisible by 3, since only those numbers are divisible by 3.
So in your example (362) you could skip all combinations with 3 and 2, 6 and 2 and all possible combinations with the 3 digits (because the sum of the 3 digits is not divisible by 3).
For the larger number (43522283000229) you can skip a lot more, for example all combinations with digits:
43, 35, 52, ...
435, 352, .., 283, ...
4352 (thus, including all possible combinations of those 4 digits), ...
43522, ...
43528, 43529, ...
43528309, ...
and so on
Possible algorithm:
Original number: 43522283000229
First, sort the digits: 00022222334589
Then find all distinct combinations of digits where
their sum is a multiple of 3:
left to right:
1 digit : 3, 9
2 digits: 03, 09, 24, 33, 39, 45, 48, ...
3 digits: 003, 009, 024, 033, 039, 222, 234, ...
n digits ...
Now, for all above numbers create every possible combination with their
digits, skip those with leading zeros.:
3, 9, 30, 90, 24, 42, 33, 39, 93, 45, 54, 48, 84, 300, 900,
204, 240, 402, 420, 309, 390, 903, 930, 222, 234, 243, ...
We don't have to check for division by 3, they all match.
We don't have to check for duplicates.
You could then sort the resulting list if needed.

How much memory is allocated to different data types in Javascript & Python? [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 5 years ago.
Improve this question
I wanted to know how much memory does different data types take up in Python and Javascript.I know that you do not need to declare types in both the languages but don't forget they are dynamically typed languages
For the case of Python 3, let's explore the the amount of storage each of the basic data types consume,
First, let's define a helper function sizeof which will help us explore,
import sys
def sizeof(x):
print(x.__class__, sys.getsizeof(x), x)
Running this with 64-bit Python 3.6.1, here are the results :
>>> sizeof(None)
<class 'NoneType'> 16 None
None takes 16 bytes. Next let's explore integers,
>>> sizeof(10)
<class 'int'> 28 10
>>> sizeof(2**64)
<class 'int'> 36 18446744073709551616
>>> sizeof(2**128)
<class 'int'> 44 340282366920938463463374607431768211456
Integers in python are arbitrary precision and the above rightfully exhibits the behaviour, the size grows linearly in the logarithm of the integer represented.
>>> sizeof(0.0)
<class 'float'> 24 0.0
>>> sizeof(10.0**100)
<class 'float'> 24 1e+100
>>> sizeof(10.0**308)
<class 'float'> 24 1e+308
Floats in python appear to be of constant size (24 bytes)!
>>> sizeof("")
<class 'str'> 49
>>> sizeof("A")
<class 'str'> 50 A
>>> sizeof("AB")
<class 'str'> 51 AB
>>> sizeof("ABC")
<class 'str'> 52 ABC
An empty string is 49 bytes, it grows linearly with the length of the string.
Next, lets explore lists, dicts, tuples & sets,
>>> sizeof([])
<class 'list'> 64 []
>>> sizeof([1,2,3,4,5,6,7,8,9,10])
<class 'list'> 144 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
An empty list occupies 64 bytes, it grows with the number of elements.
>>> sizeof(())
<class 'tuple'> 48 ()
>>> sizeof((1,))
<class 'tuple'> 56 (1,)
>>> sizeof((1,2,3,4,5,6,7,8,9,10,))
<class 'tuple'> 128 (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
An empty tuple occupies 48 bytes, it grows with the number of elements.
>>> sizeof({})
<class 'dict'> 240 {}
>>> sizeof({"a" : 1})
<class 'dict'> 240 {'a': 1}
>>> a = {}
>>> for i in range(20):
... a[i] = i**2
...
>>> sizeof(a)
<class 'dict'> 648 {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81, 10: 100, 11: 121, 12: 144, 13: 169, 14: 196, 15: 225, 16: 256, 17: 289, 18: 324, 19: 361}
An empty dict occupies 240 bytes, it grows with the number of elements.
>>> sizeof(set())
<class 'set'> 224 set()
>>> sizeof({1})
<class 'set'> 224 {1}
>>> sizeof({1,2,3,4,5,6,7,8,9,10})
<class 'set'> 736 {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
An empty set occupies 224 bytes, it grows with the number of elements.
This concludes my brief exploration on the memory consumption of various data types in python. Memory management in Python is easy—if you just don’t care but it proves to be challenge when you scale !
You read more on memory management in Python in the theano docs here.

Javascript to match a specific number using regular expressions

I was using javascript to detect for specific key strokes and while writing the method I thought I'd try regular expressions and the test() method and came up with:
if (/8|9|37|38|46|47|48|49|50|51|52|53|54|55|56|57|96|97|98|99|100|101|102|103|104|105|110/.test(num)) {
// do something if there's a match
}
This doesn't seem to work 100% as some values seem to make it past the regex test, such as 83. I've since moved on, but I'm still curious as to why this didn't work.
This is the completely wrong way to do it.
To answer the question, the regex is matching part of your string. The string 83 passes by matching the 8.
You need to anchor your regex by putting ^( at the beginning and )$ at the end.
The correct way to do this is to make an array of valid numbers, and compare using parseInt.
For example:
var validNumbers = [ 8, 9, 37, 38, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 110 ];
if (validNumbers.indexOf(parseInt(num, 10)) >=0 ) {
//Match
}
You'll need an indexOf function for IE:
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function(needle) {
for(var i = 0; i < this.length; i++) {
if(this[i] === needle) {
return i;
}
}
return -1;
};
}
You need to specify the start and end of the string. Otherwise 8 in 8|… will match the 8 in 83:
/^(8|9|37|38|46|47|48|49|50|51|52|53|54|55|56|57|96|97|98|99|100|101|102|103|104|105|110)$/.test(num)
But you should rather use numeric comparison. If you don’t like to list every number, you can use ranges like this:
function foo(number, numbers) {
for (var i=0; i<numbers.length; ++i) {
if (numbers[i] === number) {
return true;
} else if (numbers[i].constructor === Array) {
if (numbers[i][0] <= number && number <= numbers[i][1]) {
return true;
}
}
}
return false;
}
var numbers = [8, 9, 37, 38, [46, 57], [96, 105], 110];
if (foo(num, numbers)) {
// …
}
If you make a regular expresion like /\b(100|101)/g it will match only 100 and 101 and not 5100, 5101 or ...101...;
The only problem with this is if your are using negative numbers, e.g in case of 101 and -101 both match with the regexp.
I know this because is what I'm facing and want to avoid.
I can share with you an example:
let array = [89, 7, 92, 78, 899, 65, 56, 92, 922, 292, 289, 389, 2879, 2932, 8999];
I want to find how many instances of the number 92 exist in that array. Therefore I am searching for the precise number of 92. And I want to use Regular Expressions in Javascript.
First part comes with the transformation of the array into a string:
let strCheck = array.toString(); // "89,7,92,78,899,65,56,92,922,292,289,389,2879,2932,8999"
let regex = /\b92\b/g;
I used the flag g for global so that it finds all the matches and the \b word boundary as described in MDN.
let arrResult = strCheck.match(regex); // ["92", "92"]
That's it. The tricky part was first to acknowledge that Regular Expressions work with strings, the second was that once I got the string I had to think about getting the number I wanted not as a number but as a string which was going to be surrounded by other characters and reaching out to those other characters, helped to find the solution.

Are Javascript arrays sparse?

That is, if I use the current time as an index into the array:
array[Date.getTime()] = value;
will the interpreter instantiate all the elements from 0 to now? Do different browsers do it differently?
I remember there used to be a bug in the AIX kernel, which would create pseudo-ttys on request, but if you did, say, "echo > /dev/pty10000000000" it would create /dev/pty0, /dev/pty1, .... and then fall over dead. It was fun at trade shows, but I don't want this to happen to my customers.
Yes, they are. They are actually hash tables internally, so you can use not only large integers but also strings, floats, or other objects. All keys get converted to strings via toString() before being added to the hash. You can confirm this with some test code:
<script>
var array = [];
array[0] = "zero";
array[new Date().getTime()] = "now";
array[3.14] = "pi";
for (var i in array) {
alert("array["+i+"] = " + array[i] + ", typeof("+i+") == " + typeof(i));
}
</script>
Displays:
array[0] = zero, typeof(0) == string
array[1254503972355] = now, typeof(1254503972355) == string
array[3.14] = pi, typeof(3.14) == string
Notice how I used for...in syntax, which only gives you the indices that are actually defined. If you use the more common for (var i = 0; i < array.length; ++i) style of iteration then you will obviously have problems with non-standard array indices.
How exactly JavaScript arrays are implemented differs from browser to browser, but they generally fall back to a sparse implementation - most likely the same one used for property access of regular objects - if using an actual array would be inefficient.
You'll have to ask someone with more knowledge about specific implementations to answer what excatly triggers the shift from dense to sparse, but your example should be perfectly safe. If you want to get a dense array, you should call the constructor with an explicit length argument and hope you'll actually get one.
See this answer for a more detailed description by olliej.
You could avoid the issue by using a javascript syntax designed for this sort of thing. You can treat it as a dictionary, yet the "for ... in ... " syntax will let you grab them all.
var sparse = {}; // not []
sparse["whatever"] = "something";
Javascript objects are sparse, and arrays are just specialized objects with an auto-maintained length property (which is actually one larger than the largest index, not the number of defined elements) and some additional methods. You are safe either way; use an array if you need it's extra features, and an object otherwise.
The answer, as is usually true with JavaScript, is "it's a bit wierder...."
Memory usage is not defined and any implementation is allowed to be stupid. In theory, const a = []; a[1000000]=0; could burn megabytes of memory, as could const a = [];. In practice, even Microsoft avoids those implementations.
Justin Love points out, the length attribute is the highest index set. BUT its only updated if the index is an integer.
So, the array is sparse. BUT built-in functions like reduce(), Math.max(), and "for ... of" will walk through the entire range of possible integer indices form 0 to the length, visiting many that return 'undefined'. BUT 'for ... in' loops might do as you expect, visiting only the defined keys.
Here's an example using Node.js:
"use strict";
const print = console.log;
let a = [0, 10];
// a[2] and a[3] skipped
a[4] = 40;
a[5] = undefined; // which counts towards setting the length
a[31.4] = 'ten pi'; // doesn't count towards setting the length
a['pi'] = 3.14;
print(`a.length= :${a.length}:, a = :${a}:`);
print(`Math.max(...a) = :${Math.max(a)}: because of 'undefined values'`);
for (let v of a) print(`v of a; v=:${v}:`);
for (let i in a) print(`i in a; i=:${i}: a[i]=${a[i]}`);
giving:
a.length= :6:, a = :0,10,,,40,:
Math.max(...a) = :NaN: because of 'undefined values'
v of a; v=:0:
v of a; v=:10:
v of a; v=:undefined:
v of a; v=:undefined:
v of a; v=:40:
v of a; v=:undefined:
i in a; i=:0: a[i]=0
i in a; i=:1: a[i]=10
i in a; i=:4: a[i]=40
i in a; i=:5: a[i]=undefined
i in a; i=:31.4: a[i]=ten pi
i in a; i=:pi: a[i]=3.14
But. There are more corner cases with Arrays not yet mentioned.
Sparseness (or denseness) can be confirmed empirically for NodeJS with the non-standard process.memoryUsage().
Sometimes node is clever enough to keep the array sparse:
Welcome to Node.js v12.15.0.
Type ".help" for more information.
> console.log(`The script is using approximately ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024 * 100) / 100} MB`)
The script is using approximately 3.07 MB
undefined
> array = []
[]
> array[2**24] = 2**24
16777216
> array
[ <16777216 empty items>, 16777216 ]
> console.log(`The script is using approximately ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024 * 100) / 100} MB`)
The script is using approximately 2.8 MB
undefined
Sometimes node chooses to make it dense (this behavior might well be optimized in future):
> otherArray = Array(2**24)
[ <16777216 empty items> ]
> console.log(`The script is using approximately ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024 * 100) / 100} MB`)
The script is using approximately 130.57 MB
undefined
Then sparse again:
> yetAnotherArray = Array(2**32-1)
[ <4294967295 empty items> ]
> console.log(`The script is using approximately ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024 * 100) / 100} MB`)
The script is using approximately 130.68 MB
undefined
So perhaps using a dense array to get a feel for the original AIX kernel bug might need to be forced with a range-alike:
> denseArray = [...Array(2**24).keys()]
[
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71,
72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83,
84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95,
96, 97, 98, 99,
... 16777116 more items
]
> console.log(`The script is using approximately ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024 * 100) / 100} MB`);
The script is using approximately 819.94 MB
undefined
Because why not make it fall over?
> tooDenseArray = [...Array(2**32-1).keys()]
<--- Last few GCs --->
[60109:0x1028ca000] 171407 ms: Scavenge 1072.7 (1090.0) -> 1056.7 (1090.0) MB, 0.2 / 0.0 ms (average mu = 0.968, current mu = 0.832) allocation failure
[60109:0x1028ca000] 171420 ms: Scavenge 1072.7 (1090.0) -> 1056.7 (1090.0) MB, 0.2 / 0.0 ms (average mu = 0.968, current mu = 0.832) allocation failure
[60109:0x1028ca000] 171434 ms: Scavenge 1072.7 (1090.0) -> 1056.7 (1090.0) MB, 0.2 / 0.0 ms (average mu = 0.968, current mu = 0.832) allocation failure
<--- JS stacktrace --->
==== JS stack trace =========================================
0: ExitFrame [pc: 0x100931399]
1: StubFrame [pc: 0x1008ee227]
2: StubFrame [pc: 0x100996051]
Security context: 0x1043830808a1 <JSObject>
3: /* anonymous */ [0x1043830b6919] [repl:1] [bytecode=0x1043830b6841 offset=28](this=0x104306fc2261 <JSGlobal Object>)
4: InternalFrame [pc: 0x1008aefdd]
5: EntryFrame [pc: 0x1008aedb8]
6: builtin exit frame: runInThisContext(this=0x104387b8cac1 <ContextifyScript map = 0x1043...
FATAL ERROR: invalid array length Allocation failed - JavaScript heap out of memory
Writing Node.js report to file: report.20200220.220620.60109.0.001.json
Node.js report completed
1: 0x10007f4b9 node::Abort() [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
2: 0x10007f63d node::OnFatalError(char const*, char const*) [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
3: 0x100176a27 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
4: 0x1001769c3 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
5: 0x1002fab75 v8::internal::Heap::FatalProcessOutOfMemory(char const*) [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
6: 0x1005f3e9b v8::internal::Runtime_FatalProcessOutOfMemoryInvalidArrayLength(int, unsigned long*, v8::internal::Isolate*) [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
7: 0x100931399 Builtins_CEntry_Return1_DontSaveFPRegs_ArgvOnStack_NoBuiltinExit [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
8: 0x1008ee227 Builtins_IterableToList [/Users/pzrq/.nvm/versions/node/v12.15.0/bin/node]
Abort trap: 6
They can be but they don't always have to be, and they can perform better when they're not.
Here's a discussion about how to test for index sparseness in an array instance:
https://benmccormick.org/2018/06/19/code-golf-sparse-arrays/
This code golf (fewest characters) winner is:
let isSparse = a => !!a.reduce(x=>x-1,a.length)
Basically walking the array for indexed entries while decrementing the length value and returning the hardened !! boolean of the falsy/truthy numerical result (if the accumulator is decremented all the way to zero, the index is fully populated and not sparse). Charles Merriam's caveats above should be considered as well and this code doesn't address them, but they apply to hashed string entries which can happen when assigning elements with arr[var]= (something) where var wasn't an integer.
On reason to care about index sparseness is its effects on performance, which can differ between script engines, there's a great discussion about array creation/.initialization here:
What’s the difference between "Array()" and "[]" while declaring a JavaScript array?
A recent answer to that post has a link to this deep dive into how V8 tries to optimize arrays by tagging them to avoid (re-)testing for characteristics like sparseness: https://v8.dev/blog/elements-kinds. The blog post is from Sept '17 and the material is subject to some change, but the breakdown to implications for day-to-day development is useful and clear.

Categories