In this article I compare the performance of the spread operator ...
and the performance of the assignement operator =
in the context of data transformation.
I show that using the spread operator isn't a trivial choice to make and I suggest that immutability and mutation don't have to be mutually exclusive. I also show how one-liner functions can be enriched with the comma operator ,
.
We're measuring the performance of transforming an array of records into a map e.g.,
From:
[
[
"bda579a3-c259-4529-b845-12644a22cdbc",
"Jeff_Thompson26@example.org"
],
[
"389c1582-a694-41a2-b867-2f6fd05e7c91",
"Lilly.Lockman@example.com"
],
[
"4b204b42-13f5-487e-929f-0d64ee1c122b",
"Marianna62@example.com"
]
]
To:
{
"bda579a3-c259-4529-b845-12644a22cdbc": "Jeff_Thompson26@example.org",
"389c1582-a694-41a2-b867-2f6fd05e7c91": "Lilly.Lockman@example.com",
"4b204b42-13f5-487e-929f-0d64ee1c122b": "Marianna62@example.com"
}
We'll be using this script to generate an array of records:
// generate-records.js
const { v4: uuid } = require('uuid');
const faker = require('faker');
const records = [];
for (let i = 0; i < process.env.RECORD; i++) {
records.push([
uuid(),
faker.internet.exampleEmail()
]);
}
console.log(JSON.stringify(records, null, 2));
To create a JSON file of 100 records we'll invoke the script as follow:
RECORD=100 node generate-records.js > records.json
// to-map-spread.js
const records = require('./records');
const to_map =
rs =>
rs.reduce
( (o, [k, v]) => ({...o, [k]: v})
, {}
);
to_map(records);
// to-map-assign.js
const records = require('./records');
const to_map =
rs =>
rs.reduce
( (o, [k, v]) => (o[k] = v, o)
, {}
);
to_map(records);
It is worth noting that the comma operator ,
is used to chain expressions and return the last expression. It is equivalent to:
(o, [k, v]) => {
o[k] = v;
return o;
}
The two transformation methods will be compared against datasets of different sizes:
- 100 records
- 500 records
- 1000 records
- 2000 records
- 5000 records
- 10000 records
We'll use this script to automate the generation of records and each run:
#!/bin/sh
run () {
RECORD=$1 node generate-records.js > records.json
echo "$1 - spread"
time node to-map-spread.js
echo "$1 - assign"
time node to-map-assign.js
}
run 100
run 500
run 1000
run 2000
run 5000
run 10000
Only the real time will be taken into account.
Here are the results for one run on my MacBook Pro using Node.js 12
(2.5 GHz Intel Core i7 ∙ 16 GB 1600 MHz DDR3 ∙ macOS 10.14.6 (18G4032))
Records | Spread | Assign |
---|---|---|
100 | 42ms | 41ms |
500 | 62ms | 41ms |
1000 | 151ms | 41ms |
2000 | 969ms | 41ms |
5000 | 6.163s | 49ms |
10000 | 24.758s | 56ms |
While the approach to measuring performance could definitely do with more scientific rigour, the results should compel developers to either proceed with caution or investigate further.
The spread operator ...
is a popular choice to create one-liner functions but it seems to quickly perform poorly in an iterative process. In such scenario the use of the spread operator to provide mutation-free computation should be questioned. Consider this contrived example:
const map = xs.reduce((acc, x) => ({...acc, [x]: true}), {});
// ^^
Uncontrolled access to shared state can lead to complex bugs which is why immutability is so dear to functional programmers. However immutability isn't only achieved through the cloning of data, in fact this is most likely to be a naive approach to it. Immutability is a quality of a data structure not a process per se.
In this particular case the initial value of the reducing function is safe to mutate. The spread operator offers no additional protection against side effects.
Good one..