diff --git a/runtime/vm/compiler/backend/redundancy_elimination.cc b/runtime/vm/compiler/backend/redundancy_elimination.cc index a7a16100d9c..f544dfc0f23 100644 --- a/runtime/vm/compiler/backend/redundancy_elimination.cc +++ b/runtime/vm/compiler/backend/redundancy_elimination.cc @@ -1539,6 +1539,142 @@ void LICM::Optimize() { } } +void DelayAllocations::Optimize(FlowGraph* graph) { + // Go through all AllocateObject instructions and move them down to their + // dominant use when doing so is sound. + DirectChainedHashMap> moved; + for (BlockIterator block_it = graph->reverse_postorder_iterator(); + !block_it.Done(); block_it.Advance()) { + BlockEntryInstr* block = block_it.Current(); + + for (ForwardInstructionIterator instr_it(block); !instr_it.Done(); + instr_it.Advance()) { + Definition* def = instr_it.Current()->AsDefinition(); + if (def != nullptr && (def->IsAllocateObject() || def->IsCreateArray()) && + def->env() == nullptr && !moved.HasKey(def)) { + Instruction* use = DominantUse(def); + if (use != nullptr && IsOneTimeUse(use, def)) { + instr_it.RemoveCurrentFromGraph(); + def->InsertBefore(use); + moved.Insert(def); + } + } + } + } +} + +Instruction* DelayAllocations::DominantUse(Definition* def) { + // Find the use that dominates all other uses. + + // Quick case for no uses or only one use. + Value* maybe_only_use = def->input_use_list(); + if (maybe_only_use == nullptr) return nullptr; + if (def->HasOnlyUse(maybe_only_use)) { + Instruction* use = maybe_only_use->instruction(); + return use->IsPhi() ? nullptr : use; + } + + // Collect all blocks containing uses. + DirectChainedHashMap> use_blocks; + for (Value::Iterator it(def->input_use_list()); !it.Done(); it.Advance()) { + Instruction* use = it.Current()->instruction(); + if (auto phi = use->AsPhi()) { + // For phi uses, the dominant use only has to dominate the + // predecessor block corresponding to the phi input. + use_blocks.Insert(phi->block()->PredecessorAt(it.Current()->use_index())); + } else { + use_blocks.Insert(use->GetBlock()); + } + } + for (Value::Iterator it(def->env_use_list()); !it.Done(); it.Advance()) { + Instruction* use = it.Current()->instruction(); + use_blocks.Insert(use->GetBlock()); + } + + // Find the common dominator block of all blocks containing uses. + BlockEntryInstr* common_dominator = nullptr; + auto block_it = use_blocks.GetIterator(); + while (auto block = block_it.Next()) { + bool dominated = false; + for (auto dom = (*block)->dominator(); dom != nullptr; + dom = dom->dominator()) { + if (use_blocks.HasKey(dom)) { + dominated = true; + break; + } + } + if (!dominated) { + // Potential common dominator block. + if (common_dominator != nullptr && common_dominator != *block) { + // No common dominator of all uses. + return nullptr; + } + common_dominator = *block; + } + } + + // Collect uses in block. + DirectChainedHashMap> uses_in_block; + for (Value::Iterator it(def->input_use_list()); !it.Done(); it.Advance()) { + Instruction* use = it.Current()->instruction(); + if (!use->IsPhi() && use->GetBlock() == common_dominator) { + uses_in_block.Insert(use); + } + } + for (Value::Iterator it(def->env_use_list()); !it.Done(); it.Advance()) { + Instruction* use = it.Current()->instruction(); + if (use->GetBlock() == common_dominator) { + uses_in_block.Insert(use); + } + } + + // Find first use in block. + Instruction* first_use = nullptr; + auto use_it = uses_in_block.GetIterator(); + while (auto use = use_it.Next()) { + bool dominated = false; + for (auto instr = (*use)->previous(); instr != nullptr; + instr = instr->previous()) { + if (uses_in_block.HasKey(instr)) { + dominated = true; + break; + } + } + if (!dominated) { + first_use = *use; + break; + } + } + + return first_use; +} + +bool DelayAllocations::IsOneTimeUse(Instruction* use, Definition* def) { + // Check that this use is always executed at most once for each execution of + // the definition, i.e. that there is no path from the use to itself that + // doesn't pass through the definition. + BlockEntryInstr* use_block = use->GetBlock(); + BlockEntryInstr* def_block = def->GetBlock(); + if (use_block == def_block) return true; + + DirectChainedHashMap> seen; + GrowableArray worklist; + worklist.Add(use_block); + + while (!worklist.is_empty()) { + BlockEntryInstr* block = worklist.RemoveLast(); + for (intptr_t i = 0; i < block->PredecessorCount(); i++) { + BlockEntryInstr* pred = block->PredecessorAt(i); + if (pred == use_block) return false; + if (pred == def_block) continue; + if (seen.HasKey(pred)) continue; + seen.Insert(pred); + worklist.Add(pred); + } + } + return true; +} + class LoadOptimizer : public ValueObject { public: LoadOptimizer(FlowGraph* graph, AliasedSet* aliased_set) diff --git a/runtime/vm/compiler/backend/redundancy_elimination.h b/runtime/vm/compiler/backend/redundancy_elimination.h index 84fc7d29c7b..ab092708d12 100644 --- a/runtime/vm/compiler/backend/redundancy_elimination.h +++ b/runtime/vm/compiler/backend/redundancy_elimination.h @@ -145,6 +145,16 @@ class LICM : public ValueObject { FlowGraph* const flow_graph_; }; +// Move allocations down to their first use. Improves write barrier elimination. +class DelayAllocations : public AllStatic { + public: + static void Optimize(FlowGraph* graph); + + private: + static Instruction* DominantUse(Definition* def); + static bool IsOneTimeUse(Instruction* use, Definition* def); +}; + class CheckStackOverflowElimination : public AllStatic { public: // For leaf functions with only a single [StackOverflowInstr] we remove it. diff --git a/runtime/vm/compiler/backend/redundancy_elimination_test.cc b/runtime/vm/compiler/backend/redundancy_elimination_test.cc index 21d67ffc692..74cf38ec22a 100644 --- a/runtime/vm/compiler/backend/redundancy_elimination_test.cc +++ b/runtime/vm/compiler/backend/redundancy_elimination_test.cc @@ -1114,4 +1114,105 @@ ISOLATE_UNIT_TEST_CASE(LoadOptimizer_RedundantInitializerCallInLoop) { EXPECT(load_field_in_loop2->calls_initializer()); } +#if !defined(TARGET_ARCH_IA32) + +ISOLATE_UNIT_TEST_CASE(DelayAllocations_DelayAcrossCalls) { + const char* kScript = R"( + class A { + dynamic x, y; + A(this.x, this.y); + } + + int count = 0; + + @pragma("vm:never-inline") + dynamic foo(int i) => count++ < 2 ? i : '$i'; + + @pragma("vm:never-inline") + dynamic use(v) {} + + void test() { + A a = new A(foo(1), foo(2)); + use(a); + } + )"; + + const auto& root_library = Library::Handle(LoadTestScript(kScript)); + const auto& function = Function::Handle(GetFunction(root_library, "test")); + + // Get fields to kDynamicCid guard + Invoke(root_library, "test"); + Invoke(root_library, "test"); + + TestPipeline pipeline(function, CompilerPass::kAOT); + FlowGraph* flow_graph = pipeline.RunPasses({}); + auto entry = flow_graph->graph_entry()->normal_entry(); + + StaticCallInstr* call1; + StaticCallInstr* call2; + AllocateObjectInstr* allocate; + StoreInstanceFieldInstr* store1; + StoreInstanceFieldInstr* store2; + + ILMatcher cursor(flow_graph, entry, true, ParallelMovesHandling::kSkip); + RELEASE_ASSERT(cursor.TryMatch({ + kMoveGlob, + {kMatchAndMoveStaticCall, &call1}, + kMoveGlob, + {kMatchAndMoveStaticCall, &call2}, + kMoveGlob, + {kMatchAndMoveAllocateObject, &allocate}, + {kMatchAndMoveStoreInstanceField, &store1}, + {kMatchAndMoveStoreInstanceField, &store2}, + })); + + EXPECT(strcmp(call1->function().UserVisibleNameCString(), "foo") == 0); + EXPECT(strcmp(call2->function().UserVisibleNameCString(), "foo") == 0); + EXPECT(store1->instance()->definition() == allocate); + EXPECT(!store1->ShouldEmitStoreBarrier()); + EXPECT(store2->instance()->definition() == allocate); + EXPECT(!store2->ShouldEmitStoreBarrier()); +} + +ISOLATE_UNIT_TEST_CASE(DelayAllocations_DontDelayIntoLoop) { + const char* kScript = R"( + void test() { + Object o = new Object(); + for (int i = 0; i < 10; i++) { + use(o); + } + } + + @pragma('vm:never-inline') + void use(Object o) { + print(o.hashCode); + } + )"; + + const auto& root_library = Library::Handle(LoadTestScript(kScript)); + const auto& function = Function::Handle(GetFunction(root_library, "test")); + + TestPipeline pipeline(function, CompilerPass::kAOT); + FlowGraph* flow_graph = pipeline.RunPasses({}); + auto entry = flow_graph->graph_entry()->normal_entry(); + + AllocateObjectInstr* allocate; + StaticCallInstr* call; + + ILMatcher cursor(flow_graph, entry, true, ParallelMovesHandling::kSkip); + RELEASE_ASSERT(cursor.TryMatch({ + kMoveGlob, + {kMatchAndMoveAllocateObject, &allocate}, + kMoveGlob, + kMatchAndMoveBranchTrue, + kMoveGlob, + {kMatchAndMoveStaticCall, &call}, + })); + + EXPECT(strcmp(call->function().UserVisibleNameCString(), "use") == 0); + EXPECT(call->Receiver()->definition() == allocate); +} + +#endif // !defined(TARGET_ARCH_IA32) + } // namespace dart diff --git a/runtime/vm/compiler/compiler_pass.cc b/runtime/vm/compiler/compiler_pass.cc index d5e6297197e..691ef289c89 100644 --- a/runtime/vm/compiler/compiler_pass.cc +++ b/runtime/vm/compiler/compiler_pass.cc @@ -224,6 +224,15 @@ void CompilerPass::PrintGraph(CompilerPassState* state, #define INVOKE_PASS(Name) \ CompilerPass::Get(CompilerPass::k##Name)->Run(pass_state); +#if defined(DART_PRECOMPILER) +#define INVOKE_PASS_AOT(Name) \ + if (mode == kAOT) { \ + INVOKE_PASS(Name); \ + } +#else +#define INVOKE_PASS_AOT(Name) +#endif + void CompilerPass::RunGraphIntrinsicPipeline(CompilerPassState* pass_state) { INVOKE_PASS(AllocateRegistersForGraphIntrinsic); } @@ -241,9 +250,7 @@ void CompilerPass::RunInliningPipeline(PipelineMode mode, // may open more opportunities for call specialization. // Call specialization during inlining may cause more call // sites to be discovered and more functions inlined. - if (mode == kAOT) { - INVOKE_PASS(ApplyClassIds); - } + INVOKE_PASS_AOT(ApplyClassIds); // Optimize (a << b) & c patterns, merge instructions. Must occur // before 'SelectRepresentations' which inserts conversion nodes. INVOKE_PASS(TryOptimizePatterns); @@ -272,13 +279,10 @@ FlowGraph* CompilerPass::RunForceOptimizedPipeline( // so it should not be lifted earlier than that pass. INVOKE_PASS(DCE); INVOKE_PASS(Canonicalize); + INVOKE_PASS_AOT(DelayAllocations); INVOKE_PASS(EliminateWriteBarriers); INVOKE_PASS(FinalizeGraph); -#if defined(DART_PRECOMPILER) - if (mode == kAOT) { - INVOKE_PASS(SerializeGraph); - } -#endif + INVOKE_PASS_AOT(SerializeGraph); if (FLAG_late_round_trip_serialization) { INVOKE_PASS(RoundTripSerialization); } @@ -293,12 +297,8 @@ FlowGraph* CompilerPass::RunPipeline(PipelineMode mode, if (FLAG_early_round_trip_serialization) { INVOKE_PASS(RoundTripSerialization); } -#if defined(DART_PRECOMPILER) - if (mode == kAOT) { - INVOKE_PASS(ApplyClassIds); - INVOKE_PASS(TypePropagation); - } -#endif + INVOKE_PASS_AOT(ApplyClassIds); + INVOKE_PASS_AOT(TypePropagation); INVOKE_PASS(ApplyICData); INVOKE_PASS(TryOptimizePatterns); INVOKE_PASS(SetOuterInliningId); @@ -316,18 +316,12 @@ FlowGraph* CompilerPass::RunPipeline(PipelineMode mode, INVOKE_PASS(ConstantPropagation); INVOKE_PASS(OptimisticallySpecializeSmiPhis); INVOKE_PASS(TypePropagation); -#if defined(DART_PRECOMPILER) - if (mode == kAOT) { - // The extra call specialization pass in AOT is able to specialize more - // calls after ConstantPropagation, which removes unreachable code, and - // TypePropagation, which can infer more accurate types after removing - // unreachable code. - INVOKE_PASS(ApplyICData); - } - if (mode == kAOT) { - INVOKE_PASS(OptimizeTypedDataAccesses); - } -#endif + // The extra call specialization pass in AOT is able to specialize more + // calls after ConstantPropagation, which removes unreachable code, and + // TypePropagation, which can infer more accurate types after removing + // unreachable code. + INVOKE_PASS_AOT(ApplyICData); + INVOKE_PASS_AOT(OptimizeTypedDataAccesses); INVOKE_PASS(WidenSmiToInt32); INVOKE_PASS(SelectRepresentations); INVOKE_PASS(CSE); @@ -345,6 +339,7 @@ FlowGraph* CompilerPass::RunPipeline(PipelineMode mode, // so it should not be lifted earlier than that pass. INVOKE_PASS(DCE); INVOKE_PASS(Canonicalize); + INVOKE_PASS_AOT(DelayAllocations); // Repeat branches optimization after DCE, as it could make more // empty blocks. INVOKE_PASS(OptimizeBranches); @@ -360,13 +355,9 @@ FlowGraph* CompilerPass::RunPipeline(PipelineMode mode, INVOKE_PASS(AllocationSinking_DetachMaterializations); INVOKE_PASS(EliminateWriteBarriers); INVOKE_PASS(FinalizeGraph); -#if defined(DART_PRECOMPILER) - if (mode == kAOT) { - // If we are serializing the flow graph, do it now before we start - // doing register allocation. - INVOKE_PASS(SerializeGraph); - } -#endif + // If we are serializing the flow graph, do it now before we start + // doing register allocation. + INVOKE_PASS_AOT(SerializeGraph); if (FLAG_late_round_trip_serialization) { INVOKE_PASS(RoundTripSerialization); } @@ -498,6 +489,8 @@ COMPILER_PASS(EliminateDeadPhis, COMPILER_PASS(DCE, { DeadCodeElimination::EliminateDeadCode(flow_graph); }); +COMPILER_PASS(DelayAllocations, { DelayAllocations::Optimize(flow_graph); }); + COMPILER_PASS(AllocationSinking_Sink, { // TODO(vegorov): Support allocation sinking with try-catch. if (flow_graph->graph_entry()->catch_entries().is_empty()) { diff --git a/runtime/vm/compiler/compiler_pass.h b/runtime/vm/compiler/compiler_pass.h index ffda7a6b0b7..2897609a7da 100644 --- a/runtime/vm/compiler/compiler_pass.h +++ b/runtime/vm/compiler/compiler_pass.h @@ -30,6 +30,7 @@ namespace dart { V(ComputeSSA) \ V(ConstantPropagation) \ V(DCE) \ + V(DelayAllocations) \ V(DSE) \ V(EliminateDeadPhis) \ V(EliminateEnvironments) \ diff --git a/runtime/vm/hash_map.h b/runtime/vm/hash_map.h index 0ac5463f419..409ab896f9e 100644 --- a/runtime/vm/hash_map.h +++ b/runtime/vm/hash_map.h @@ -574,6 +574,25 @@ class IntMap : public DirectChainedHashMap > { DISALLOW_COPY_AND_ASSIGN(IntMap); }; +template +class IdentitySetKeyValueTrait { + public: + // Typedefs needed for the DirectChainedHashMap template. + typedef V Key; + typedef V Value; + typedef V Pair; + + static Key KeyOf(Pair kv) { return kv; } + + static Value ValueOf(Pair kv) { return kv; } + + static inline intptr_t Hashcode(Key key) { + return reinterpret_cast(key); + } + + static inline bool IsKeyEqual(Pair pair, Key key) { return pair == key; } +}; + } // namespace dart #endif // RUNTIME_VM_HASH_MAP_H_