Skip to content

Commit 983cf86

Browse files
authored
⚡️ Faster ulid mapper function (#4098)
* ⚡️ Faster `ulid` mapper function Here are some stats extracted from a benchmark: ```txt ┌─────────┬────────┬──────────────────────┬─────────────────────┬─────────────────────┐ │ (index) │ name │ mean │ p99 │ p999 │ ├─────────┼────────┼──────────────────────┼─────────────────────┼─────────────────────┤ │ 0 │ 'old' │ 0.036524547798902726 │ 0.06470000091940165 │ 0.24509999994188547 │ │ 1 │ 'new' │ 0.02174825419917982 │ 0.04059999994933605 │ 0.24960000067949295 │ │ 2 │ 'old2' │ 0.03769995739969145 │ 0.06599999964237213 │ 0.3251000000163913 │ │ 3 │ 'new2' │ 0.022186629200091585 │ 0.04079999960958958 │ 0.29510000068694353 │ └─────────┴────────┴──────────────────────┴─────────────────────┴─────────────────────┘ ``` Benchmark have been done using: ```js const { Bench } = require('tinybench'); const data = [...Array(100)].map(() => Math.floor(Math.random() * 0xffffffffffff)); const bench = new Bench({ warmupIterations: 20, iterations: 1_000_000 }); bench.add('old', () => { for (const value of data) { uintToBase32StringMapperOld(value, 10); } }); bench.run().then((data) => { console.table(data.map((e) => ({ name: e.name, mean: e.result.mean, p99: e.result.p99, p999: e.result.p999 }))); }); ``` * versions
1 parent ef725c2 commit 983cf86

File tree

4 files changed

+49
-25
lines changed

4 files changed

+49
-25
lines changed

.yarn/versions/35875d1a.yml

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
releases:
2+
fast-check: minor
3+
4+
declined:
5+
- "@fast-check/ava"
6+
- "@fast-check/jest"
7+
- "@fast-check/vitest"
8+
- "@fast-check/worker"

packages/fast-check/src/arbitrary/_internals/mappers/UintToBase32String.ts

+15-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { Error, String } from '../../../utils/globals';
22

3-
const safeMathFloor = Math.floor;
4-
53
/** @internal */
64
const encodeSymbolLookupTable: Record<number, string> = {
75
10: 'A',
@@ -79,13 +77,24 @@ function pad(value: string, paddingLength: number) {
7977
}
8078

8179
/** @internal */
82-
export function uintToBase32StringMapper(num: number, paddingLength: number): string {
80+
function smallUintToBase32StringMapper(num: number): string {
8381
let base32Str = '';
84-
for (let remaining = num; remaining !== 0; remaining = safeMathFloor(remaining / 32)) {
85-
const current = remaining % 32;
82+
// num must be in 0 (incl.), 0x7fff_ffff (incl.)
83+
// >>5 is equivalent to /32 and <<5 to x32
84+
for (let remaining = num; remaining !== 0; ) {
85+
const next = remaining >> 5;
86+
const current = remaining - (next << 5);
8687
base32Str = encodeSymbol(current) + base32Str;
88+
remaining = next;
8789
}
88-
return pad(base32Str, paddingLength);
90+
return base32Str;
91+
}
92+
93+
/** @internal */
94+
export function uintToBase32StringMapper(num: number, paddingLength: number): string {
95+
const head = ~~(num / 0x40000000);
96+
const tail = num & 0x3fffffff;
97+
return pad(smallUintToBase32StringMapper(head), paddingLength - 6) + pad(smallUintToBase32StringMapper(tail), 6);
8998
}
9099

91100
/** @internal */

packages/fast-check/src/arbitrary/ulid.ts

+24-17
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,28 @@ import { paddedUintToBase32StringMapper, uintToBase32StringUnmapper } from './_i
66
const padded10Mapper = paddedUintToBase32StringMapper(10);
77
const padded8Mapper = paddedUintToBase32StringMapper(8);
88

9+
type MapperIn = [number, number, number];
10+
type MapperOut = string;
11+
12+
function ulidMapper(parts: MapperIn): MapperOut {
13+
return (
14+
padded10Mapper(parts[0]) + // 10 chars of base32 -> 48 bits
15+
padded8Mapper(parts[1]) + // 8 chars of base32 -> 40 bits
16+
padded8Mapper(parts[2])
17+
);
18+
}
19+
20+
function ulidUnmapper(value: unknown): MapperIn {
21+
if (typeof value !== 'string' || value.length !== 26) {
22+
throw new Error('Unsupported type');
23+
}
24+
return [
25+
uintToBase32StringUnmapper(value.slice(0, 10)),
26+
uintToBase32StringUnmapper(value.slice(10, 18)),
27+
uintToBase32StringUnmapper(value.slice(18)),
28+
];
29+
}
30+
931
/**
1032
* For ulid
1133
*
@@ -24,22 +46,7 @@ export function ulid(): Arbitrary<string> {
2446
const randomnessPartTwoArbitrary = integer({ min: 0, max: 0xffffffffff }); // 40 bits
2547

2648
return tuple(timestampPartArbitrary, randomnessPartOneArbitrary, randomnessPartTwoArbitrary).map(
27-
(parts) => {
28-
return (
29-
padded10Mapper(parts[0]) + // 10 chars of base32 -> 48 bits
30-
padded8Mapper(parts[1]) + // 8 chars of base32 -> 40 bits
31-
padded8Mapper(parts[2])
32-
);
33-
},
34-
(value) => {
35-
if (typeof value !== 'string' || value.length !== 26) {
36-
throw new Error('Unsupported type');
37-
}
38-
return [
39-
uintToBase32StringUnmapper(value.slice(0, 10)),
40-
uintToBase32StringUnmapper(value.slice(10, 18)),
41-
uintToBase32StringUnmapper(value.slice(18)),
42-
];
43-
}
49+
ulidMapper,
50+
ulidUnmapper
4451
);
4552
}

packages/fast-check/test/unit/arbitrary/_internals/mappers/UintToBase32String.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import {
77
describe('uintToBase32StringUnmapper', () => {
88
it('should be able to unmap any mapped value', () =>
99
fc.assert(
10-
fc.property(fc.maxSafeNat(), (input) => {
10+
fc.property(fc.maxSafeNat(), fc.integer({ min: 6, max: 20 }), (input, length) => {
1111
// Arrange
12-
const mapped = paddedUintToBase32StringMapper(12)(input);
12+
const mapped = paddedUintToBase32StringMapper(length)(input);
1313
// Act
1414
const out = uintToBase32StringUnmapper(mapped);
1515
// Assert

0 commit comments

Comments
 (0)