Coordinate pairs in JavaScript
Any application that works with 2D spaces, grids or tile maps needs a way of representing individual points as coordinate pairs. These will be extremely common short-lived objects in a wide variety of scenarios so I was curious to see the impacts of different representations across different browsers.
Overview
There’s a huge variety of things to look at here. To scratch the surface, I constructed a series of jsPerf benchmarks using the common pattern of enumerating neighbouring grid cells for a given x,y point as an adjacency list (without actually using an adjacency list data structure).
This approach is widely used to generate maps using cellular automata and run pathfinding algorithms, so the tests roughly approximate the kinds of real workloads one might see in a game or simulation.
The tests are initialised with a set of identical map grids containing a random distribution of cells marked as either ‘ocean’ or ‘land’ tiles. Each test iterates through every cell in the map and flips the marked tile if the majority of surrounding cells are a different kind of tile.
Representations
Array with two indices
[x, y]
Object literal with x,y properties
{
x: x,
y: y
}
Prototype constructor with x,y properties
function Point(x, y) {
this.x = x
this.y = y
}
ES6 class with getters
class Position {
constructor(x, y) {
this._x = x
this._y = y
}
get x() {
return this._x
}
get y() {
return this._y
}
}
Disclaimer
These benchmarks aren’t authoritative or definitive. At best, they give a rough overview of the characteristics of allocating memory in several different ways in the specific context of these tests.
It’s possible that the way I’ve implemented the loops could be confounding the measurements and there are a couple of changes I can make that will have a major impact on garbage collection.
Browser JavaScript engines change often (usually getting faster and faster) so this is really just a snapshot in time. YMMV.
Measurements
All results measured in ops/sec (higher is better).
Tested on: Mac OS X 10.12.5, 2.2 GHz Intel Core i7, 16 GB 1600 MHz DDR3.
Array with two indices
Grid size | Safari 10.1.1 | Firefox 54.0.0 | Chrome 59.0.3071 |
---|---|---|---|
2000 | 2.17 (±0.86%) | 1.30 (±20.41%) | 2.09 (±7.21%) |
1000 | 7.81 (±1.01%) | 4.20 (±9.84%) | 6.86 (±5.95%) |
500 | 30.55 (±2.60%) | 15.83 (±2.69%) | 33.02 (±1.93%) |
200 | 127 (±3.23%) | 99.36 (±0.67%) | 214 (±1.59%) |
100 | 152 (±16.04%) | 387 (±0.50%) | 854 (±1.43%) |
Object literal with x,y properties
Grid size | Safari 10.1.1 | Firefox 54.0.0 | Chrome 59.0.3071 |
---|---|---|---|
2000 | 2.39 (±1.51%) | 1.11 (±1.61%) | 2.33 (±6.94%) |
1000 | 8.56 (±0.98%) | 4.04 (±2.41%) | 7.69 (±5.69%) |
500 | 33.24 (±2.76%) | 16.56 (±1.01%) | 37.31 (±2.04%) |
200 | 157 (±5.30%) | 101 (±0.54%) | 251 (±2.19%) |
100 | 261 (±31.46%) | 419 (±0.97%) | 987 (±1.32%) |
Prototype constructor with x,y properties
Grid size | Safari 10.1.1 | Firefox 54.0.0 | Chrome 59.0.3071 |
---|---|---|---|
2000 | 2.38 (±0.90%) | 1.07 (±2.50%) | 2.28 (±3.56%) |
1000 | 8.63 (±0.68%) | 4.11 (±1.21%) | 7.60 (±8.21%) |
500 | 33.24 (±2.55%) | 16.44 (±0.64%) | 35.50 (±3.16%) |
200 | 158 (±3.91%) | 101 (±0.60%) | 244 (±2.30%) |
100 | 197 (±26.54%) | 413 (±1.32%) | 991 (±1.79%) |
ES6 class with getters
Grid size | Safari 10.1.1 | Firefox 54.0.0 | Chrome 59.0.3071 |
---|---|---|---|
2000 | 2.41 (±1.69%) | 1.02 (±3.24%) | 1.11 (±3.24%) |
1000 | 8.56 (±0.86%) | 3.94 (±0.93%) | 3.86 (±2.59%) |
500 | 33.33 (±2.61%) | 15.48 (±0.84%) | 15.77 (±5.08%) |
200 | 139 (±3.61%) | 93.09 (±0.66%) | 96.87 (±1.56%) |
100 | 157 (±18.82%) | 386 (±1.01%) | 395 (±1.11%) |
ES5 Object.assign
Grid size | Safari 10.1.1 | Firefox 54.0.0 | Chrome 59.0.3071 |
---|---|---|---|
2000 | 0.10 (±0.59%) | 0.06 (±43.13%) | 0.20 (±0.57%) |
1000 | 0.41 (±1.79%) | 0.26 (±44.64%) | 0.78 (±2.99%) |
500 | 1.59 (±2.65%) | 0.92 (±21.03%) | 3.07 (±2.74%) |
200 | 7.96 (±4.50%) | 5.29 (±6.94%) | 17.42 (±1.62%) |
100 | 28.80 (±2.27%) | 20.44 (±1.37%) | 68.08 (±1.23%) |
Chrome
Firefox
Safari
Summary
There’s probably nothing here that is remarkable or unexpected.
Chrome wins at smaller collection sizes. Safari wins at larger collection sizes.
Between 200 and 500 cells, garbage collection seems to wash out any major differences between implementations.
Storing points in two element arrays is slightly slower than using object properties. Firefox arrays are closer to object properties in performance than in the other browsers.
Safari seems to exhibit a much greater standard deviation at smaller grid sizes.
In general, getter method wrappers are slower than direct property access.
Don’t use Object.assign
in hot code paths. I included this out of curiosity and unsurprisingly, it was much, much slower. It’s also not a great fit for the problem of creating thousands of short-lived objects (though this isn’t a reason to avoid it in general—it’s a really useful feature!).
Resources
- Fast properties in V8 explains how properties are handled internally in the V8 engine, which provides useful context for understanding these results.