Context
When dealing with large sets of points, should we use [number, number]
or {x: number, y: number}
.
Conventionally, using an array is believed to offer better performance. Arrays are ordered data structures, and accessing an element requires only a base address plus an offset. In contrast, accessing properties of an object involves a hash table lookup to find the offset before accessing the memory value. This makes arrays seem advantageous.
However, test results were surprisingly counterintuitive.
Testing
Ideally, we should measure both runtime and memory usage in a browser environment. However, attempts to monitor memory usage via window.performance.memory.usedJSHeapSize
were unsuccessful, as the value remained unchanged.
I suspected that the garbage collector (GC) was working too quickly, but even when holding onto the data until after printing, there was no change in memory usage. In fact, the entire window.performance.memory
remained static. Therefore, memory usage was not considered in this test.
- I hypothesize that the V8 JavaScript engine pre-allocates sufficient memory upon startup, so under normal circumstances, memory usage does not increase.
Testing Time
The test code is as follows:
- The number of iterations is 1 million.
- Data is generated and pushed into a collection.
- The data is traversed and accessed.
- The data is traversed and modified.
const testCount = 1000000
function testArray() {
const points = []
for (let i = 0; i < testCount; i++) {
points.push([Math.random(), Math.random()])
}
let sumX = 0,
sumY = 0
for (let i = 0; i < testCount; i++) {
sumX += points[i][0]
sumY += points[i][1]
}
for (let i = 0; i < testCount; i++) {
points[i][0] += 1
points[i][1] += 1
}
}
function testObject() {
const points = []
for (let i = 0; i < testCount; i++) {
points.push({ x: Math.random(), y: Math.random() })
}
let sumX = 0,
sumY = 0
for (let i = 0; i < testCount; i++) {
sumX += points[i].x
sumY += points[i].y
}
for (let i = 0; i < testCount; i++) {
points[i].x += 1
points[i].y += 1
}
}
The results were neck and neck, even when the number of iterations was increased to 10 million.
Optimized Testing
To prevent cold start issues, the order of execution was randomized, and multiple runs were averaged.
function testTime() {
const iterations = 10
for (let i = 0; i < iterations; i++) {
if (Math.random() > 0.5) {
// array => object
} else {
// object => array
}
}
}
The results remained similar.
Adding Properties
With too few properties, the test was not comprehensive.
points.push([
Math.random(),
Math.random(),
`color${i % 10}`,
i % 100,
Math.random(),
Math.random(),
Math.random(),
Math.random(),
Math.random(),
Math.random()
])
points.push({
coordX: Math.random(),
coordY: Math.random(),
hue: `color${i % 10}`,
mass: i % 100,
attrAlpha: Math.random(),
attrBeta: Math.random(),
attrGamma: Math.random(),
attrDelta: Math.random(),
attrEpsilon: Math.random(),
attrZeta: Math.random()
})
Surprisingly, the results were as follows:
// const testCount = 1000000
Average Array Time: 330.960 ms
Average Object Time: 171.580 ms
Median Array Time: 341.200 ms
Median Object Time: 188.700 ms
Arrays were more time-consuming.
Read / Write
First, removing the write operations:
Average Array Time: 195.040 ms
Average Object Time: 143.280 ms
Median Array Time: 186.600 ms
Median Object Time: 140.600 ms
Then, removing the read operations:
Average Array Time: 177.260 ms
Average Object Time: 102.540 ms
Median Array Time: 188.800 ms
Median Object Time: 103.900 ms
Time Conclusion
In cases with multiple values, objects perform better. The time consumption is concentrated in the write operations. Even the push operation is faster for objects.
Modern JavaScript engines (such as V8) have highly optimized property access for objects, especially when the object structure is fixed (i.e., the number and type of properties remain unchanged).
Other Tests
I attempted to randomly add new properties to each object and access random properties that may or may not exist. The result was a browser crash.
Testing Memory
I found a way to test memory usage. Browsers automatically display the current memory usage. By running different scenarios and observing the memory usage, a rough comparison can be made.
The test method involves repeatedly closing and reopening the browser to observe memory usage. The Task Manager can also be used to monitor memory usage.
After changing the test content and continuing to observe:
Object: 600+ MB
Array: 800 ~ 1200 MB
Memory Conclusion
In terms of memory usage, objects still perform better.
Conclusion
In most cases, a collection of objects with fixed properties is more suitable for processing, and there is no need to optimize performance by using arrays.
Node.js Case
Running the current code directly in Node.js:
$ node -v
v21.6.2
$ node test.js
Average Array Time: 598.258 ms
Average Object Time: 312.044 ms
Median Array Time: 678.761 ms
Median Object Time: 244.425 ms
Arrays remain more time-consuming.
Test Code for Reference
If there is any suspicion that the code might be incorrect, please refer to the specific code.
function testArray() {
const points = []
for (let i = 0; i < testCount; i++) {
points.push([
Math.random(),
Math.random(),
`color${i % 10}`,
i % 100,
Math.random(),
Math.random(),
Math.random(),
Math.random(),
Math.random(),
Math.random()
])
}
let sumX = 0,
sumY = 0,
sumWeight = 0,
sumAttr1 = 0,
sumAttr2 = 0,
sumAttr3 = 0,
sumAttr4 = 0,
sumAttr5 = 0,
sumAttr6 = 0
for (let i = 0; i < testCount; i++) {
const point = points[i]
sumX += point[0]
sumY += point[1]
sumWeight += point[3]
sumAttr1 += point[4]
sumAttr2 += point[5]
sumAttr3 += point[6]
sumAttr4 += point[7]
sumAttr5 += point[8]
sumAttr6 += point[9]
}
for (let i = 0; i < testCount; i++) {
const point = points[i]
point[0] += 1
point[1] += 1
point[3] += 1
point[4] += 1
point[5] += 1
point[6] += 1
point[7] += 1
point[8] += 1
point[9] += 1
}
}
function testObject() {
const points = []
for (let i = 0; i < testCount; i++) {
points.push({
coordX: Math.random(),
coordY: Math.random(),
hue: `color${i % 10}`,
mass: i % 100,
attrAlpha: Math.random(),
attrBeta: Math.random(),
attrGamma: Math.random(),
attrDelta: Math.random(),
attrEpsilon: Math.random(),
attrZeta: Math.random()
})
}
let sumX = 0,
sumY = 0,
sumWeight = 0,
sumAttr1 = 0,
sumAttr2 = 0,
sumAttr3 = 0,
sumAttr4 = 0,
sumAttr5 = 0,
sumAttr6 = 0
for (let i = 0; i < testCount; i++) {
const point = points[i]
sumX += point.coordX
sumY += point.coordY
sumWeight += point.mass
sumAttr1 += point.attrAlpha
sumAttr2 += point.attrBeta
sumAttr3 += point.attrGamma
sumAttr4 += point.attrDelta
sumAttr5 += point.attrEpsilon
sumAttr6 += point.attrZeta
}
for (let i = 0; i < testCount; i++) {
const point = points[i]
point.coordX += 1
point.coordY += 1
point.mass += 1
point.attrAlpha += 1
point.attrBeta += 1
point.attrGamma += 1
point.attrDelta += 1
point.attrEpsilon += 1
point.attrZeta += 1
}
}