Skip to content

Commit

Permalink
perf(base64): Replace random access with iteration
Browse files Browse the repository at this point in the history
XS is O(n) for the former (not that it really matters here because they
also supply a native base64 implementation).

```console
$ echo "Node.js" && node test/bench-main.js && echo "XS" && ~/.esvu/bin/xs -m test/bench-main.js
Node.js
encodes 9801.535832414553 characters per millisecond
JS encodes 18329.573806881242 characters per millisecond
decodes 45848.339350180504 bytes per millisecond
JS decodes 45448.94117647059 bytes per millisecond
XS
encodes 578303.1160714285 characters per millisecond
JS encodes 1700.2734761120264 characters per millisecond
decodes 393870.26373626373 bytes per millisecond
JS decodes 1946.3762376237623 bytes per millisecond
```
  • Loading branch information
gibson042 committed Jan 18, 2024
1 parent debc574 commit e5163c4
Show file tree
Hide file tree
Showing 2 changed files with 29 additions and 21 deletions.
47 changes: 28 additions & 19 deletions packages/base64/src/decode.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,45 +19,54 @@ import { monodu64, padding } from './common.js';
* @returns {Uint8Array} decoded bytes
*/
export const jsDecodeBase64 = (string, name = '<unknown>') => {
const data = new Uint8Array(Math.ceil((string.length * 4) / 3));
// eslint-disable-next-line no-nested-ternary
const paddingLength = string.endsWith('==')
? 2
: string.endsWith('=')
? 1
: 0;
const data = new Uint8Array((string.length * 3) / 4 - paddingLength);
let register = 0;
let quantum = 0;
let i = 0; // index in string
let j = 0; // index in data
let done = false;

while (i < string.length && string[i] !== padding) {
const number = monodu64[string[i]];
for (const ch of string) {
if (done || ch === padding) {
done = true;
// Padding is only expected to complete a short chunk of two or three data characters
// (i.e., the last two in the `quantum` cycle of [0, 6, 12 - 8 = 4, 10 - 8 = 2]).
if (quantum === 4 || quantum === 2) {
quantum -= 2;
i += 1;
// eslint-disable-next-line no-continue
continue;
}
break;
}
const number = monodu64[ch];
if (number === undefined) {
throw Error(`Invalid base64 character ${string[i]} in string ${name}`);
throw Error(`Invalid base64 character ${ch} in string ${name}`);
}
register = (register << 6) | number;
quantum += 6;
if (quantum >= 8) {
quantum -= 8;
data[j] = register >>> quantum;
data[j] = register >> quantum;
j += 1;
register &= (1 << quantum) - 1;
}
i += 1;
}

while (i < string.length && quantum % 8 !== 0) {
if (string[i] !== padding) {
throw Error(`Missing padding at offset ${i} of string ${name}`);
}
i += 1;
quantum += 6;
}

if (i < string.length) {
throw Error(
`Base64 string has trailing garbage ${string.substr(
i,
)} in string ${name}`,
`Base64 string has trailing garbage ${string.slice(i)} in string ${name}`,
);
} else if (quantum !== 0) {
throw Error(`Missing padding at offset ${i} of string ${name}`);
}

return data.subarray(0, j);
return data;
};

// The XS Base64.decode function is faster, but might return ArrayBuffer (not
Expand Down
3 changes: 1 addition & 2 deletions packages/base64/src/encode.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ export const jsEncodeBase64 = data => {
let register = 0;
let quantum = 0;

for (let i = 0; i < data.length; i += 1) {
const b = data[i];
for (const b of data) {
register = (register << 8) | b;
quantum += 8;
if (quantum === 24) {
Expand Down

0 comments on commit e5163c4

Please sign in to comment.