From 00edd9756b98a1e059f7dc5b7da64264141b0fa2 Mon Sep 17 00:00:00 2001 From: Aske Simon Christensen Date: Thu, 30 Jul 2020 15:57:33 +0000 Subject: [PATCH] [vm/aot] Delay allocation instructions until right before first use. Moves AllocateObject and CreateArray instructions down to their dominant use (use that dominates all other uses) when such a use exists and the move is not hampered by environment uses (which can happen when the allocation is inside a try block). This improves write barrier elimination for inlined constructors, since it moves the allocation after evaluation of the arguments. Any Dart calls in an argument would disable elimination after it. The optimization is particularly effective for Flutter Widget code, since such code typically contains many nested constructor calls. Reduces instructions size of Flutter Gallery by about 0.8%. Change-Id: Ife30850c1a23f0986f85d42c1015f4caa7cf1fa6 Cq-Do-Not-Cancel-Tryjobs: true Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/153602 Commit-Queue: Aske Simon Christensen Reviewed-by: Martin Kustermann --- .../backend/redundancy_elimination.cc | 136 ++++++++++++++++++ .../compiler/backend/redundancy_elimination.h | 10 ++ .../backend/redundancy_elimination_test.cc | 101 +++++++++++++ runtime/vm/compiler/compiler_pass.cc | 59 ++++---- runtime/vm/compiler/compiler_pass.h | 1 + runtime/vm/hash_map.h | 19 +++ 6 files changed, 293 insertions(+), 33 deletions(-) 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_