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

Simplify helper wrapper #2041

Open
wants to merge 1 commit into
base: 4.x
Choose a base branch
from

Conversation

mohd-akram
Copy link
Contributor

This improves performance slightly and gives the wrapper function a more descriptive name to help with debugging/profiling.

@mohd-akram mohd-akram mentioned this pull request Sep 4, 2024
@jaylinski
Copy link
Member

Can you post the setup you are using for measuring the performance improvements?

@mohd-akram
Copy link
Contributor Author

@jaylinski Posted the setup + the numbers in #1991. For this change, the difference is small, but we drop an extra function call.

@nknapp
Copy link
Collaborator

nknapp commented Sep 6, 2024

I agree that the "wrapHelper" function is a bit redundant. I guess my goal was to also use it in different use-cases.

However, I think, "passLookupPropertyOption" (or "wrapWithLookupPropertyOption") is a more descriptive name than just "wrapHelper". So I would keep that name.

Another thing is that the runtime.js file is already too large for my taste and I would try to extract code into different files where possible and let the bundler put them back together.

In this case, it might be good to extract the whole "addHelpers" or even the four lines where it is called, into a function.

Tbh, I never expected function calls to have a lot of impact on JavaScript performance. Since V8 is an optimizing JIT compiler, I always thought it would inline them when necessary.

This improves performance slightly and gives the wrapper function a more
descriptive name to help with debugging/profiling.
@mohd-akram
Copy link
Contributor Author

@nknapp Good points, updated the PR. Regarding the performance, I agree about the inlining, and I didn't expect to find any difference, but there is something like 5%. Either way, it's hard to tell what exactly the JIT will do so it doesn't hurt to simplify in this case.

@nknapp
Copy link
Collaborator

nknapp commented Sep 6, 2024

Just to get a feeling on how much improvement we have here, I modified you benchmark a little, to only include the "partial" part and to run it multiple times, gathering 10th and 90th percentile of the run time.

const Handlebars = require('./dist/handlebars');

const count = 100000;

// Handlebars
Handlebars.registerPartial('partial', Handlebars.compile('1234'));
const template = Handlebars.compile('{{> partial}}');

let times = []

for (let r = 0; r < 100; r++) {
  // Warm up
  for (let i = 0; i < count; i++) {
    template();
  }

  const start = performance.now()
  for (let i = 0; i < count; i++) {
    template();
  }
  const time = performance.now() - start
  times.push(time)
}

times.sort((a,b) => a - b)

console.log("10th percentile", times[10])
console.log("90th percentile", times[90])

For the 4.x version I got the following numbers:

➜  handlebars.js git:(e914d60) ✗ node ./performance.js 
10th percentile 55.09403999999995
90th percentile 56.883372000000236
➜  handlebars.js git:(e914d60) ✗ node ./performance.js
10th percentile 55.61303500000008
90th percentile 58.357834999999795
➜  handlebars.js git:(e914d60) ✗ node ./performance.js
10th percentile 55.286535000000185
90th percentile 57.09553800000003

For the current version of this PR:

➜  handlebars.js git:(simplify-helper-wrapper) ✗ node ./performance.js                                                           
10th percentile 52.90112299999964
90th percentile 54.70712900000035
➜  handlebars.js git:(simplify-helper-wrapper) ✗ node ./performance.js
10th percentile 52.66511000000014
90th percentile 54.73322900000039
➜  handlebars.js git:(simplify-helper-wrapper) ✗ node ./performance.js
10th percentile 54.04121799999939
90th percentile 55.698201000000154

I see that the difference between 10th and 90th percentile can be 4 to 6 percent.
The PR makes thinks about 6 percent faster, which is just outside of the significance threshold.
That is, for templates that just consist of a partial call and don't do anything else.

There is always this problem with micro-benchmarks, that they have a high variability. This is because they are not deterministic. The computer is doing different things in the background, which might affect the numbers.
The Node.js runtime may or may not apply different optimizations between different runs.
It is difficult to determine why exactly the numbers vary.

So my take on this matter is this:

I don't think removing function calls does much to increase performance in JavaScript, so I would not remove a function just for performance benefits.
If the function makes the code more difficult to understand, that's a different matter.

But usually, the opposite is true. I sometimes extract functions, even if they are only called from a single location

  • in order to give it a descriptive name, and
  • in order to have shorter functions.

Sorry for this wall of text. I got pulled into the rabbit hole somehow.

@nknapp nknapp self-requested a review September 6, 2024 14:49
@nknapp
Copy link
Collaborator

nknapp commented Sep 6, 2024

Oh, I didn't see your comment until now.

@jaylinski I think this can be merged. But ultimately, it is your choice.

@mohd-akram
Copy link
Contributor Author

I very much agree with your take, and thanks for verifying the numbers, optimization is definitely a rabbit hole :D

@jaylinski jaylinski self-assigned this Sep 12, 2024
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

Successfully merging this pull request may close these issues.

3 participants