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

The import language affect the benchmark result #6543

Open
6 tasks done
D-Sketon opened this issue Sep 21, 2024 · 4 comments
Open
6 tasks done

The import language affect the benchmark result #6543

D-Sketon opened this issue Sep 21, 2024 · 4 comments
Labels
feat: benchmark Issues and PRs related to the benchmark feature p2-edge-case Bug, but has workaround or limited in scope (priority)

Comments

@D-Sketon
Copy link

Describe the bug

I try the following code and run vitest bench

import { bench, describe } from "vitest";
import { after as afterToolkit } from "es-toolkit/function";

const after = afterToolkit;
const add = (a: number, b: number) => a + b;
const n = 10;
describe("after", () => {
  bench("es-toolkit/after - reassign", () => {
    after(n, add);
  });

  bench("es-toolkit/after", () => {
    afterToolkit(n, add);
  });
});

Here's what I got:
{47802ED8-3BC5-4898-BA84-0CEFDCEF555A}

You can see that a direct import statement using import is significantly slower than reassigning values
However, when I try tinybench like this:

import { Bench } from "tinybench";
import { after as afterToolkit } from "es-toolkit";

const bench = new Bench({ time: 100 });
const add = (a: number, b: number) => a + b;
const n = 10;
const after = afterToolkit;
bench
  .add("es-toolkit/after - reassign", () => {
    after(n, add);
  })
  .add("es-toolkit/after", () => {
    afterToolkit(n, add);
  });

await bench.warmup();
await bench.run();

console.table(bench.table());

There is no difference between them:
{000A4F95-07BD-4389-B166-32207828BF30}

Reproduction

Use the code above

System Info

System:
    OS: Windows 11 10.0.22631
    CPU: (16) x64 13th Gen Intel(R) Core(TM) i5-13400F
    Memory: 16.18 GB / 31.82 GB
  Binaries:
    Node: 20.13.1 - D:\nodejs\node.EXE
    Yarn: 1.22.22 - D:\nodejs\yarn.CMD
    npm: 10.5.2 - D:\nodejs\npm.CMD
    bun: 1.1.27 - D:\nodejs\bun.CMD
  Browsers:
    Edge: Chromium (127.0.2651.74)
    Internet Explorer: 11.0.22621.3527
  npmPackages:
    vitest: ^2.0.4 => 2.0.4

Used Package Manager

yarn

Validations

@dayongkr
Copy link

dayongkr commented Sep 21, 2024

The same issue arises when calling with the subpath as shown below, resulting in a performance difference.

Before

import { bench, describe } from 'vitest';
import { after as afterToolkit } from 'es-toolkit'; // This is difference.
import { after as afterLodash } from 'lodash';

describe('after', () => {
 bench('es-toolkit/after', () => {
   const add = (a: number, b: number) => a + b;
   const n = 10;
   const afterFn = afterToolkit(n, add);
   for (let i = 0; i < n + 1; i++) {
     afterFn(1, 2);
   }
 });

 bench('lodash/after', () => {
   const add = (a: number, b: number) => a + b;
   const n = 10;
   const afterFn = afterLodash(n, add);
   for (let i = 0; i < n + 1; i++) {
     afterFn(1, 2);
   }
 });
});

Bench Result

image

Using subpath

import { bench, describe } from 'vitest';
import { after as afterToolkit } from 'es-toolkit/function'; // This is difference.
import { after as afterLodash } from 'lodash';

describe('after', () => {
  bench('es-toolkit/after', () => {
    const add = (a: number, b: number) => a + b;
    const n = 10;
    const afterFn = afterToolkit(n, add);
    for (let i = 0; i < n + 1; i++) {
      afterFn(1, 2);
    }
  });

  bench('lodash/after', () => {
    const add = (a: number, b: number) => a + b;
    const n = 10;
    const afterFn = afterLodash(n, add);
    for (let i = 0; i < n + 1; i++) {
      afterFn(1, 2);
    }
  });
});

Bench Result

image

Additional

You can see our barrel files in here(src/index.ts, src/function/index.ts).

Thanks

@hi-ogawa
Copy link
Contributor

hi-ogawa commented Sep 22, 2024

This is likely due to how Vite/Vitest transform and import modules. You can roughly see that by VITE_NODE_DEBUG_DUMP=1 and import/export are transformed as Object.defineProperty, which might be causing perf hit when benchmarking simple enough functions.

For example, in this reproduction https://stackblitz.com/edit/vitest-dev-vitest-br64n3?file=src%2Fbasic.bench.ts,

// basic.ts
export const squared = (n: number) => n * n

// basic.bench.ts
import { squared } from './basic.js';

squared(2)

will be transformed to something like

const viteSsrExports = Object.create(null);

const viteSsrExportsSquared = (n: number) => n * n;

Object.defineProperty(viteSsrExports, 'squared', {
  enumerable: true,
  configurable: true,
  get() {
    return viteSsrExportsSquared;
  },
});

viteSsrExports.squared(2)

To avoid this, you need to benchmark without transforming, so you probably should try using pre-built version of es-toolkit.

@crimx
Copy link

crimx commented Oct 23, 2024

Is there a workaround for this bug (without modifying code in src)? It appears to occur when importing anything from another file, whether from node_modules or local project files.

// a.ts

export const fn = () => {};

export const a = () => {
  fn();
}
// b.ts

import { fn } from "./a";

export const b = () => {
  fn();
}
// x.bench.ts

import { bench, describe } from "vitest"
import { a } from "./a"
import { b } from "./b"

describe("x", () => {
  bench("a", () => {
    a()
  })
  bench("a2", () => {
    a()
  })
  bench("b", () => {
    b()
  })
  bench("b2", () => {
    b()
  })
})
 RUN  v2.1.3

 ✓ src/x.bench.ts (4) 15487ms
   ✓ x (4) 15486ms
     name             hz     min     max    mean     p75     p99    p995    p999     rme   samples
   · a     24,697,176.57  0.0000  0.1943  0.0000  0.0000  0.0000  0.0000  0.0001  ±0.16%  12348589
   · a2    24,768,664.32  0.0000  0.1781  0.0000  0.0000  0.0000  0.0000  0.0001  ±0.18%  12384333   fastest
   · b     20,451,525.10  0.0000  0.1673  0.0000  0.0000  0.0001  0.0001  0.0001  ±0.18%  10225764
   · b2    20,424,575.59  0.0000  0.1577  0.0000  0.0000  0.0001  0.0001  0.0001  ±0.19%  10212288   slowest


 BENCH  Summary

  a2 - src/x.bench.ts > x
    1.00x faster than a
    1.21x faster than b
    1.21x faster than b2

@sheremet-va sheremet-va added feat: benchmark Issues and PRs related to the benchmark feature p2-to-be-discussed Enhancement under consideration (priority) and removed pending triage labels Oct 23, 2024
@sheremet-va sheremet-va moved this to P2 - 5 in Team Board Oct 23, 2024
@D-Sketon
Copy link
Author

@crimx Here's how we handle it in es-toolkit:

  1. Use deps.optimizer.ssr to bundle the source files, thus eliminating the import overhead inside the modules
deps: {
  optimizer: {
    ssr: {
      enabled: true,
      include: ['es-toolkit', 'es-toolkit/compat'],
    },
  },
}
  1. Re-assign values to imported variables, thus eliminating import overhead in the bench file
import { bench, describe } from 'vitest';
import { chunk as chunkToolkit_ } from 'es-toolkit';
import { chunk as chunkCompatToolkit_ } from 'es-toolkit/compat';
import { chunk as chunkLodash_ } from 'lodash';

const chunkToolkit = chunkToolkit_;
const chunkCompatToolkit = chunkCompatToolkit_;
const chunkLodash = chunkLodash_;

describe('chunk', () => {
  bench('lodash/chunk', () => {
    chunkLodash([1, 2, 3, 4, 5, 6], 3);
  });

  bench('es-toolkit/chunk', () => {
    chunkToolkit([1, 2, 3, 4, 5, 6], 3);
  });

  bench('es-toolkit/compat/chunk', () => {
    chunkCompatToolkit([1, 2, 3, 4, 5, 6], 3);
  });
});

After all this effort, our results are not too far off from tinybench (although there are still some gaps)
before

   name                                hz     min     max    mean     p75     p99    p995    p999     rme  samples
   · lodash/chunk              7,786,900.20  0.0000  0.4533  0.0001  0.0001  0.0003  0.0003  0.0005  ±0.73%  3893461   slowest
   · es-toolkit/chunk         10,678,422.00  0.0000  0.9141  0.0001  0.0001  0.0002  0.0002  0.0003  ±0.46%  5339211   fastest
   · es-toolkit/compat/chunk   7,789,212.00  0.0000  1.2057  0.0001  0.0001  0.0003  0.0003  0.0005  ±0.52%  3894606

after

  name                                hz     min     max    mean     p75     p99    p995    p999     rme  samples
   · lodash/chunk             17,114,118.00  0.0000  1.3395  0.0001  0.0001  0.0001  0.0001  0.0003  ±1.21%  8557059   fastest
   · es-toolkit/chunk         15,336,056.93  0.0000  0.7054  0.0001  0.0001  0.0001  0.0002  0.0002  ±0.47%  7668030   slowest
   · es-toolkit/compat/chunk  15,599,688.00  0.0000  0.4493  0.0001  0.0001  0.0001  0.0002  0.0002  ±0.30%  7799844

tinybench

┌─────────┬─────────────────────┬──────────────┬────────────────────┬──────────┬─────────┐
│ (index) │ Task Name           │ ops/sec      │ Average Time (ns)  │ Margin   │ Samples │
├─────────┼─────────────────────┼──────────────┼────────────────────┼──────────┼─────────┤
│ 0       │ 'lodash'            │ '17,132,909' │ 58.36720090166528  │ '±2.20%' │ 1713291 │
│ 1       │ 'es-toolkit'        │ '16,746,723' │ 59.71317402670255  │ '±0.67%' │ 1674674 │
│ 2       │ 'es-toolkit/compat' │ '16,974,469' │ 58.912001376214626 │ '±0.54%' │ 1697447 │
└─────────┴─────────────────────┴──────────────┴────────────────────┴──────────┴─────────┘

We hope our efforts have inspired you.

@sheremet-va sheremet-va moved this from P2 - 5 to Has plan in Team Board Oct 24, 2024
@sheremet-va sheremet-va added p2-edge-case Bug, but has workaround or limited in scope (priority) and removed p2-to-be-discussed Enhancement under consideration (priority) labels Nov 7, 2024
@sheremet-va sheremet-va mentioned this issue Nov 14, 2024
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feat: benchmark Issues and PRs related to the benchmark feature p2-edge-case Bug, but has workaround or limited in scope (priority)
Projects
Status: Has plan
Development

No branches or pull requests

5 participants