Skip to content
This repository has been archived by the owner on Dec 3, 2022. It is now read-only.

Optimize encode and decode with TypedArrays #86

Closed
wants to merge 7 commits into from

Conversation

jridgewell
Copy link
Contributor

@jridgewell jridgewell commented Jan 17, 2022

For decode, the optimization is to convert charToInteger to a Uint8Array. Indexed lookups on a TypedArray are faster than property lookups on an object. With that, we decode 1, 4, or 5 ints at a time, instead of looping through multiple times for each char. This leads to a ~20% speedup.

For encode, we switch to a preallocated Uint8Array to store the VLQ data. When needed, we'll grow it, and push the next ASCII byte onto it. At the end, we use a TextDecoder to quickly convert to a string. This leads to a ~2.5x speedup.


Running this on my M1 MBP (10 core, 32gb):

Running node v14.18.3 (npm v6.14.15)
decode (latest) x 39,390 ops/sec ±0.20% (94 runs sampled)
decode (proposal) x 47,696 ops/sec ±0.12% (97 runs sampled)
Fastest is decode (proposal)

encode (latest) x 19,949 ops/sec ±0.09% (99 runs sampled)
encode (proposal) x 48,402 ops/sec ±0.93% (93 runs sampled)
Fastest is encode (proposal)


Running node v16.13.2 (npm v8.1.2)
decode (latest) x 39,033 ops/sec ±0.42% (97 runs sampled)
decode (proposal) x 47,220 ops/sec ±0.11% (98 runs sampled)
Fastest is decode (proposal)

encode (latest) x 19,109 ops/sec ±0.13% (100 runs sampled)
encode (proposal) x 49,603 ops/sec ±0.68% (99 runs sampled)
Fastest is encode (proposal)


Running node v17.3.1 (npm v8.3.0)
decode (latest) x 38,961 ops/sec ±0.23% (99 runs sampled)
decode (proposal) x 47,153 ops/sec ±0.13% (98 runs sampled)
Fastest is decode (proposal)

encode (latest) x 19,034 ops/sec ±0.16% (101 runs sampled)
encode (proposal) x 34,013 ops/sec ±0.38% (100 runs sampled)
Fastest is encode (proposal)
benchmark.js
const Benchmark = require('benchmark');
const latest = require('./latest');
const proposal = require('./');
const map = JSON.parse(require("fs").readFileSync(`dist/sourcemap-codec.umd.js.map`));
const mappings = map.mappings;
const decoded = latest.decode(mappings);

console.log(process.version);

const decode = new Benchmark.Suite;
decode
	.add('decode (latest)', () => {
		latest.decode(mappings);
	})
	.add('decode (proposal)', () => {
		proposal.decode(mappings);
	})
	// add listeners
	.on('cycle', (event) => {
		console.log(String(event.target));
	})
	.on('complete', function () {
		console.log('Fastest is ' + this.filter('fastest').map('name'));
	})
	.run({});

console.log('');

const encode = new Benchmark.Suite;
encode
	.add('encode (latest)', () => {
		latest.encode(decoded);
	})
	.add('encode (proposal)', () => {
		proposal.encode(decoded);
	})
	// add listeners
	.on('cycle', (event) => {
		console.log(String(event.target));
	})
	.on('complete', function () {
		console.log('Fastest is ' + this.filter('fastest').map('name'));
	})
	.run({});

For `decode`, the optimization is to convert `charToInteger` to a `Uint8Array`. Indexed lookups on a `TypedArray` are faster than property lookups on an object. This leads to a ~7% speedup.

- - -

For `encode`, we switch to a preallocated `Uint8Array` to store the VLQ data. When needed, we'll grow it, and push the next ASCII byte onto it. At the end, we use a `TextDecoder` to quickly convert to a string.

- - -

Running this on my M1 MBP (10 core, 32gb):

```
$ nvm run 14 benchmark.js
Running node v14.18.3 (npm v6.14.15)
v14.18.3
decode (latest) x 40,250 ops/sec ±0.35% (99 runs sampled)
decode (proposal) x 42,866 ops/sec ±0.13% (101 runs sampled)
Fastest is decode (proposal)

encode (latest) x 20,283 ops/sec ±0.18% (99 runs sampled)
encode (proposal) x 50,049 ops/sec ±1.00% (94 runs sampled)
Fastest is encode (proposal)

$ nvm run 16 benchmark.js
Running node v16.13.0 (npm v8.1.0)
v16.13.0
decode (latest) x 39,872 ops/sec ±0.42% (100 runs sampled)
decode (proposal) x 42,522 ops/sec ±0.12% (100 runs sampled)
Fastest is decode (proposal)

encode (latest) x 18,902 ops/sec ±0.23% (99 runs sampled)
encode (proposal) x 52,375 ops/sec ±0.53% (97 runs sampled)
Fastest is encode (proposal)
$ nvm run 17 benchmark.js
Running node v17.3.1 (npm v8.3.0)
v17.3.1
decode (latest) x 39,976 ops/sec ±0.26% (101 runs sampled)
decode (proposal) x 42,265 ops/sec ±0.11% (99 runs sampled)
Fastest is decode (proposal)

encode (latest) x 19,598 ops/sec ±0.15% (98 runs sampled)
encode (proposal) x 36,023 ops/sec ±0.37% (98 runs sampled)
Fastest is encode (proposal)
```

<details>

<summary>
`benchmark.js`
<summary>

```js
const Benchmark = require('benchmark');
const latest = require('./latest');
const proposal = require('./');
const map = JSON.parse(require("fs").readFileSync(`dist/sourcemap-codec.umd.js.map`));
const mappings = map.mappings;
const decoded = latest.decode(mappings);

console.log(process.version);

const decode = new Benchmark.Suite;
decode
	.add('decode (latest)', () => {
		latest.decode(mappings);
	})
	.add('decode (proposal)', () => {
		proposal.decode(mappings);
	})
	// add listeners
	.on('cycle', (event) => {
		console.log(String(event.target));
	})
	.on('complete', function () {
		console.log('Fastest is ' + this.filter('fastest').map('name'));
	})
	.run({});

console.log('');

const encode = new Benchmark.Suite;
encode
	.add('encode (latest)', () => {
		latest.encode(decoded);
	})
	.add('encode (proposal)', () => {
		proposal.encode(decoded);
	})
	// add listeners
	.on('cycle', (event) => {
		console.log(String(event.target));
	})
	.on('complete', function () {
		console.log('Fastest is ' + this.filter('fastest').map('name'));
	})
	.run({});
```

</details>
@jridgewell
Copy link
Contributor Author

Friendly ping @Rich-Harris, is there anything you'd like me to do with this PR?

@jridgewell jridgewell closed this May 6, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant