Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Buffered PRNG #45

Open
obazoud opened this issue Nov 23, 2024 · 0 comments
Open

Buffered PRNG #45

obazoud opened this issue Nov 23, 2024 · 0 comments

Comments

@obazoud
Copy link

obazoud commented Nov 23, 2024

Hello,

I'm using ulidx in my project and was concerned about its performance based on benchmarks. While the built-in functionality seemed slow (compared to uuid v7 for example), I decided to investigate further.

Local Machine Benchmark

Here is the result of the benchmark on my local machine, a Mac M1 16 Go

Simple ulid: 26,597 ops/sec ±0.79% (95 runs)
ulid with timestamp: 26,823 ops/sec ±2.37% (74 runs)

Profiling and Bottleneck Identification:

I profiled ulidx (for Node.js only with --prof) to pinpoint the performance bottleneck. The results indicated that the encodeRandom method was the primary culprit. Upon closer inspection, I observed that this method calls crypto.getRandomValues 16 times per ulid generation, which seemed excessive.

Proposed Solution: Buffered PRNG

To address this, I implemented a custom PRNG that reduces the number of calls to crypto.getRandomValues while using a larger buffer size. Here's the code:

class BufferedPRNG {
  constructor(size) {
    this.buffer = new Uint8Array(size);
    this.cursor = 0xffff;
  }

  next = () => {
    if (this.cursor >= this.buffer.length) {
      crypto.getRandomValues(this.buffer);
      this.cursor = 0;
    }
    return this.buffer[this.cursor++] / 0xff;
  }
}

This custom PRNG generates random values from a pre-filled buffer, reducing the number of calls to crypto.getRandomValues.

Integration with Benchmark

I incorporated the BufferedPRNG class into the benchmark to compare performance:

const bufferedPRNG_2 = new BufferedPRNG(2);
const bufferedPRNG_4 = new BufferedPRNG(4);
const bufferedPRNG_16 = new BufferedPRNG(16);
const bufferedPRNG_32 = new BufferedPRNG(32);
const bufferedPRNG_64 = new BufferedPRNG(64);

suite.add("ulid with bufferedPRNG 2", function () {
  ulid(undefined, bufferedPRNG_2.next);
});

// ... similar tests for other buffer sizes

Benchmark Results:

Simple ulid: 27,561 ops/sec ±0.76% (90 runs)
ulid with timestamp: 26,556 ops/sec ±0.85% (93 runs)
ulid with bufferedPRNG 2: 74,244 ops/sec ±0.69% (93 runs)
ulid with bufferedPRNG 4: 137,098 ops/sec ±0.38% (97 runs)
ulid with bufferedPRNG 16: 381,865 ops/sec ±0.35% (97 runs)
ulid with bufferedPRNG 32: 531,374 ops/sec ±0.52% (96 runs)
ulid with bufferedPRNG 64: 668,991 ops/sec ±0.20% (98 runs)

The results demonstrate significant performance improvements. Using a buffer size of 16 (which is the minimum required for a single ulid) yields a roughly 14x speedup.

Request for Feedback

Please let me know if there are any errors in my implementation or approach. I'm open to any suggestions because I plan to use this optimization in my project.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant