In short: it's due to inlining.
When a call such as callback()
has seen only one target function being called, and the containing function ("fn
" in this case) is optimized, then the optimizing compiler will (usually) decide to inline that call target. So in the fast version, no actual call is performed, instead the empty function is inlined.
When you then call different callbacks, the old optimized code needs to be thrown away ("deoptimized"), because it is now incorrect (if the new callback has different behavior), and upon re-optimization a little while later, the inlining heuristic decides that inlining multiple possible targets probably isn't worth the cost (because inlining, while sometimes enabling great performance benefits, also has certain costs), so it doesn't inline anything. Instead, generated optimized code will now perform actual calls, and you'll see the cost of that.
As @0stone0 observed, when you pass the same callback on the second call to fn
, then deoptimization isn't necessary, so the originally generated optimized code (that inlined this callback) can continue to be used. Defining three different callbacks all with the same (empty) source code doesn't count as "the same callback".
FWIW, this effect is most pronounced in microbenchmarks; though sometimes it's also visible in more real-world-ish code. It's certainly a common trap for microbenchmarks to fall into and produce confusing/misleading results.
In the second experiment, when there is no callback
, then of course the callback &&
part of the expression will already bail out, and none of the three calls to fn
will call (or inline) any callbacks, because there are no callbacks.
fn1 = (...) => ...;
fn2 = (...) = ;
fn3 = (...) =>
) or if you use the same callback function (socb = () => {}; fn(length, label, cb)
then you get all acting fast. This kind of rings a bell and I believe it's already been asked here before... but I'm not sure I'll be able to find the Q/A if it exists. – Lmcallback
and optimise the check away. In the subsequent runs, it notices that there's differentcallback
values so it has to make an actual call. – Anthiaconst clb = () => {};
and then pass itfn(length, "1", clb)
, the difference disappears. – Sangsanger