Simplify SimplifyAlwaysThrows
Remove most of the preconditions of the optimization
since they are not needed e.g.:
* Ending with a Goto
* Not flowing to the Exit block
* The successor block having another predecessor
* Not doing the optimization in a catch block
Test: art/test/testrunner/testrunner.py --host --64 --optimizing -b
Change-Id: Idd0c722d1cd34fa6c394338a2e475a7ff60bd88a
diff --git a/compiler/optimizing/dead_code_elimination.cc b/compiler/optimizing/dead_code_elimination.cc
index 0ce8bfa..8e3010d 100644
--- a/compiler/optimizing/dead_code_elimination.cc
+++ b/compiler/optimizing/dead_code_elimination.cc
@@ -236,8 +236,11 @@
// B2 Exit
//
// Rationale:
-// Removal of the never taken edge to B2 may expose
-// other optimization opportunities, such as code sinking.
+// Removal of the never taken edge to B2 may expose other optimization opportunities, such as code
+// sinking.
+//
+// Note: The example above is a simple one that uses a `goto` but we could end the block with an If,
+// for example.
bool HDeadCodeElimination::SimplifyAlwaysThrows() {
HBasicBlock* exit = graph_->GetExitBlock();
if (!graph_->HasAlwaysThrowingInvokes() || exit == nullptr) {
@@ -248,70 +251,56 @@
// Order does not matter, just pick one.
for (HBasicBlock* block : graph_->GetReversePostOrder()) {
- if (block->GetTryCatchInformation() != nullptr) {
+ if (block->IsTryBlock()) {
// We don't want to perform the simplify always throws optimizations for throws inside of
- // tries since those throws might not go to the exit block. We do that by checking the
- // TryCatchInformation of the blocks.
- //
- // As a special case the `catch_block` is the first block of the catch and it has
- // TryCatchInformation. Other blocks in the catch don't have try catch information (as long as
- // they are not part of an outer try). Knowing if a `catch_block` is part of an outer try is
- // possible by checking its successors, but other restrictions of the simplify always throws
- // optimization will block `catch_block` nevertheless (e.g. only one predecessor) so it is not
- // worth the effort.
-
- // TODO(solanes): Maybe we can do a `goto catch` if inside of a try catch instead of going to
- // the exit. If we do so, we have to take into account that we should go to the nearest valid
- // catch i.e. one that would accept our exception type.
+ // tries since those throws might not go to the exit block.
continue;
}
- if (block->GetLastInstruction()->IsGoto() &&
- block->GetPhis().IsEmpty() &&
- block->GetPredecessors().size() == 1u) {
- HBasicBlock* pred = block->GetSinglePredecessor();
- HBasicBlock* succ = block->GetSingleSuccessor();
- // Ensure no computations are merged through throwing block. This does not prevent the
- // optimization per se, but would require an elaborate clean up of the SSA graph.
- if (succ != exit &&
- !block->Dominates(pred) &&
- pred->Dominates(succ) &&
- succ->GetPredecessors().size() > 1u &&
- succ->GetPhis().IsEmpty()) {
- // We iterate to find the first instruction that always throws. If two instructions always
- // throw, the first one will throw and the second one will never be reached.
- HInstruction* throwing_instruction = nullptr;
- for (HInstructionIterator it(block->GetInstructions()); !it.Done(); it.Advance()) {
- if (it.Current()->AlwaysThrows()) {
- throwing_instruction = it.Current();
- break;
- }
- }
-
- if (throwing_instruction == nullptr) {
- // No always-throwing instruction found. Continue with the rest of the blocks.
- continue;
- }
-
- // We split the block at the throwing instruction, and the instructions after the throwing
- // instructions will be disconnected from the graph after `block` points to the exit.
- // `RemoveDeadBlocks` will take care of removing this new block and its instructions.
- // Even though `SplitBefore` doesn't guarantee the graph to remain in SSA form, it is fine
- // since we do not break it.
- HBasicBlock* new_block = block->SplitBefore(throwing_instruction->GetNext(),
- /* require_graph_not_in_ssa_form= */ false);
- DCHECK_EQ(block->GetSingleSuccessor(), new_block);
- block->ReplaceSuccessor(new_block, exit);
-
- rerun_dominance_and_loop_analysis = true;
- MaybeRecordStat(stats_, MethodCompilationStat::kSimplifyThrowingInvoke);
- // Perform a quick follow up optimization on object != null control dependences
- // that is much cheaper to perform now than in a later phase.
- if (RemoveNonNullControlDependences(pred, block)) {
- MaybeRecordStat(stats_, MethodCompilationStat::kRemovedNullCheck);
- }
+ // We iterate to find the first instruction that always throws. If two instructions always
+ // throw, the first one will throw and the second one will never be reached.
+ HInstruction* throwing_invoke = nullptr;
+ for (HInstructionIterator it(block->GetInstructions()); !it.Done(); it.Advance()) {
+ if (it.Current()->IsInvoke() && it.Current()->AsInvoke()->AlwaysThrows()) {
+ throwing_invoke = it.Current();
+ break;
}
}
+
+ if (throwing_invoke == nullptr) {
+ // No always-throwing instruction found. Continue with the rest of the blocks.
+ continue;
+ }
+
+ // If we are already pointing at the exit block we could still remove the instructions
+ // between the always throwing instruction, and the exit block. If we have no other
+ // instructions, just continue since there's nothing to do.
+ if (block->GetSuccessors().size() == 1 &&
+ block->GetSingleSuccessor() == exit &&
+ block->GetLastInstruction()->GetPrevious() == throwing_invoke) {
+ continue;
+ }
+
+ // We split the block at the throwing instruction, and the instructions after the throwing
+ // instructions will be disconnected from the graph after `block` points to the exit.
+ // `RemoveDeadBlocks` will take care of removing this new block and its instructions.
+ // Even though `SplitBefore` doesn't guarantee the graph to remain in SSA form, it is fine
+ // since we do not break it.
+ HBasicBlock* new_block = block->SplitBefore(throwing_invoke->GetNext(),
+ /* require_graph_not_in_ssa_form= */ false);
+ DCHECK_EQ(block->GetSingleSuccessor(), new_block);
+ block->ReplaceSuccessor(new_block, exit);
+
+ rerun_dominance_and_loop_analysis = true;
+ MaybeRecordStat(stats_, MethodCompilationStat::kSimplifyThrowingInvoke);
+ // Perform a quick follow up optimization on object != null control dependences
+ // that is much cheaper to perform now than in a later phase.
+ // If there are multiple predecessors, none may end with a HIf as required in
+ // RemoveNonNullControlDependences because we split critical edges.
+ if (block->GetPredecessors().size() == 1u &&
+ RemoveNonNullControlDependences(block->GetSinglePredecessor(), block)) {
+ MaybeRecordStat(stats_, MethodCompilationStat::kRemovedNullCheck);
+ }
}
// We need to re-analyze the graph in order to run DCE afterwards.
diff --git a/compiler/optimizing/nodes.h b/compiler/optimizing/nodes.h
index 8f7635a..58ce9ce 100644
--- a/compiler/optimizing/nodes.h
+++ b/compiler/optimizing/nodes.h
@@ -4734,7 +4734,7 @@
void SetAlwaysThrows(bool always_throws) { SetPackedFlag<kFlagAlwaysThrows>(always_throws); }
- bool AlwaysThrows() const override { return GetPackedFlag<kFlagAlwaysThrows>(); }
+ bool AlwaysThrows() const override final { return GetPackedFlag<kFlagAlwaysThrows>(); }
bool CanBeMoved() const override { return IsIntrinsic() && !DoesAnyWrite(); }
diff --git a/test/2042-checker-dce-always-throw/src/Main.java b/test/2042-checker-dce-always-throw/src/Main.java
index 3ee9183..e164917 100644
--- a/test/2042-checker-dce-always-throw/src/Main.java
+++ b/test/2042-checker-dce-always-throw/src/Main.java
@@ -31,8 +31,20 @@
// Test that we update the phis correctly after simplifying an always throwing method, and
// recomputing dominance.
- assertEquals(0, $noinline$testUpdatePhisCorrectly(1, 1));
- assertEquals(0, $noinline$testDeleteAllUsesBeforeDeletingInstruction(1, 1));
+ assertEquals(0, $noinline$testUpdatePhisCorrectly(1));
+ assertEquals(0, $noinline$testDeleteAllUsesBeforeDeletingInstruction(1));
+
+ // SimplifyAlwaysThrows for blocks without a goto at the end
+ assertEquals(0, $noinline$testEndsWithIf(1, 1));
+ assertEquals(0, $noinline$testEndsWithReturn(1));
+ // Since we cannot use `assertEquals`, not throwing would be the success.
+ $noinline$testEndsWithReturnVoid(1);
+ assertEquals(0, $noinline$testEndsWithSwitch(1, 1));
+ assertEquals(0, $noinline$testEndsWithThrow(1));
+ assertEquals(0, $noinline$testEndsWithTryBoundary(1));
+
+ // SimplifyAlwaysThrows for invokes in catch blocks
+ assertEquals(0, $noinline$testInsideCatch(1));
}
private static void alwaysThrows() throws Error {
@@ -299,7 +311,7 @@
// Check that when we perform SimplifyAlwaysThrows, that the phi for `phi_value` exists, and that
// we correctly update it after running DCE.
- /// CHECK-START: int Main.$noinline$testUpdatePhisCorrectly(int, int) dead_code_elimination$after_inlining (before)
+ /// CHECK-START: int Main.$noinline$testUpdatePhisCorrectly(int) dead_code_elimination$after_inlining (before)
/// CHECK-DAG: <<Const0:i\d+>> IntConstant 0
/// CHECK-DAG: <<Const5:i\d+>> IntConstant 5
/// CHECK-DAG: <<ReturnValue:i\d+>> Phi [<<Const0>>,<<Const5>>]
@@ -309,71 +321,51 @@
/// CHECK-DAG: Goto block:<<InvokeBlock>> target:<<TargetBlock:B\d+>>
/// CHECK-EVAL: "<<ExitBlock>>" != "<<TargetBlock>>"
- /// CHECK-START: int Main.$noinline$testUpdatePhisCorrectly(int, int) dead_code_elimination$after_inlining (after)
+ /// CHECK-START: int Main.$noinline$testUpdatePhisCorrectly(int) dead_code_elimination$after_inlining (after)
/// CHECK-DAG: <<Const0:i\d+>> IntConstant 0
/// CHECK-DAG: Return [<<Const0>>]
/// CHECK-DAG: InvokeStaticOrDirect block:<<InvokeBlock:B\d+>> method_name:Main.alwaysThrows always_throws:true
/// CHECK-DAG: Exit block:<<ExitBlock:B\d+>>
/// CHECK-DAG: Goto block:<<InvokeBlock>> target:<<ExitBlock>>
- /// CHECK-START: int Main.$noinline$testUpdatePhisCorrectly(int, int) dead_code_elimination$after_inlining (after)
+ /// CHECK-START: int Main.$noinline$testUpdatePhisCorrectly(int) dead_code_elimination$after_inlining (after)
/// CHECK-NOT: Phi
- private static int $noinline$testUpdatePhisCorrectly(int num, int other_num) {
+ private static int $noinline$testUpdatePhisCorrectly(int num) {
int phi_value = 0;
if (num == 0) {
alwaysThrows();
-
- // This while loop is here so that the `if (num == 0)` will be several blocks instead of
- // just one. We use `other_num` here because otherwise we propagate the knowledge that `num`
- // equals zero.
- while (other_num == 0) {
- // Assign to phi_value so that the loop is not empty.
- phi_value = 2;
- }
-
phi_value = 5;
}
return phi_value;
}
-
// Test to check that we delete all uses before the instruction.
private static int $noinline$foo(int num) {
return num;
}
- /// CHECK-START: int Main.$noinline$testDeleteAllUsesBeforeDeletingInstruction(int, int) dead_code_elimination$after_inlining (before)
+ /// CHECK-START: int Main.$noinline$testDeleteAllUsesBeforeDeletingInstruction(int) dead_code_elimination$after_inlining (before)
/// CHECK-DAG: <<Const0:i\d+>> IntConstant 0
/// CHECK-DAG: <<Invoke:i\d+>> InvokeStaticOrDirect method_name:Main.$noinline$foo
/// CHECK-DAG: <<ReturnValue:i\d+>> Phi [<<Const0>>,<<Invoke>>]
/// CHECK-DAG: Return [<<ReturnValue>>]
/// CHECK-DAG: InvokeStaticOrDirect block:<<InvokeBlock:B\d+>> method_name:Main.alwaysThrows always_throws:true
+ /// CHECK-DAG: TryBoundary block:<<InvokeBlock>>
/// CHECK-DAG: Exit block:<<ExitBlock:B\d+>>
- /// CHECK-DAG: Goto block:<<InvokeBlock>> target:<<TargetBlock:B\d+>>
- /// CHECK-EVAL: "<<ExitBlock>>" != "<<TargetBlock>>"
- /// CHECK-START: int Main.$noinline$testDeleteAllUsesBeforeDeletingInstruction(int, int) dead_code_elimination$after_inlining (after)
+ /// CHECK-START: int Main.$noinline$testDeleteAllUsesBeforeDeletingInstruction(int) dead_code_elimination$after_inlining (after)
/// CHECK-DAG: <<Const0:i\d+>> IntConstant 0
/// CHECK-DAG: Return [<<Const0>>]
/// CHECK-DAG: InvokeStaticOrDirect block:<<InvokeBlock:B\d+>> method_name:Main.alwaysThrows always_throws:true
/// CHECK-DAG: Exit block:<<ExitBlock:B\d+>>
/// CHECK-DAG: Goto block:<<InvokeBlock>> target:<<ExitBlock>>
- /// CHECK-START: int Main.$noinline$testDeleteAllUsesBeforeDeletingInstruction(int, int) dead_code_elimination$after_inlining (after)
+ /// CHECK-START: int Main.$noinline$testDeleteAllUsesBeforeDeletingInstruction(int) dead_code_elimination$after_inlining (after)
/// CHECK-NOT: Phi
- private static int $noinline$testDeleteAllUsesBeforeDeletingInstruction(int num, int other_num) {
+ private static int $noinline$testDeleteAllUsesBeforeDeletingInstruction(int num) {
int phi_value = 0;
if (num == 0) {
alwaysThrows();
-
- // This while loop is here so that we have a Goto after `alwaysThrows` and therefore perform
- // the `SimplifyAlwaysThrows` optimization. We use `other_num` here because otherwise we
- // propagate the knowledge that `num` equals zero.
- while (other_num == 0) {
- // Assign to phi_value so that the loop is not empty.
- phi_value = 2;
- }
-
try {
phi_value = $noinline$foo(2);
} catch (Error e) {
@@ -383,6 +375,187 @@
return phi_value;
}
+ /// CHECK-START: int Main.$noinline$testEndsWithIf(int, int) dead_code_elimination$after_inlining (before)
+ /// CHECK-DAG: InvokeStaticOrDirect block:<<InvokeBlock:B\d+>> method_name:Main.alwaysThrows always_throws:true
+ /// CHECK-DAG: If block:<<InvokeBlock>>
+ /// CHECK-DAG: InvokeVirtual method_name:java.io.PrintStream.println
+ /// CHECK-DAG: Exit block:<<ExitBlock:B\d+>>
+
+ /// CHECK-START: int Main.$noinline$testEndsWithIf(int, int) dead_code_elimination$after_inlining (after)
+ /// CHECK-DAG: InvokeStaticOrDirect block:<<InvokeBlock:B\d+>> method_name:Main.alwaysThrows always_throws:true
+ /// CHECK-DAG: Exit block:<<ExitBlock:B\d+>>
+ /// CHECK-DAG: Goto block:<<InvokeBlock>> target:<<ExitBlock>>
+
+ /// CHECK-START: int Main.$noinline$testEndsWithIf(int, int) dead_code_elimination$after_inlining (after)
+ /// CHECK-NOT: InvokeVirtual method_name:java.io.PrintStream.println
+ private static int $noinline$testEndsWithIf(int num, int other_num) {
+ if (num == 0) {
+ alwaysThrows();
+ if (other_num == 0) {
+ System.out.println("I am unrechable!");
+ }
+ }
+ return 0;
+ }
+
+ /// CHECK-START: int Main.$noinline$testEndsWithReturn(int) dead_code_elimination$after_inlining (before)
+ /// CHECK-DAG: InvokeStaticOrDirect block:<<InvokeBlock:B\d+>> method_name:Main.alwaysThrows always_throws:true
+ /// CHECK-DAG: Return block:<<InvokeBlock>>
+ /// CHECK-DAG: InvokeVirtual method_name:java.io.PrintStream.println
+ /// CHECK-DAG: Exit block:<<ExitBlock:B\d+>>
+
+ /// CHECK-START: int Main.$noinline$testEndsWithReturn(int) dead_code_elimination$after_inlining (after)
+ /// CHECK-DAG: InvokeStaticOrDirect block:<<InvokeBlock:B\d+>> method_name:Main.alwaysThrows always_throws:true
+ /// CHECK-DAG: Exit block:<<ExitBlock:B\d+>>
+ /// CHECK-DAG: Goto block:<<InvokeBlock>> target:<<ExitBlock>>
+
+ /// CHECK-START: int Main.$noinline$testEndsWithReturn(int) dead_code_elimination$after_inlining (after)
+ /// CHECK-NOT: InvokeVirtual method_name:java.io.PrintStream.println
+ private static int $noinline$testEndsWithReturn(int num) {
+ if (num == 0) {
+ alwaysThrows();
+ System.out.println("I am unrechable!");
+ return 1;
+ }
+ return 0;
+ }
+
+ /// CHECK-START: void Main.$noinline$testEndsWithReturnVoid(int) dead_code_elimination$after_inlining (before)
+ /// CHECK-DAG: InvokeStaticOrDirect block:<<InvokeBlock:B\d+>> method_name:Main.alwaysThrows always_throws:true
+ /// CHECK-DAG: ReturnVoid block:<<InvokeBlock>>
+ /// CHECK-DAG: InvokeVirtual method_name:java.io.PrintStream.println
+ /// CHECK-DAG: Exit block:<<ExitBlock:B\d+>>
+
+ /// CHECK-START: void Main.$noinline$testEndsWithReturnVoid(int) dead_code_elimination$after_inlining (after)
+ /// CHECK-DAG: InvokeStaticOrDirect block:<<InvokeBlock:B\d+>> method_name:Main.alwaysThrows always_throws:true
+ /// CHECK-DAG: Exit block:<<ExitBlock:B\d+>>
+ /// CHECK-DAG: Goto block:<<InvokeBlock>> target:<<ExitBlock>>
+
+ /// CHECK-START: void Main.$noinline$testEndsWithReturnVoid(int) dead_code_elimination$after_inlining (after)
+ /// CHECK-NOT: InvokeVirtual method_name:java.io.PrintStream.println
+ private static void $noinline$testEndsWithReturnVoid(int num) {
+ if (num == 0) {
+ alwaysThrows();
+ System.out.println("I am unrechable!");
+ return;
+ }
+ return;
+ }
+
+ /// CHECK-START: int Main.$noinline$testEndsWithSwitch(int, int) dead_code_elimination$after_inlining (before)
+ /// CHECK-DAG: InvokeStaticOrDirect block:<<InvokeBlock:B\d+>> method_name:Main.alwaysThrows always_throws:true
+ /// CHECK-DAG: PackedSwitch block:<<InvokeBlock>>
+ /// CHECK-DAG: InvokeVirtual method_name:java.io.PrintStream.println
+ /// CHECK-DAG: Exit block:<<ExitBlock:B\d+>>
+
+ /// CHECK-START: int Main.$noinline$testEndsWithSwitch(int, int) dead_code_elimination$after_inlining (after)
+ /// CHECK-DAG: InvokeStaticOrDirect block:<<InvokeBlock:B\d+>> method_name:Main.alwaysThrows always_throws:true
+ /// CHECK-DAG: Exit block:<<ExitBlock:B\d+>>
+ /// CHECK-DAG: Goto block:<<InvokeBlock>> target:<<ExitBlock>>
+
+ /// CHECK-START: int Main.$noinline$testEndsWithSwitch(int, int) dead_code_elimination$after_inlining (after)
+ /// CHECK-NOT: InvokeVirtual method_name:java.io.PrintStream.println
+ private static int $noinline$testEndsWithSwitch(int num, int other_num) {
+ if (num == 0) {
+ alwaysThrows();
+ System.out.println("I am unrechable!");
+ int result = 10;
+ switch (other_num) {
+ case 1:
+ result = 100;
+ break;
+ case 2:
+ result = 300;
+ break;
+ case 3:
+ result = 500;
+ break;
+ case 4:
+ result = 700;
+ break;
+ }
+ return result;
+ }
+ return 0;
+ }
+
+ /// CHECK-START: int Main.$noinline$testEndsWithThrow(int) dead_code_elimination$after_inlining (before)
+ /// CHECK-DAG: InvokeStaticOrDirect block:<<InvokeBlock:B\d+>> method_name:Main.alwaysThrows always_throws:true
+ /// CHECK-DAG: Throw block:<<InvokeBlock>>
+ /// CHECK-DAG: InvokeVirtual method_name:java.io.PrintStream.println
+ /// CHECK-DAG: Exit block:<<ExitBlock:B\d+>>
+
+ /// CHECK-START: int Main.$noinline$testEndsWithThrow(int) dead_code_elimination$after_inlining (after)
+ /// CHECK-DAG: InvokeStaticOrDirect block:<<InvokeBlock:B\d+>> method_name:Main.alwaysThrows always_throws:true
+ /// CHECK-DAG: Exit block:<<ExitBlock:B\d+>>
+ /// CHECK-DAG: Goto block:<<InvokeBlock>> target:<<ExitBlock>>
+
+ /// CHECK-START: int Main.$noinline$testEndsWithThrow(int) dead_code_elimination$after_inlining (after)
+ /// CHECK-NOT: InvokeVirtual method_name:java.io.PrintStream.println
+ private static int $noinline$testEndsWithThrow(int num) {
+ if (num == 0) {
+ alwaysThrows();
+ System.out.println("I am unrechable!");
+ throw new Error("Other error");
+ }
+ return 0;
+ }
+
+ /// CHECK-START: int Main.$noinline$testEndsWithTryBoundary(int) dead_code_elimination$after_inlining (before)
+ /// CHECK-DAG: InvokeStaticOrDirect block:<<InvokeBlock:B\d+>> method_name:Main.alwaysThrows always_throws:true
+ /// CHECK-DAG: TryBoundary block:<<InvokeBlock>>
+ /// CHECK-DAG: InvokeVirtual method_name:java.io.PrintStream.println
+ /// CHECK-DAG: Exit block:<<ExitBlock:B\d+>>
+
+ /// CHECK-START: int Main.$noinline$testEndsWithTryBoundary(int) dead_code_elimination$after_inlining (after)
+ /// CHECK-DAG: InvokeStaticOrDirect block:<<InvokeBlock:B\d+>> method_name:Main.alwaysThrows always_throws:true
+ /// CHECK-DAG: Exit block:<<ExitBlock:B\d+>>
+ /// CHECK-DAG: Goto block:<<InvokeBlock>> target:<<ExitBlock>>
+
+ /// CHECK-START: int Main.$noinline$testEndsWithTryBoundary(int) dead_code_elimination$after_inlining (after)
+ /// CHECK-NOT: InvokeVirtual method_name:java.io.PrintStream.println
+ private static int $noinline$testEndsWithTryBoundary(int num) {
+ if (num == 0) {
+ alwaysThrows();
+ System.out.println("I am unrechable!");
+ try {
+ alwaysThrows();
+ } catch (Error e) {
+ return 1;
+ }
+ }
+ return 0;
+ }
+
+ // Empty method to forbid the try from disappearing
+ private static void $noinline$emptyMethod() {}
+
+ /// CHECK-START: int Main.$noinline$testInsideCatch(int) dead_code_elimination$after_inlining (before)
+ /// CHECK-DAG: InvokeStaticOrDirect block:<<InvokeBlock:B\d+>> method_name:Main.alwaysThrows always_throws:true
+ /// CHECK-DAG: Return block:<<InvokeBlock>>
+ /// CHECK-DAG: InvokeVirtual method_name:java.io.PrintStream.println
+ /// CHECK-DAG: Exit block:<<ExitBlock:B\d+>>
+
+ /// CHECK-START: int Main.$noinline$testInsideCatch(int) dead_code_elimination$after_inlining (after)
+ /// CHECK-DAG: InvokeStaticOrDirect block:<<InvokeBlock:B\d+>> method_name:Main.alwaysThrows always_throws:true
+ /// CHECK-DAG: Exit block:<<ExitBlock:B\d+>>
+ /// CHECK-DAG: Goto block:<<InvokeBlock>> target:<<ExitBlock>>
+
+ /// CHECK-START: int Main.$noinline$testInsideCatch(int) dead_code_elimination$after_inlining (after)
+ /// CHECK-NOT: InvokeVirtual method_name:java.io.PrintStream.println
+ private static int $noinline$testInsideCatch(int num) {
+ if (num == 0) {
+ try {
+ $noinline$emptyMethod();
+ } catch (Error e) {
+ alwaysThrows();
+ System.out.println("I am unrechable!");
+ return 1;
+ }
+ }
+ return 0;
+ }
+
static void assertEquals(int expected, int actual) {
if (expected != actual) {
throw new AssertionError("Expected " + expected + " got " + actual);