bun
bun copied to clipboard
nanoid is ~3.5 times slower on bun when compared to node
Version
0.1.4
Platform
Linux s212 5.15.0-41-generic #44~20.04.1-Ubuntu SMP Fri Jun 24 13:27:29 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux (Ubuntu 20.04.4 LTS x86_64)
What steps will reproduce the bug?
Run simple test using Bun:
sudo docker run --entrypoint ash --rm -it jarredsumner/bun:edge -c 'mkdir x && cd x && bun add nanoid@3 && echo "const {nanoid}=require(\"nanoid\");const ts = Date.now();let i = 0;while (true) {nanoid();i++;if (Date.now() > ts + 1000) {console.log(i);process.exit(0);}}" > s.js && bun run s.js'
Result: 469205
Run the same test using Node:
sudo docker run --entrypoint ash --rm -it node:18.5.0-alpine3.16 -c 'mkdir x && cd x && npm init -y && npm i nanoid@3 --save && node -e "const {nanoid}=require(\"nanoid\");const ts = Date.now();let i = 0;while (true) {nanoid();i++;if (Date.now() > ts + 1000) {console.log(i);process.exit(0);}}"'
Result: 1657601
How often does it reproduce? Is there a required condition?
Always.
What is the expected behavior?
Bun should be as fast as node or faster while generating nanoids.
What do you see instead?
Bun is ~3.5 times slower than node while generating nanoids.
Additional information
CPU: Intel(R) Core(TM) i3-5005U CPU @ 2.00GHz
RAM: 2x 4 GB 1600 MHz DDR3L
👍

bun & node:
import { bench, run } from "mitata";
import { nanoid } from "nanoid";
import { save } from "../../summary.mjs";
bench("generate id", () => nanoid(36));
save(await run(), "bun", __dirname);
deno:
import { bench, run } from "../../node_modules/mitata/src/cli.mjs";
import { nanoid } from "https://deno.land/x/nanoid/mod.ts"
import { save } from "../../summary.mjs";
const __dirname = new URL('.', import.meta.url).pathname;
bench("generate id", () => nanoid(36));
save(await run(), "deno", __dirname);
maybe toString(32) is less optimized in JSC compared to v8
This is the js for nanoid:
export let nanoid = (t = 21) =>
crypto
.getRandomValues(new Uint8Array(t))
.reduce(
(t, e) =>
(t +=
(e &= 63) < 36
? e.toString(36)
: e < 62
? (e - 26).toString(36).toUpperCase()
: e > 62
? "-"
: "_"),
""
);
also the crypto random number generator bun is using seems to spend a lot of time looking up the threadlocal variable
would be smarter to instantiate it
oh i see
The implementation of nanoid is completely different for browsers and for node. Bun is using the implementation of nanoid for browsers, which is slower.
The fix here is not in JSC. The fix is to make bun's crypto polyfill work better and disable reading the "browser" field for Bun, so that it imports modules meant for node.
This is the node implementation, which pools buffers unlike the browser implementation.
import { randomFillSync } from 'crypto'
import { urlAlphabet } from './url-alphabet/index.js'
export { urlAlphabet }
const POOL_SIZE_MULTIPLIER = 128
let pool, poolOffset
let fillPool = bytes => {
if (!pool || pool.length < bytes) {
pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER)
randomFillSync(pool)
poolOffset = 0
} else if (poolOffset + bytes > pool.length) {
randomFillSync(pool)
poolOffset = 0
}
poolOffset += bytes
}
export let random = bytes => {
fillPool((bytes -= 0))
return pool.subarray(poolOffset - bytes, poolOffset)
}
export let customRandom = (alphabet, defaultSize, getRandom) => {
let mask = (2 << (31 - Math.clz32((alphabet.length - 1) | 1))) - 1
let step = Math.ceil((1.6 * mask * defaultSize) / alphabet.length)
return (size = defaultSize) => {
let id = ''
while (true) {
let bytes = getRandom(step)
let i = step
while (i--) {
id += alphabet[bytes[i] & mask] || ''
if (id.length === size) return id
}
}
}
}
export let customAlphabet = (alphabet, size = 21) =>
customRandom(alphabet, size, random)
export let nanoid = (size = 21) => {
fillPool((size -= 0))
let id = ''
for (let i = poolOffset - size; i < poolOffset; i++) {
id += urlAlphabet[pool[i] & 63]
}
return id
}
After https://github.com/oven-sh/bun/commit/fd7be10f3a02fb7ce426a4598b1303854b88d65e it use node version?
Looks like bpkdf2 is similarly slow (probably for the same reason):
Running the following via time time bun ./password.mjs produces these times:
bun: 1.319s
node: 0.136s
Example code:
import { promisify } from 'util';
// Note: Cannot name this import "crypto" as that seems to always be a reserved global...
import nodeCrypto from 'crypto';
const pbkdf2 = promisify(nodeCrypto.pbkdf2);
const randomBytes = promisify(nodeCrypto.randomBytes);
const iterations = 10000;
const keyLen = 128;
const digest = 'sha256';
async function hashPassword(password) {
const salt = await randomBytes(16);
const result = await pbkdf2(password, salt, iterations, keyLen, digest);
return `${salt.toString('base64')}:${result.toString('base64')}`;
}
hashPassword('helloworld').then(console.log);
import * as crypto from 'crypto'
const rnd256 = crypto.randomBytes(32)
const randomHash = crypto.createHash('sha256')
.update(rnd256.toString('hex'))
.digest('hex')
console.log(randomHash)
time bun index.js
02fe382a5a25ad40ba27ee71a5b73104991507f1d8e66d5d190eacd09d0caba7
bun index.js 0.07s user 0.01s system 100% cpu 0.086 total
time node index.js
c16b7da650edd606cb6b54fd651246062fa0fb7c00ed279267adf26899b772e5
node index.js 0.05s user 0.01s system 87% cpu 0.067 total
I have a hunch that Bun is slower than Node when using Crypto, regardless of the function being used
@chrisdavies @Jarred-Sumner
node crypto implementation is a browserify polyfill currently
need to move it to use:
Bun.SHA128Bun.SHA256Bun.SHA328Bun.SHA512Bun.SHA512_256Bun.MD5- crypto.randomFill
- crypto.randomFillSync
Until that's done, it won't be faster than Node's
I retried @innerop example
time bun index.js
2505d5191d9797700ec6c1c4797972b4600b15262ad061dde0311495ae51216f
bun index.js 0.04s user 0.05s system 28% cpu 0.330 total
time node index.js
ec4e6267b7a368a8b7fde18eba2f6d4074227b57290834687f675621272dfc39
node index.js 0.05s user 0.08s system 4% cpu 3.039 total
bun --version (1.0.25)
node --version (v20.8.0)
This issue might be solved?