-
-
Notifications
You must be signed in to change notification settings - Fork 922
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
DeadCodeElimination does not see into method ensures #1786
Comments
To get around this for now, I am disabling DCE (currently run all the time in response to runDeadCodeAndVarLoadStorePasses called from prepareForInterpretation). |
This is because I assumed that get_field instruction will always complete and not throw a ruby exception (java exceptions would presumably be handled in the runtime). Is that false? In that scenario, you dont need another init of osync to nil (or in other words, that init you added is truly dead). Try setting the exception flag for GET_FIELD in Operation.java as follows and see if that fixes it.
|
I think the issue here is that when @headius sets up the equivalent of a try/catch for the method the assignment of that temp is essentially in what is the body of the try and the int to nil would have come before. Perhaps we can not eliminate the nil init if it is inside an exception handling BB? |
Wow that last sentence is %t_osync_2 = nil cannot be removed if it is referenced by any exception handler BB (which I think is known at CFG build time). |
Notes from talking to subbu... CFG should add new exception edges from one BB before a try to each catch BB. This should end up solving the liveness of those tempvars if they are really living or not. The basis of this problem looks like this in Java: class C {
public void foo() {
int a;
try {
a = 1;
} catch (Exception e) {
System.out.println("a: " + a);
}
}
} In this snippet 'int a' represents our init temp. |
Try: ast --ir -e "a = 1; begin; b = 1; ensure; p a, b; end" or ast --ir -e "a = 1; begin; b = 1; rescue => e; p a, b; end" You will notice that by the time DCE is done, it effectively eliminates the begin-ensure and the begin-resuce since it figures that they are spurious, i.e. the dataflow analysis does a finer grained analysis on a per-instruction level by testing whether those can throw ruby exceptions or not. Right now, it doesn't assume that just because a protected region is entered, control can enter the rescue handler from the top of the protected region (which is what the the javac/jvm bytecode assumption seems to be) .. If you try to javac the snippet that Tom added above, it won't compile .. neither will any of the java-equivalent variants of the code above. So, one way to mimic jvm requirement is to forcibly add a dummy edge from the protected region predecessor to the handler (which if done will block the opts that we are currently able to do with the snippets above). To be continued .... |
That's certainly reasonable. In my original case, however, there's at least one line that can cause an exception: p osync. That keeps the ensure from being removed, but for whatever reason the ensure's eventual use of the osync variable does not keep our nil initialization alive. I can remove spurious code to simplify the case: def foo
a = 1
p a
ensure
p a
end This method does nothing except local variable assignment and calls to |
Right, that is because the DCE reasons that a =1 won't thrown an exception (which it won't), and hence will always be initialized. The bytecode spec has stricter constraints. For now, turning off DCE is a good enough soln. to get you unblocked on getting JIT done, but let us discuss what is a good / cheap solution that works. Adding the dummy exception edge (as in earlier comments) will solve this problem, but it also seems quite conservative. |
Put another way, our analyses effectively shrinks protected regions till it hits the first exception-throwing instruction, but leaves the exception regions unshrunk ==> the JIT and hence JVM cannot exploit this information. So, perhaps, this should be the very first thing done during cfg building so that the first instruction in any protected region is always an exception-throwing instruction. With that fix, all dataflow analyses will do the right thing. So, in this example, the code effectively should effectively get rewritten to: def foo
a = 1
begin
p a
ensure
p a
end
end``` |
Yeah that seems like it would work properly. If the exceptional region shrinks to the first instruction that might cause an exception, it should move non-exceptional init outside that region. Where is that shrinking done? |
To recap the brief IRC discussion from the afternoon, the thing to reason about is what happens when you hit control-flow instructions while inspecting protected code. Thinking about this some more, the core logic is to find a single-entry-single-exit control-flow region within the protected code that has no excepting instructions and move that out of the protected code. begin
a = 1
c = a == 1 ? code-path-1 : code-path-2
p a, c
rescue
...
end can be transformed to: a = 1
c = a == 1 ? code-path-1: code-path-2
begin
p a, c
rescue
...
end only if both code-path-1 and code-path-2 don't have excepting instrs. otherwise, the branch and both code paths stay behind. This is probably best done as a post-cfg-build pass and the details need working out, and anecdotally, this seems straightforward, but requires some more analysis in the general case. "The Program Structure Tree: Computing Control Regions in Linear Time" might be relevant here. |
Is the reason this cannot be done at CFG time is because we have not added the init temp decls? Or is it for a cleaner separation of logic? We could add those as part of CFG building and during optimize() shift non-side-effect things before the begin starts? It might read cleaner as a pass but the idea of requiring m*n linear passes makes me want m to be as small as possible. |
Cannot be done at CFG build time, because the SESE region identification requires the CFG. This will be faster pass since we are primarily examining only basic blocks, not instructions (by setting flags on the basic-block while cfg is being built ... the flag would be: bb.canRaiseException() ..). That said, i haven't thought about this a whole lot yet. |
A quick update. I got this implemented over the weekend -- need to test it a bit more throughly. |
* This was an experimental fix for issue #1786. * Now abandoning that experiment, but this shrinking is potentially interesting on its own merit. For now, committing this code for putting this in git history.
Marking for 9k but we now do nil inits now after we run DCE. The problem has been mitigated but it would be nice to solve this in a more robust way. (probably not 9k initial release issue) |
I am going to close this and open a new issue that is about handling the nil initialization problem more generally. Now that ensure tmp vars are assigned pass runs at the end just before ir -> bytecode compilation, it handles JVM semantics of having the necessary inits. That said, we need a generic solution that ensures that all local/tmps are assigned - this will also let us get rid of the return lookup-var OR return nil pattern in DynamicScope, interpreter, and jit. |
For this code:
DCE appears to be wiping out necessary initialization of "osync" that I'm doing in my EnsureTempsAssigned pass.
Here's the IR before DCE, with assignments intact:
And here it is after:
This blocks JIT because I have no way to ensure I'm initializing all JVM locals (mapped to IR temp vars) at the IR level.
The text was updated successfully, but these errors were encountered: