[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 <askesc@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
This commit is contained in:
Aske Simon Christensen
2020-07-30 15:57:33 +00:00
committed by commit-bot@chromium.org
parent 7831716efd
commit 00edd9756b
6 changed files with 293 additions and 33 deletions
@@ -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<IdentitySetKeyValueTrait<Instruction*>> 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<IdentitySetKeyValueTrait<BlockEntryInstr*>> 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<IdentitySetKeyValueTrait<Instruction*>> 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<IdentitySetKeyValueTrait<BlockEntryInstr*>> seen;
GrowableArray<BlockEntryInstr*> 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)
@@ -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.
@@ -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
+26 -33
View File
@@ -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()) {
+1
View File
@@ -30,6 +30,7 @@ namespace dart {
V(ComputeSSA) \
V(ConstantPropagation) \
V(DCE) \
V(DelayAllocations) \
V(DSE) \
V(EliminateDeadPhis) \
V(EliminateEnvironments) \
+19
View File
@@ -574,6 +574,25 @@ class IntMap : public DirectChainedHashMap<IntKeyRawPointerValueTrait<V> > {
DISALLOW_COPY_AND_ASSIGN(IntMap);
};
template <typename V>
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<intptr_t>(key);
}
static inline bool IsKeyEqual(Pair pair, Key key) { return pair == key; }
};
} // namespace dart
#endif // RUNTIME_VM_HASH_MAP_H_