Closed Bug 1829736 Opened 1 year ago Closed 1 year ago

6x slower than V8 on Function.prototype.apply (affects Angular)

Categories

(Core :: JavaScript Engine, defect, P1)

defect

Tracking

()

RESOLVED FIXED
114 Branch
Tracking Status
firefox114 --- fixed

People

(Reporter: mstange, Assigned: jandem)

References

(Blocks 1 open bug)

Details

(Whiteboard: [sp3])

Attachments

(2 files)

Attached file microbenchmark

We're seeing 1.1% of the time on the TodoMVC-Angular benchmark spent in fun_apply and fun_call (excluding JS callee time). We've also seen this issue on TodoMVC-Backbone (in dispatch, 0.7% overall) and on React-Stockcharts (in invokeGuardedCallback, 0.5% overall).

The JS-self time of the Angular function invoke is 3.8x higher in SM than in V8.
SM (124 samples): https://share.firefox.dev/3H65gh6
V8 (33 samples): https://share.firefox.dev/3V00zLn

invoke(targetZone, callback, applyThis, applyArgs, source) {
  return this._invokeZS
    ? this._invokeZS.onInvoke(
        this._invokeDlgt,
        this._invokeCurrZone,
        targetZone,
        callback,
        applyThis,
        applyArgs,
        source
      )
    : callback.apply(applyThis, applyArgs);
}

invoke is called around 1800 times during the benchmark. 10% of the calls are with a one-element arguments array, the remaining 90% are with applyArgs being undefined.


Here's a microbenchmark which calls apply 200000 times:

function invoke(callback, applyThis, applyArgs) {
  callback.apply(applyThis, applyArgs);
}

function method(a) {}

var thisObj = {};

function benchmark() {
  for (var c = 0; c < 200000; c++) {
    invoke(method, thisObj, undefined);
  }
}

var start = performance.now();
benchmark();
var end = performance.now();
print(`took ${end - start}ms`);

At this iteration count it shows a 6x difference:

% js --only-inline-selfhosted apply.js
took 19.509765625ms
% d8 --no-turbo-inlining apply.js
took 3.25ms

Most of the cost seems to be the re-entry into JIT land. It would be good to make apply work without leaving the JIT.

We optimize apply when called with an arguments object or an array for the second argument. Calling it with null or undefined is equivalent to just calling the target function with the passed this value and zero arguments.

The difficulty is that you're seeing calls passing either an array or undefined. This is fine for Baseline, but because we don't have call ICs in Ion, it's annoying if we have multiple Baseline stubs because we can't transpile then. Trial inlining could potentially help though..

I have an initial patch for apply with null/undefined. Let me see if this helps Speedometer or if it's making the problem worse by having more polymorphic Call ICs...

(In reply to Jan de Mooij [:jandem] from comment #2)

Let me see if this helps Speedometer or if it's making the problem worse by having more polymorphic Call ICs...

Looking at the number of calls to js::fun_apply on Speedometer 3, it reduces it by about 10-15%. It's able to get rid of most of the calls in Angular.

Assignee: nobody → jdemooij
Status: NEW → ASSIGNED
Severity: -- → S2
Priority: -- → P1

In this case we can just call the target function with the provided this value
and no arguments.

This gets rid of about 10-15% of calls to js::fun_apply on Speedometer, mostly
from the Angular test.

Pushed by jdemooij@mozilla.com: https://hg.mozilla.org/integration/autoland/rev/380d3550d162 Optimize FunApply when the second argument is null or undefined. r=iain
Status: ASSIGNED → RESOLVED
Closed: 1 year ago
Resolution: --- → FIXED
Target Milestone: --- → 114 Branch

This optimization successfully removed invoke and invokeTask from the top 20 functions (sorted by diff) on the Angular comparison report.

Thanks for the incredibly fast response!

You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: