Skip to content

Conversation

liuxingbaoyu
Copy link
Member

Q                       A
Fixed Issues? Fixes #1, Fixes #2
Patch: Bug Fix?
Major: Breaking Change?
Minor: New Feature?
Tests Added + Pass? Yes
Documentation PR Link
Any Dependency Changes?
License MIT

This fixes potential #16873 (comment)

Also improved access speed for some properties.

before

PS F:\babel\benchmark\babel-traverse> node .\real-case.mjs --only-current
babel-traverse/real-case.mjs babel-parser-express.ts @ current: 299 ops/sec ±1.82% 150 runs (3.348ms)
babel-traverse/real-case.mjs jquery-3.6.js @ current: 87.09 ops/sec ±2.4% 44 runs (11ms)
babel-traverse/real-case.mjs ts-checker.mjs @ current: 10.94 ops/sec ±2.32% 30 runs (91ms)
babel-traverse/real-case.mjs ts-parser.mjs @ current: 81.14 ops/sec ±2.77% 41 runs (12ms)
babel-traverse/real-case.mjs ts-parser.ts @ current: 54 ops/sec ±2.2% 30 runs (19ms)
babel-traverse/real-case.mjs typescript-5.6.2.js @ current: 2.73 ops/sec ±1.61% 30 runs (366ms)

after

PS F:\babel\benchmark\babel-traverse> node .\real-case.mjs --only-current
babel-traverse/real-case.mjs babel-parser-express.ts @ current: 313 ops/sec ±2.07% 157 runs (3.197ms)
babel-traverse/real-case.mjs jquery-3.6.js @ current: 102 ops/sec ±2.52% 52 runs (9.759ms)
babel-traverse/real-case.mjs ts-checker.mjs @ current: 11.68 ops/sec ±1.7% 30 runs (86ms)
babel-traverse/real-case.mjs ts-parser.mjs @ current: 85.35 ops/sec ±3.97% 43 runs (12ms)
babel-traverse/real-case.mjs ts-parser.ts @ current: 58.79 ops/sec ±2.33% 30 runs (17ms)
babel-traverse/real-case.mjs typescript-5.6.2.js @ current: 2.85 ops/sec ±2.43% 30 runs (351ms)

This is not due to the to-fast-properties package, as %HasFastProperties shows that it correctly makes the object fast.
I suspect the original optimization here is being lost due to some changes in v8, even if I use the %ToFastProperties provided by v8, it still performs the same as without special optimizations.

Take the parser as an example
When we go from 19 to 20 options, the return value of %HasFastProperties changes from true to false, and the overall performance drops by 10%.
After we call %ToFastProperties, the return value of %HasFastProperties changes from false to true, but the performance does not return to the previous state.
Until we create a new fast object using {...obj}, performance will return to previous state.

@babel-bot
Copy link
Collaborator

babel-bot commented Oct 19, 2024

Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/58153

@liuxingbaoyu
Copy link
Member Author

liuxingbaoyu commented Oct 19, 2024

I made a minimal reproduction. We can find that %ToFastProperties(obj) and {...obj} are both fast objects, but the performance is different.
I am not sure if this is exactly the same in the real world, because in the real world, the performance of %ToFastProperties and slow is the same.

PS F:\v8-fast-object> node --allow-natives-syntax main.mjs fast     
fast: 5.823s
PS F:\v8-fast-object> node --allow-natives-syntax main.mjs toFast
toFast: 6.719s
PS F:\v8-fast-object> node --allow-natives-syntax main.mjs cloneFast
cloneFast: 5.777s
PS F:\v8-fast-object> node --allow-natives-syntax main.mjs slow  
slow: 29.914s
const object1 = {
  a1: null,
  a2: [],
  a3: 0,
  a4: "script",
  a5: undefined,
  a6: true,
  a7: true,
  a8: true,
  a9: true,
  a10: true,
  a11: true,
  a12: true,
  a13: true,
  a14: true,
  a15: true,
  a16: true,
  a17: true,
  a18: true,
  a19: true,
  a20: true,
};

let x;

function test(opts, name) {
  console.time(name);

  for (let i = 0; i < 1e10; i++) {
    x = opts.a1;
  }
  console.timeEnd(name);
}

const name = process.argv[2];
switch (name) {
  case "fast":
    test(object1, name);
    break;
  case "slow":
    const object2 = {};
    for (const key of Object.keys(object1)) {
      object2[key] = object1[key];
    }

    test(object2, name);
    break;
  case "toFast":
    const object3 = {};
    for (const key of Object.keys(object1)) {
      object3[key] = object1[key];
    }
    %ToFastProperties(object3);

    test(object3, name);
    break;
  case "cloneFast":
    const object4 = { ...object1 };

    test(object4, name);
    break;
}

Copy link
Member

@nicolo-ribaudo nicolo-ribaudo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible that ToFastProperties made sense in V8 versions from 5-6 years ago, but things changed since then.

Thanks for researching this!

@@ -104,5 +104,9 @@ export function getOptions(opts?: Options | null): OptionsWithDefaults {
for (const key of Object.keys(defaultOptions) as (keyof Options)[]) {
options[key] = opts[key] ?? defaultOptions[key];
}
return options;
Copy link
Contributor

@JLHwung JLHwung Oct 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we start from {} and then change the shape for ~20 times. Can we start the new options from { ...defaultOptions } and assign the values if opts[key] is not nullish? Doing so should also avoid the shape migration during initialization.

DEPRECATED_ALIASES,
DEPRECATED_KEYS,
_DEPRECATED_KEYS as DEPRECATED_KEYS,
NODE_PARENT_VALIDATIONS,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also do the shallow copy for NODE_PARENT_VALIDATIONS?

// https://github.com/babel/babel/pull/16918
// `options` is accessed frequently, please make sure it is a fast object.
// `%ToFastProperties` can make it a fast object, but the performance is the same as the slow object.
const options: any = { ...defaultOptions };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: If we move the assignment to the start, the opts == null branch can just reuse the options.

@JLHwung JLHwung merged commit d20b314 into babel:main Oct 20, 2024
52 checks passed
@liuxingbaoyu liuxingbaoyu added pkg: traverse PR: Performance 🏃‍♀️ A type of pull request used for our changelog categories labels Oct 20, 2024
@liuxingbaoyu
Copy link
Member Author

Oops, my description in the PR was wrong.
Because we are compiling to node 6, {...obj} will become Object.assign({}, obj).
This will make objects slow. So @babel/parser theory is correct, but it doesn't actually work in Babel 7. And @babel/types theory is the opposite, and it actually works. 😅

@liuxingbaoyu liuxingbaoyu added pkg: traverse PR: Performance 🏃‍♀️ A type of pull request used for our changelog categories and removed pkg: traverse PR: Performance 🏃‍♀️ A type of pull request used for our changelog categories labels Oct 20, 2024
@github-actions github-actions bot added the outdated A closed issue/PR that is archived due to age. Recommended to make a new issue label Jan 20, 2025
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jan 20, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
outdated A closed issue/PR that is archived due to age. Recommended to make a new issue pkg: traverse PR: Performance 🏃‍♀️ A type of pull request used for our changelog categories
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants