Hacker News new | ask | show | jobs
by titzer 1769 days ago
I actually agree that they technically aren't zero cost (notice I didn't even write that), but the cost is indirect. I've worked on a number of Java JITs and in practice, not a lot of hot code has catch blocks, and even when so, inlining is typically so deep that lots of exception edges (e.g. arising because a possible NPE) get optimized away.

Most of the lost optimization opportunities are second-order costs, not first-order costs. Java JITs make up for the extra flow edges by focusing more on global optimizations rather than local (e.g. GVN vs LVN, global code motion, global flow-sensitive load/store elimination), etc. Generally a possible exception edge splitting a basic block doesn't hurt because the non-exceptional control flow will still benefit from flow-sensitive optimizations (i.e. it has only one predecessor anyway).

We're splitting hairs anyway. Like I said, Java JITs are significantly more advanced at optimizing exception-heavy code. I'd be really surprised if you saw anything more than a 1% increase in performance, actually, no, scratch that. I doubt you can even reliably any speedup distinguishable from noise from just disabling all support for exceptions in most Java code, unless you are talking about metadata. Top-tier JITs really are that good.

1 comments

Enregistering of variables is also lost, because to restore them during unwinding they have to be retrieved from the stack.
Not sure what you mean here, but generally Java JITs generally don't use callee-saved registers at all because they need precise stack maps for GC. So whatever small amounts performance they might lose here isn't due to exceptions.
OpenJDK's (HotSpot) stack maps do support callee-saved registers -- and they are used in some special cases, like safepoint stubs (that spill all registers at a poll-point if a safepoint is triggered) -- but you're correct that they've been removed from ordinary Java calls altogether on all platforms, now.
> Not sure what you mean here

Allocating local variables into registers rather than assigning stack locations for them. Registers are faster than memory. EH unwinders restore the stack before jumping to the catch block, but not the register contents.

Stack maps wouldn't be necessary for non-pointers, like an integer variable. Stack maps also have their own performance problems, which is why D doesn't use them.

Inside a single physical frame, that contains many inlined Java methods (the current default is inlining up to depth of 15, not counting "trivial" methods that are always inlined), locals are always in registers (unless they're exhausted), and are spilled only at safepoints, e.g. when another physical call is made, which is where all languages have to spill, too. Stack maps include only pointers and incur no runtime overhead on the fast-path, and are not used when throwing exceptions, even outside the current physical frame. There's additional debug info metadata associated with compiled code, which also incurs no runtime overhead on fast paths, that maps even primitives to their registers/spill locations; that debug info also maps compiled code locations to their logical, VM bytecode, source ("de-inlining"), and is consulted in the creation of the exception when a stack trace is requested.
The term used most often for this is "spilling". I figured this is what you meant by "deregistering" but I wasn't sure, so I didn't want to assume.

> Registers are faster than memory. EH unwinders restore the stack before jumping to the catch block, but not the register contents.

I get that, which is why Java JITs don't use callee-saved registers. I mean, they use all the physical registers, of course, but their calling convention does not have callee-saved registers.

"spilling" usually means a variable is sometimes in a register, sometimes on the stack. "Enregistering" means it is full time in a register.
But what's a variable, really? After SSA renaming, optimization, SSA deconstruction, then liveness analysis, coalescing, and finally live-range splitting, variables are history and the register allocator is only dealing with live ranges, typically.