Identifying blocks of dead-code elimination

163 views
Skip to first unread message

Rafael Gonzaga

unread,
Sep 3, 2024, 5:24:49 PM9/3/24
to v8-dev
Hi folks!

I'm member of Node.js team and I'm conducting a research on our benchmark suite (https://github.com/nodejs/node/tree/main/benchmark).

In our benchmarks, we attempt to avoid the measured block from being eliminated by V8 dead-code elimination by making use of a state and checking the state after the benchmark run. Example: https://github.com/nodejs/node/blob/main/benchmark/blob/clone.js#L24

However, this is an assumption, we do not check if the measured block is being eliminated so, the benchmark result will be noop or we are measuring it correctly. I tried to run the benchmark with --trace-turbo and analyzing it with tools/turbolizer, but I couldn't find a way to identify which blocks were removed.

Is there a way to do that? I understand that usually micro-benchmarks are far from reliable, but at the moment I don't see how we could make it more sophisticated and specific.

Thanks in advance

Leszek Swirski

unread,
Sep 9, 2024, 5:49:29 AM9/9/24
to v8-...@googlegroups.com
Hi Rafael,

It's not easy to analyze optimizations with turbolizer, it's intended more as a compiler developer tool than an end-user tool. Even if you did, you might be disappointed if the current benchmark is fine and nothing is eliminated right now, but a future iteration of Turbofan/shaft ends up eliminating that loop because of some new analysis. In particular, if we were to detect that structuredClone has no side-effects, we could theoretically collapse your loop to just execute the last iteration.

You're probably better off using some intrinsics (--allow-natives-syntax) to ensure that the object escapes, and make sure that it escapes on each iteration (and then maybe compare that against a loop that does nothing). For example, you could write

function DoNotOptimize(x) {}
// Prevent DoNotOptimize from optimizing or being inlined.
%NeverOptimize(DoNotOptimize);  
...
for (let i = 0; i < n; ++i)
  DoNotOptimize(structuredClone(blob));
 

- Leszek

--
--
v8-dev mailing list
v8-...@googlegroups.com
http://groups.google.com/group/v8-dev
---
You received this message because you are subscribed to the Google Groups "v8-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to v8-dev+un...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/v8-dev/4e060cb2-477c-4037-876a-bd2f5aab245fn%40googlegroups.com.

Rafael Gonzaga

unread,
Sep 9, 2024, 7:57:24 AM9/9/24
to v8-dev
Thanks for the reply, Leszek

We have previously considered using %DoNotOptimize or even %OptimizeOnNextCall (to ensure a proper warmup). But, we concluded that it won't be realistic as some modules might be very well optimized and some of them will be full of deoptimization. Doing so, we've assumed both would be equally _efficient_ which might not be true.

Does V8 trigger any event on dead-code elimination where I could intercept? static probes, for instance. Otherwise, I can manually patch https://github.com/nodejs/node/tree/main/deps/v8 to do so. However, I assume is not that simple, right? 

- Rafael Gonzaga 

Leszek Swirski

unread,
Sep 9, 2024, 11:22:04 AM9/9/24
to v8-...@googlegroups.com
On Mon, Sep 9, 2024 at 1:57 PM Rafael Gonzaga <wo...@rafaelgss.dev> wrote:
We have previously considered using %DoNotOptimize or even %OptimizeOnNextCall (to ensure a proper warmup). But, we concluded that it won't be realistic as some modules might be very well optimized and some of them will be full of deoptimization. Doing so, we've assumed both would be equally _efficient_ which might not be true.

Your call -- fwiw this is how I'd measure it though, because I'd rather introduce some measurable systematic inefficiency like DoNotOptimize than unpredictable optimization like this example. In all honesty, you're running a microbenchmark, so realism is anyway out of the window; I would personally focus more on stability and repeatability.
 
Does V8 trigger any event on dead-code elimination where I could intercept? static probes, for instance. Otherwise, I can manually patch https://github.com/nodejs/node/tree/main/deps/v8 to do so. However, I assume is not that simple, right? 

Your assumption is right, it's not that simple -- the dead code elimination works on a lower level than you'd be thinking in JS -- it doesn't process JS basic blocks or loops, removing entire blocks in a way that is interceptable. Rather, the compiler transforms the JS into a graph, and expands and trims nodes in that graph. The dead code elimination could almost be considered a side effect of various other optimisations, like, say, branch elimination, and it fires for a _lot_ of the intermediate generated nodes. Trying to interpret any sort of hook into that would be no better than trying to interpret turbolizer.
 

Rafael Gonzaga

unread,
Sep 9, 2024, 12:52:41 PM9/9/24
to v8-dev
Alright. I understand. 

Thank you for taking the time to answer it

Cheers,

Rafael Gonzaga

Rafael Gonzaga

unread,
Sep 13, 2024, 5:44:06 PM9/13/24
to v8-dev
Hi, I have an additional question if you don't mind.

What happens if I call %GetOptimizationStatus(fn) and the nodes that are part of fn are dead-eliminated? I suspect it will return kTurboFanned if the block function was optimized regardless of whether some nodes were eliminated, but what if the entire function node was "eliminated"?

I tried to simulate a function where some branches could be eliminated by the compiler and the results I got by getting the optimization status after each 1000 iterations were kTurboFanned & kInterpreted. I might have answered my own question, but I wanted to double-check anyway.

Thanks in advance!

Leszek Swirski

unread,
Sep 16, 2024, 2:38:56 AM9/16/24
to v8-...@googlegroups.com
Hi Rafael,

%GetOptimizationStatus(fn) returns the optimization status of `fn` as a non-inlined function; a function can conceptually be in multiple optimization states (e.g. you might still have an interpreted version on the stack, an optimized version elsewhere, multiple inlined version into other functions, etc), so this returns the optimization status of the function `fn` if you were to call it now directly (e.g. from unoptimized code or an event handler). There's no builtin for "optimization status of inlined function"; even if there were,any implementation I can think of would say "was inlined" but contain no data on "was eliminated" since, again, there's no real grouping between the nodes created in function inlining that can then be dead code eliminated.

I'll give you a concrete example on why this is an underspecified problem. Consider the code

function foo(a) {
  return a.x;
}

function bar(a, b) {
  let y;
  if(b) {
    y = a.x
  } else {
    y = foo();
  }
  return y;
}

When bar is optimized, foo is inlined into it. Then the compiler sees `y = a.x` on both sides of the branch, so it decides to hoist the `a.x` load to before the branch. Has foo now been dead code eliminated? Kind of yes, since we were already doing the same operation in bar, but also kind of not because we can still execute as-if foo were not inlined and executed. The inlined `a.x` load from foo fully becomes _part of bar_, and doesn't have anything to do with foo anymore.

Hope that helps,
Leszek

Reply all
Reply to author
Forward
0 new messages