handlebars.js icon indicating copy to clipboard operation
handlebars.js copied to clipboard

Simplify helper wrapper

Open mohd-akram opened this issue 1 year ago • 7 comments

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

mohd-akram avatar Sep 04 '24 13:09 mohd-akram

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

jaylinski avatar Sep 04 '24 20:09 jaylinski

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

mohd-akram avatar Sep 05 '24 02:09 mohd-akram

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.

nknapp avatar Sep 06 '24 13:09 nknapp

@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.

mohd-akram avatar Sep 06 '24 14:09 mohd-akram

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 avatar Sep 06 '24 14:09 nknapp

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

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

nknapp avatar Sep 06 '24 14:09 nknapp

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

mohd-akram avatar Sep 07 '24 06:09 mohd-akram