1

I'm trying to make Conway's game of life in JavaScript and canvas, I have a matrix of 1280x720 that I use to store cells data, I'm currently storing the data as 1 = alive, 0 = dead, and then when I check if a cell is alive or not I simply do: if(matrix[i][j]) I was curious if this could be improved and did some tests at https://jsbench.me/ replicating a similar scenario and noticed that if using "true/false", the whole thing is +-11% slower, why is it the case? Shouldn't it be faster?

Example benchmark, just change 1 to true to test the other scenario

let array = []
for(let i = 0; i<1000000; i++){
   array.push(1)
}
let sum = 0
for(let i = 0; i<1000000;i++){
    if(array[i]){
        sum++
    }
}
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Specy
  • 189
  • 2
  • 13

2 Answers2

6

The performance difference you see isn't strictly due to the if statement evaluation, it's due to the array element kind that the value (1 or true) is accessed from. The V8 engine distinguishes between arrays of different element kinds. An array of 1 will be treated as PACKED_SMI_ELEMENTS, while an array of true will be treated as PACKED_ELEMENTS. Because of that, the version using boolean elements will be a little bit slower.

As an illustration, here's the lattice of relative performance optimizations applied between array element kinds, with best performance at the top left, and worst performance at the bottom right:

element kinds lattice

And here's a benchmark comparing both your tests to one I added based on my comment below:

benchmark results

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
  • So my best bet is keep using 1/0? , Is there any other way I can increase performance? – Specy Aug 12 '20 at 22:28
  • @Specy you could use a `Uint8Array` of length 921600 (1280x720) and access each index by `if (matrix[j * 1280 + i])`. If you wanted to use even less memory at a minor cost to speed, you could even store 8 cells per index and use bit arithmetic to access each cell's data. – Patrick Roberts Aug 12 '20 at 22:37
  • If you are looking to increase the performance of your game, Conway's game of life, you should look at the bottlenecks on your actual code. Not an artificial benchmark. However, if in your code you iterate over an array of values such as in the benchmark, you probably will be better of using PACKED_SMI_ELEMENTS (i.e. 1/0). You might gain some performance changing the 2D array for a 1D implementation like Patrick said. – Pablo K Aug 12 '20 at 22:39
  • This answer is incorrect; iteration speed is exactly the same for all elements kinds (you can look at the generated optimized code for yourself to verify!). The observed difference here is due to two things: (1) about 90% of the overall time is spent *creating* the array, which means repeatedly growing it, which must do more (GC-related) work for PACKED_ELEMENTS. (2) when finally iterating, the `if` statement, when loading an object, must check whether that object is a true-ish value, whereas for a SMI array it can simply do a ` != 0` comparison, which is faster. – jmrk Aug 12 '20 at 23:43
  • @jmrk (1) array creation is not included within the timed region of the benchmark, so that's irrelevant. (2) the array element kind affects the performance of element access (and consequently, iteration). This has nothing to do with "true-ish" evaluation in the conditional statement, as proved [here](https://jsbench.me/ojkdryuq91/2). In fact, the boolean condition is marginally faster than the numeric condition in the current version of Chrome. – Patrick Roberts Aug 12 '20 at 23:57
  • In the OP, array creation is timed. Your additional "`if(1)` vs `if(true)`" benchmark doesn't prove what you think it does -- interpreting microbenchmarks correctly *always* requires checking the generated code. See my answer for more details. Again, both iteration and element access have the same speed for all (non-dictionary) array elements kinds; the elements kind tracking (sometimes) makes *subsequent* operations faster by allowing them to skip some checks. – jmrk Aug 13 '20 at 00:14
  • @jmrk after reading this comment (and your answer), your explanation makes much more sense now than your initial comment did, especially when evaluating it against the empirical evidence I provided. I will admit that your clarification about the resulting performance of element access vs subsequent operations on the value seems a lot like a distinction without a(n observable) difference, though. Is there _any_ practical situation where that distinction actually matters? – Patrick Roberts Aug 13 '20 at 00:21
2

(V8 developer here.)

In short, the 1/0 version is faster because the array's elements kind helps the if statement do less work.

Longer version: As @PatrickRoberts points out, V8 keeps track of the type of values stored in an array. This mechanism is rather coarse-grained, it only distinguishes between "just integers", "just doubles", and "anything". if(array[i]), when it knows that the array contains only integers, can simply do a comparison against 0 to see if the branch should be taken. It doesn't get faster than that. If the array contains "anything" (which includes true), however, then per the semantics of JavaScript, V8 has to check whether the loaded value is "true-ish", i.e. evaluates to true in a conditional context. The opposite, i.e. checking for false-ish values, is actually easier/faster, so V8 checks: is the value false? Is it ""? Is it a number (which might be 0)? Is it a BigInt (which might be 0n)? Is it document.all (a particularly fun special-case relic from ancient times)? Anything else evaluates to true. In this particular case, it would be "smart"/lucky to check for true right away, but the engine can't know that, and such a heuristic wouldn't be beneficial in general.

(Note that it would be wrong to conclude that if(1) is faster than if(true) -- what matters specifically is that the value in the conditional is loaded from an array, and this array keeps track of the range of possible values, which affects the checks that subsequently need or don't need to be done on a loaded value. When you use constants 1 and true, then both evaluations have the same speed (in fact, in most situations the optimizing compiler will drop them entirely, because of course if(true) is true, duh!).)

That said, most of the difference you see isn't due to this, because the test spends more than 90% of its time in the first loop, populating the array. Growing an array from length 0 to a million means its backing store needs to be extended repeatedly, which means a new backing store is allocated and all existing elements are copied over. This is another operation where integer-only elements have a speed benefit: they can use a bulk copying operation, moving data as fast as the CPU can access memory. In an "anything" array, however, the garbage collector must perform an additional pass to see if any of the values are references that are of interest to it. In this case, with all values being the true sentinel, they're not, but the GC can't know that without checking.

jmrk
  • 34,271
  • 7
  • 59
  • 74
  • Thanks a lot for the explaination, I've done some other testings and in the end I'll turn the 2d array into a 1d array, and I'll use a Uint8Array and store 1/0s. That should increase performance by a bit. Someone suggested me to use a bitset so I'm also looking into that. – Specy Aug 13 '20 at 00:25
  • @Specy that was me that suggested that. And it wasn't to make your program faster, it was to consume less memory. – Patrick Roberts Aug 13 '20 at 00:27