bun icon indicating copy to clipboard operation
bun copied to clipboard

nanoid is ~3.5 times slower on bun when compared to node

Open KMatuszak opened this issue 3 years ago • 9 comments
trafficstars

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

KMatuszak avatar Jul 26 '22 12:07 KMatuszak

👍 image

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);

xhyrom avatar Jul 26 '22 19:07 xhyrom

maybe toString(32) is less optimized in JSC compared to v8

image

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
            ? "-"
            : "_"),
      ""
    );

Jarred-Sumner avatar Jul 27 '22 03:07 Jarred-Sumner

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

Jarred-Sumner avatar Jul 27 '22 03:07 Jarred-Sumner

oh i see

Jarred-Sumner avatar Jul 27 '22 03:07 Jarred-Sumner

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
}

Jarred-Sumner avatar Jul 27 '22 03:07 Jarred-Sumner

After https://github.com/oven-sh/bun/commit/fd7be10f3a02fb7ce426a4598b1303854b88d65e it use node version?

xhyrom avatar Aug 07 '22 05:08 xhyrom

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);

chrisdavies avatar Aug 18 '22 01:08 chrisdavies

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

innerop avatar Sep 06 '22 06:09 innerop

node crypto implementation is a browserify polyfill currently

need to move it to use:

  • Bun.SHA128
  • Bun.SHA256
  • Bun.SHA328
  • Bun.SHA512
  • Bun.SHA512_256
  • Bun.MD5
  • crypto.randomFill
  • crypto.randomFillSync

Until that's done, it won't be faster than Node's

Jarred-Sumner avatar Sep 08 '22 07:09 Jarred-Sumner

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?

eknowles avatar Jan 24 '24 17:01 eknowles