YYSuni
cover

Array vs Object Performance

When dealing with large arrays of data, should we use Array or Object.

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
	}
}