diff --git a/.changeset/famous-islands-invite.md b/.changeset/famous-islands-invite.md
new file mode 100644
index 0000000..0033bad
--- /dev/null
+++ b/.changeset/famous-islands-invite.md
@@ -0,0 +1,5 @@
+---
+"@marco.spiess/enumset": patch
+---
+
+Add benchmark
diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml
index 9d1bfa2..ea28844 100644
--- a/.github/workflows/node.js.yml
+++ b/.github/workflows/node.js.yml
@@ -37,3 +37,5 @@ jobs:
run: npm run test:mutation
env:
STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}
+ - if: ${{ matrix.node-version == '26.x' }}
+ run: npm run benchmark
diff --git a/README.md b/README.md
index 62d596b..621190a 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ All state is packed into a single 32-bit integer bitfield.
## Installation
-```
+```shell
npm install @marco.spiess/enumset
```
@@ -44,3 +44,80 @@ expect(enumSet.has(Directions.Left)).toBe(false);
- Iteration order breaks the [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set)
contract. The methods `forEach`, `keys`, `values` and `[Symbol.iterator]` iterate in the ascending order of the enum
values and *not* in insertion order.
+
+## Benchmark
+
+Compared to the native `Set` `EnumSets` perform better for `add` and `delete`.
+For `has` and iteration `EnumSets` are slightly slower.
+For all set composition methods `EnumSets` vastly outperform their native counterparts.
+
+
+Result of a benchmark run in CI
+
+```
+ ✓ bench src/main.benchmark.ts (14 tests) 209391ms
+ ✓ has (2)
+ ✓ empty set 19811ms
+ name hz min max mean p75 p99 p995 p999 rme samples
+ native set 21,206,116.26 0.0000 0.1372 0.0000 0.0001 0.0001 0.0001 0.0001 ±0.09% 20636388 fastest
+ enum set 13,198,874.44 0.0001 0.0909 0.0001 0.0001 0.0001 0.0001 0.0001 ±0.08% 12924792
+ ✓ full set 20078ms
+ name hz min max mean p75 p99 p995 p999 rme samples
+ native set 21,291,907.56 0.0000 0.0978 0.0000 0.0001 0.0001 0.0001 0.0001 ±0.10% 20736684 fastest
+ enum set 13,237,134.16 0.0001 0.0427 0.0001 0.0001 0.0001 0.0001 0.0001 ±0.07% 13005323
+ ✓ add 11337ms
+ name hz min max mean p75 p99 p995 p999 rme samples
+ enum set 14,437,209.78 0.0001 0.0320 0.0001 0.0001 0.0001 0.0001 0.0001 ±0.07% 14219315 fastest
+ native set 4,048,108.96 0.0002 0.3157 0.0003 0.0003 0.0003 0.0003 0.0007 ±0.25% 3965984
+ ✓ delete 11266ms
+ name hz min max mean p75 p99 p995 p999 rme samples
+ enum set 14,224,467.32 0.0001 0.0729 0.0001 0.0001 0.0001 0.0001 0.0001 ±0.08% 14015339 fastest
+ native set 4,287,069.25 0.0002 0.3568 0.0002 0.0002 0.0003 0.0004 0.0007 ±0.30% 4186946
+ ✓ keys 22782ms
+ name hz min max mean p75 p99 p995 p999 rme samples
+ native set 22,845,624.54 0.0000 0.0415 0.0000 0.0001 0.0001 0.0001 0.0001 ±0.07% 22276472 fastest
+ enum set 22,849,172.68 0.0000 0.0951 0.0000 0.0001 0.0001 0.0001 0.0001 ±0.08% 22254698
+ ✓ values 17108ms
+ name hz min max mean p75 p99 p995 p999 rme samples
+ native set 19,279,170.57 0.0000 0.0413 0.0001 0.0001 0.0001 0.0001 0.0001 ±0.08% 18898286 fastest
+ enum set 12,656,450.07 0.0001 18.8421 0.0001 0.0001 0.0001 0.0001 0.0005 ±4.09% 11694214
+ ✓ Symbol(Symbol.iterator) 19436ms
+ name hz min max mean p75 p99 p995 p999 rme samples
+ native set 19,442,343.10 0.0000 0.0798 0.0001 0.0001 0.0001 0.0001 0.0001 ±0.08% 19006739 fastest
+ enum set 17,544,892.40 0.0000 66.8953 0.0001 0.0001 0.0001 0.0001 0.0004 ±13.17% 15254944
+ ✓ union 12892ms
+ name hz min max mean p75 p99 p995 p999 rme samples
+ enum set 20,008,001.47 0.0000 0.1361 0.0001 0.0001 0.0001 0.0001 0.0001 ±0.08% 19682920 fastest
+ native set 2,583,085.76 0.0003 0.3434 0.0004 0.0004 0.0006 0.0007 0.0011 ±0.48% 2473422
+ ✓ intersection 10870ms
+ name hz min max mean p75 p99 p995 p999 rme samples
+ enum set 14,711,000.12 0.0001 0.0299 0.0001 0.0001 0.0001 0.0001 0.0001 ±0.08% 14419920 fastest
+ native set 2,186,253.34 0.0004 0.4180 0.0005 0.0005 0.0006 0.0008 0.0011 ±0.37% 2127560
+ ✓ difference 11318ms
+ name hz min max mean p75 p99 p995 p999 rme samples
+ enum set 13,911,659.27 0.0001 0.0480 0.0001 0.0001 0.0001 0.0001 0.0001 ±0.08% 13645951 fastest
+ native set 3,541,035.40 0.0002 0.0290 0.0003 0.0003 0.0004 0.0005 0.0009 ±0.08% 3463004
+ ✓ symmetricDifference 11600ms
+ name hz min max mean p75 p99 p995 p999 rme samples
+ enum set 13,746,639.49 0.0001 0.0841 0.0001 0.0001 0.0001 0.0001 0.0001 ±0.11% 13438668 fastest
+ native set 5,153,365.76 0.0002 0.3619 0.0002 0.0002 0.0002 0.0003 0.0006 ±0.40% 4995671
+ ✓ isSubsetOf 15365ms
+ name hz min max mean p75 p99 p995 p999 rme samples
+ enum set 21,584,121.56 0.0000 0.1773 0.0000 0.0001 0.0001 0.0001 0.0001 ±0.12% 20949499 fastest
+ native set 5,478,922.03 0.0002 0.0427 0.0002 0.0002 0.0002 0.0002 0.0004 ±0.07% 5405264
+ ✓ isSupersetOf 12293ms
+ name hz min max mean p75 p99 p995 p999 rme samples
+ enum set 15,724,180.49 0.0000 0.0490 0.0001 0.0001 0.0001 0.0001 0.0001 ±0.07% 15426052 fastest
+ native set 4,863,504.30 0.0002 0.0346 0.0002 0.0002 0.0004 0.0004 0.0005 ±0.08% 4700775
+ ✓ isDisjointFrom 13232ms
+ name hz min max mean p75 p99 p995 p999 rme samples
+ enum set 16,888,742.09 0.0000 0.0478 0.0001 0.0001 0.0001 0.0001 0.0001 ±0.08% 16538521 fastest
+ native set 4,836,225.74 0.0002 0.0411 0.0002 0.0002 0.0002 0.0003 0.0004 ±0.07% 4765987
+```
+
+
+
+To run the benchmark:
+```shell
+npm run benchmark
+```
\ No newline at end of file
diff --git a/package.json b/package.json
index 68aceb3..4eb7d6d 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"lint": "eslint src/",
"test": "vitest --typecheck --run",
"test:mutation": "stryker run",
+ "benchmark": "vitest bench",
"changeset": "changeset",
"release": "npm run build && changeset tag && npm publish --provenance --access public"
},
diff --git a/src/main.benchmark.ts b/src/main.benchmark.ts
new file mode 100644
index 0000000..4a9f4e1
--- /dev/null
+++ b/src/main.benchmark.ts
@@ -0,0 +1,205 @@
+import { describe, expect, test } from 'vitest';
+import type { IntegerLessThan32 } from './IntegerLessThan32.ts';
+import { EnumSet } from './main.ts';
+
+const allValues = [...Array(32).keys()] as IntegerLessThan32[];
+const lowerHalfValues = [...Array(16).keys()] as IntegerLessThan32[];
+const upperHalfValues = [...Array(16).keys()].map(value => value + 16) as IntegerLessThan32[];
+
+describe('has', () => {
+ test('empty set', async ({ bench }) => {
+ const nativeSet = new Set();
+ const enumSet = new EnumSet();
+
+ await bench.compare(
+ bench('native set', () => {
+ allValues.forEach((value) => {
+ nativeSet.has(value);
+ });
+ }), bench('enum set', () => {
+ allValues.forEach((value) => {
+ enumSet.has(value);
+ });
+ }),
+ );
+ });
+
+ test('full set', async ({ bench }) => {
+ const nativeSet = new Set(allValues);
+ const enumSet = new EnumSet(allValues);
+
+ await bench.compare(
+ bench('native set', () => {
+ allValues.forEach((value) => {
+ nativeSet.has(value);
+ });
+ }), bench('enum set', () => {
+ allValues.forEach((value) => {
+ enumSet.has(value);
+ });
+ }),
+ );
+ });
+});
+
+test('add', async ({ bench }) => {
+ const nativeSet = new Set();
+ const enumSet = new EnumSet();
+
+ const result = await bench.compare(
+ bench('native set', () => {
+ allValues.forEach((value) => {
+ nativeSet.add(value);
+ });
+ }), bench('enum set', () => {
+ allValues.forEach((value) => {
+ enumSet.add(value);
+ });
+ }),
+ );
+
+ expect([...nativeSet.values()]).toStrictEqual(allValues);
+ expect([...enumSet.values()]).toStrictEqual(allValues);
+ expect(result.get('enum set')).toBeFasterThan(result.get('native set'));
+});
+
+test('delete', async ({ bench }) => {
+ const nativeSet = new Set(allValues);
+ const enumSet = new EnumSet(allValues);
+
+ const result = await bench.compare(
+ bench('native set', () => {
+ allValues.forEach((value) => {
+ nativeSet.delete(value);
+ });
+ }), bench('enum set', () => {
+ allValues.forEach((value) => {
+ enumSet.delete(value);
+ });
+ }),
+ );
+
+ expect(nativeSet.size).toBe(0);
+ expect(enumSet.size).toBe(0);
+ expect(result.get('enum set')).toBeFasterThan(result.get('native set'));
+});
+
+test.for([
+ { method: 'keys' as const },
+ { method: 'values' as const },
+ // workaround for https://github.com/microsoft/TypeScript/issues/54100
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
+ { method: Symbol.iterator } as { method: typeof Symbol.iterator },
+])('$method', async ({ method }, { bench }) => {
+ const nativeSet = new Set(allValues);
+ const enumSet = new EnumSet(allValues);
+
+ await bench.compare(
+ bench('native set', () => {
+ nativeSet[method]();
+ }), bench('enum set', () => {
+ enumSet[method]();
+ }),
+ );
+});
+
+test.for<{
+ operation: 'union' | 'intersection' | 'difference' | 'symmetricDifference';
+ leftValues: IntegerLessThan32[];
+ rightValues: IntegerLessThan32[];
+ expectedValues: IntegerLessThan32[];
+}>([
+ {
+ operation: 'union',
+ leftValues: lowerHalfValues,
+ rightValues: upperHalfValues,
+ expectedValues: allValues,
+ },
+ {
+ operation: 'intersection',
+ leftValues: lowerHalfValues,
+ rightValues: allValues,
+ expectedValues: lowerHalfValues,
+ },
+ {
+ operation: 'difference',
+ leftValues: allValues,
+ rightValues: upperHalfValues,
+ expectedValues: lowerHalfValues,
+ },
+ {
+ operation: 'symmetricDifference',
+ leftValues: [0, 1, 2, 3],
+ rightValues: [2, 3, 4, 5],
+ expectedValues: [0, 1, 4, 5],
+ },
+])('$operation', async ({ operation, leftValues, rightValues, expectedValues }, { bench }) => {
+ const nativeSets = [new Set(leftValues), new Set(rightValues)];
+ const enumSets = [new EnumSet(leftValues), new EnumSet(rightValues)];
+ type CompositionMethod = typeof nativeSets[number][typeof operation];
+
+ let nativeResult: Set | undefined;
+ let enumSetResult: Set | undefined;
+
+ const result = await bench.compare(
+ bench('native set', () => {
+ const [left, right] = nativeSets;
+
+ nativeResult = left[operation](right);
+ }),
+ bench('enum set', () => {
+ const [left, right] = enumSets;
+
+ enumSetResult = (left[operation] as CompositionMethod)(right) as EnumSet;
+ }),
+ );
+
+ expect([...nativeResult!.values()]).toEqual(expectedValues);
+ expect([...enumSetResult!.values()]).toEqual(expectedValues);
+ expect(result.get('enum set')).toBeFasterThan(result.get('native set'));
+});
+
+test.for<{
+ operation: 'isDisjointFrom' | 'isSubsetOf' | 'isSupersetOf';
+ leftValues: IntegerLessThan32[];
+ rightValues: IntegerLessThan32[];
+}>([
+ {
+ operation: 'isSubsetOf',
+ leftValues: lowerHalfValues,
+ rightValues: allValues,
+ },
+ {
+ operation: 'isSupersetOf',
+ leftValues: allValues,
+ rightValues: lowerHalfValues,
+ },
+ {
+ operation: 'isDisjointFrom',
+ leftValues: lowerHalfValues,
+ rightValues: upperHalfValues,
+ },
+])('$operation', async ({ operation, leftValues, rightValues }, { bench }) => {
+ const nativeSets = [new Set(leftValues), new Set(rightValues)];
+ const enumSets = [new EnumSet(leftValues), new EnumSet(rightValues)];
+
+ let nativeResult = false;
+ let enumSetResult = false;
+
+ const result = await bench.compare(
+ bench('native set', () => {
+ const [left, right] = nativeSets;
+
+ nativeResult = left[operation](right);
+ }),
+ bench('enum set', () => {
+ const [left, right] = enumSets;
+
+ enumSetResult = left[operation](right);
+ }),
+ );
+
+ expect(nativeResult).toBe(true);
+ expect(enumSetResult).toBe(true);
+ expect(result.get('enum set')).toBeFasterThan(result.get('native set'));
+});