| // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| #include "vm/compiler/backend/redundancy_elimination.h" |
| |
| #include <utility> |
| |
| #include "vm/bit_vector.h" |
| #include "vm/class_id.h" |
| #include "vm/compiler/backend/flow_graph.h" |
| #include "vm/compiler/backend/il.h" |
| #include "vm/compiler/backend/il_printer.h" |
| #include "vm/compiler/backend/loops.h" |
| #include "vm/hash_map.h" |
| #include "vm/object_store.h" |
| |
| namespace dart { |
| |
| DEFINE_FLAG(bool, dead_store_elimination, true, "Eliminate dead stores"); |
| DEFINE_FLAG(bool, load_cse, true, "Use redundant load elimination."); |
| DEFINE_FLAG(bool, |
| optimize_lazy_initializer_calls, |
| true, |
| "Eliminate redundant lazy initializer calls."); |
| DEFINE_FLAG(bool, |
| trace_load_optimization, |
| false, |
| "Print live sets for load optimization pass."); |
| |
| // Quick access to the current zone. |
| #define Z (zone()) |
| |
| // A set of Instructions used by CSE pass. |
| // |
| // Instructions are compared as if all redefinitions were removed from the |
| // graph, with the exception of LoadField instruction which gets special |
| // treatment. |
| class CSEInstructionSet : public ValueObject { |
| public: |
| CSEInstructionSet() : map_() {} |
| explicit CSEInstructionSet(const CSEInstructionSet& other) |
| : ValueObject(), map_(other.map_) {} |
| |
| Instruction* Lookup(Instruction* other) const { |
| ASSERT(other->AllowsCSE()); |
| return map_.LookupValue(other); |
| } |
| |
| void Insert(Instruction* instr) { |
| ASSERT(instr->AllowsCSE()); |
| return map_.Insert(instr); |
| } |
| |
| private: |
| static Definition* OriginalDefinition(Value* value) { |
| return value->definition()->OriginalDefinition(); |
| } |
| |
| static bool EqualsIgnoringRedefinitions(const Instruction& a, |
| const Instruction& b) { |
| const auto tag = a.tag(); |
| if (tag != b.tag()) return false; |
| const auto input_count = a.InputCount(); |
| if (input_count != b.InputCount()) return false; |
| |
| // We would like to avoid replacing a load from a redefinition with a |
| // load from an original definition because that breaks the dependency |
| // on the redefinition and enables potentially incorrect code motion. |
| if (tag != Instruction::kLoadField) { |
| for (intptr_t i = 0; i < input_count; ++i) { |
| if (OriginalDefinition(a.InputAt(i)) != |
| OriginalDefinition(b.InputAt(i))) { |
| return false; |
| } |
| } |
| } else { |
| for (intptr_t i = 0; i < input_count; ++i) { |
| if (!a.InputAt(i)->Equals(*b.InputAt(i))) return false; |
| } |
| } |
| return a.AttributesEqual(b); |
| } |
| |
| class Trait { |
| public: |
| typedef Instruction* Value; |
| typedef Instruction* Key; |
| typedef Instruction* Pair; |
| |
| static Key KeyOf(Pair kv) { return kv; } |
| static Value ValueOf(Pair kv) { return kv; } |
| |
| static inline uword Hash(Key key) { |
| uword result = key->tag(); |
| for (intptr_t i = 0; i < key->InputCount(); ++i) { |
| result = CombineHashes( |
| result, OriginalDefinition(key->InputAt(i))->ssa_temp_index()); |
| } |
| return FinalizeHash(result, kBitsPerInt32 - 1); |
| } |
| |
| static inline bool IsKeyEqual(Pair kv, Key key) { |
| return EqualsIgnoringRedefinitions(*kv, *key); |
| } |
| }; |
| |
| DirectChainedHashMap<Trait> map_; |
| }; |
| |
| // Place describes an abstract location (e.g. field) that IR can load |
| // from or store to. |
| // |
| // Places are also used to describe wild-card locations also known as aliases, |
| // that essentially represent sets of places that alias each other. Places A |
| // and B are said to alias each other if store into A can affect load from B. |
| // |
| // We distinguish the following aliases: |
| // |
| // - for fields |
| // - *.f - field inside some object; |
| // - X.f - field inside an allocated object X; |
| // - f - static fields |
| // |
| // - for indexed accesses |
| // - *[*] - non-constant index inside some object; |
| // - *[C] - constant index inside some object; |
| // - X[*] - non-constant index inside an allocated object X; |
| // - X[C] - constant index inside an allocated object X. |
| // |
| // Constant indexed places are divided into two subcategories: |
| // |
| // - Access to homogeneous array-like objects: Array, ImmutableArray, |
| // OneByteString, TwoByteString. These objects can only be accessed |
| // on element by element basis with all elements having the same size. |
| // This means X[C] aliases X[K] if and only if C === K. |
| // - TypedData accesses. TypedData allow to read one of the primitive |
| // data types at the given byte offset. When TypedData is accessed through |
| // index operator on a typed array or a typed array view it is guaranteed |
| // that the byte offset is always aligned by the element size. We write |
| // these accesses as X[C|S], where C is constant byte offset and S is size |
| // of the data type. Obviously X[C|S] and X[K|U] alias if and only if either |
| // C = RoundDown(K, S) or K = RoundDown(C, U). |
| // Note that not all accesses to typed data are aligned: e.g. ByteData |
| // allows unaligned access through it's get*/set* methods. |
| // Check in Place::SetIndex ensures that we never create a place X[C|S] |
| // such that C is not aligned by S. |
| // |
| // Separating allocations from other objects improves precision of the |
| // load forwarding pass because of the following two properties: |
| // |
| // - if X can be proven to have no aliases itself (i.e. there is no other SSA |
| // variable that points to X) then no place inside X can be aliased with any |
| // wildcard dependent place (*.f, *.@offs, *[*], *[C]); |
| // - given allocations X and Y no place inside X can be aliased with any place |
| // inside Y even if any of them or both escape. |
| // |
| // It is important to realize that single place can belong to multiple aliases. |
| // For example place X.f with aliased allocation X belongs both to X.f and *.f |
| // aliases. Likewise X[C] with non-aliased allocation X belongs to X[C] and X[*] |
| // aliases. |
| // |
| class Place : public ValueObject { |
| public: |
| enum Kind { |
| kNone, |
| |
| // Static field location. Is represented as a Field object with a |
| // nullptr instance. |
| kStaticField, |
| |
| // Instance field location. It is represented by a pair of instance |
| // and a Slot. |
| kInstanceField, |
| |
| // Indexed location with a non-constant index. |
| kIndexed, |
| |
| // Indexed location with a constant index. |
| kConstantIndexed, |
| }; |
| |
| // Size of the element accessed by constant index. Size is only important |
| // for TypedData because those accesses can alias even when constant indexes |
| // are not the same: X[0|4] aliases X[0|2] and X[2|2]. |
| enum ElementSize { |
| // If indexed access is not a TypedData access then element size is not |
| // important because there is only a single possible access size depending |
| // on the receiver - X[C] aliases X[K] if and only if C == K. |
| // This is the size set for Array, ImmutableArray, OneByteString and |
| // TwoByteString accesses. |
| kNoSize, |
| |
| // 1 byte (Int8List, Uint8List, Uint8ClampedList). |
| k1Byte, |
| |
| // 2 bytes (Int16List, Uint16List). |
| k2Bytes, |
| |
| // 4 bytes (Int32List, Uint32List, Float32List). |
| k4Bytes, |
| |
| // 8 bytes (Int64List, Uint64List, Float64List). |
| k8Bytes, |
| |
| // 16 bytes (Int32x4List, Float32x4List, Float64x2List). |
| k16Bytes, |
| |
| kLargestElementSize = k16Bytes, |
| }; |
| |
| Place(const Place& other) |
| : ValueObject(), |
| flags_(other.flags_), |
| instance_(other.instance_), |
| raw_selector_(other.raw_selector_), |
| id_(other.id_) {} |
| |
| // Construct a place from instruction if instruction accesses any place. |
| // Otherwise constructs kNone place. |
| Place(Instruction* instr, bool* is_load, bool* is_store) |
| : flags_(0), instance_(nullptr), raw_selector_(0), id_(0) { |
| switch (instr->tag()) { |
| case Instruction::kLoadField: { |
| LoadFieldInstr* load_field = instr->AsLoadField(); |
| set_representation(load_field->representation()); |
| instance_ = load_field->instance()->definition()->OriginalDefinition(); |
| set_kind(kInstanceField); |
| instance_field_ = &load_field->slot(); |
| *is_load = true; |
| break; |
| } |
| |
| case Instruction::kStoreField: { |
| StoreFieldInstr* store = instr->AsStoreField(); |
| set_representation( |
| store->RequiredInputRepresentation(StoreFieldInstr::kValuePos)); |
| instance_ = store->instance()->definition()->OriginalDefinition(); |
| set_kind(kInstanceField); |
| instance_field_ = &store->slot(); |
| *is_store = true; |
| break; |
| } |
| |
| case Instruction::kLoadStaticField: |
| set_kind(kStaticField); |
| set_representation(instr->AsLoadStaticField()->representation()); |
| static_field_ = &instr->AsLoadStaticField()->field(); |
| *is_load = true; |
| break; |
| |
| case Instruction::kStoreStaticField: |
| set_kind(kStaticField); |
| set_representation( |
| instr->AsStoreStaticField()->RequiredInputRepresentation( |
| StoreStaticFieldInstr::kValuePos)); |
| static_field_ = &instr->AsStoreStaticField()->field(); |
| *is_store = true; |
| break; |
| |
| case Instruction::kLoadIndexed: { |
| LoadIndexedInstr* load_indexed = instr->AsLoadIndexed(); |
| // Since the same returned representation is used for arrays with small |
| // elements, use the array element representation instead. |
| // |
| // For example, loads from signed and unsigned byte views of the same |
| // array share the same place if the returned representation is used, |
| // which means a load which sign extends the byte to the native word |
| // size might be replaced with an load that zero extends the byte |
| // instead and vice versa. |
| set_representation(RepresentationUtils::RepresentationOfArrayElement( |
| load_indexed->class_id())); |
| instance_ = load_indexed->array()->definition()->OriginalDefinition(); |
| SetIndex(load_indexed->index()->definition()->OriginalDefinition(), |
| load_indexed->index_scale(), load_indexed->class_id()); |
| *is_load = true; |
| break; |
| } |
| |
| case Instruction::kStoreIndexed: { |
| StoreIndexedInstr* store_indexed = instr->AsStoreIndexed(); |
| // Use the array element representation instead of the value |
| // representation for the same reasons as for LoadIndexed above. |
| set_representation(RepresentationUtils::RepresentationOfArrayElement( |
| store_indexed->class_id())); |
| instance_ = store_indexed->array()->definition()->OriginalDefinition(); |
| SetIndex(store_indexed->index()->definition()->OriginalDefinition(), |
| store_indexed->index_scale(), store_indexed->class_id()); |
| *is_store = true; |
| break; |
| } |
| |
| default: |
| break; |
| } |
| } |
| |
| // Construct a place from an allocation where the place represents a store to |
| // a slot that corresponds to the given input position. |
| // Otherwise, constructs a kNone place. |
| Place(AllocationInstr* alloc, intptr_t input_pos) |
| : flags_(0), instance_(nullptr), raw_selector_(0), id_(0) { |
| if (const Slot* slot = alloc->SlotForInput(input_pos)) { |
| set_representation(alloc->RequiredInputRepresentation(input_pos)); |
| instance_ = alloc; |
| set_kind(kInstanceField); |
| instance_field_ = slot; |
| } |
| } |
| |
| // Create object representing *[*] alias. |
| static Place* CreateAnyInstanceAnyIndexAlias(Zone* zone, intptr_t id) { |
| return Wrap( |
| zone, |
| Place(EncodeFlags(kIndexed, kNoRepresentation, kNoSize), nullptr, 0), |
| id); |
| } |
| |
| // Return least generic alias for this place. Given that aliases are |
| // essentially sets of places we define least generic alias as a smallest |
| // alias that contains this place. |
| // |
| // We obtain such alias by a simple transformation: |
| // |
| // - for places that depend on an instance X.f, X.@offs, X[i], X[C] |
| // we drop X if X is not an allocation because in this case X does not |
| // possess an identity obtaining aliases *.f, *.@offs, *[i] and *[C] |
| // respectively; also drop instance of X[i] for typed data view |
| // allocations as they may alias other indexed accesses. |
| // - for non-constant indexed places X[i] we drop information about the |
| // index obtaining alias X[*]. |
| // - we drop information about representation, but keep element size |
| // if any. |
| // |
| Place ToAlias() const { |
| Definition* alias_instance = nullptr; |
| if (DependsOnInstance() && IsAllocation(instance()) && |
| ((kind() != kIndexed) || !IsTypedDataViewAllocation(instance()))) { |
| alias_instance = instance(); |
| } |
| return Place(RepresentationBits::update(kNoRepresentation, flags_), |
| alias_instance, (kind() == kIndexed) ? 0 : raw_selector_); |
| } |
| |
| bool DependsOnInstance() const { |
| switch (kind()) { |
| case kInstanceField: |
| case kIndexed: |
| case kConstantIndexed: |
| return true; |
| |
| case kStaticField: |
| case kNone: |
| return false; |
| } |
| |
| UNREACHABLE(); |
| return false; |
| } |
| |
| // Given instance dependent alias X.f, X.@offs, X[C], X[*] return |
| // wild-card dependent alias *.f, *.@offs, *[C] or *[*] respectively. |
| Place CopyWithoutInstance() const { |
| ASSERT(DependsOnInstance()); |
| return Place(flags_, nullptr, raw_selector_); |
| } |
| |
| // Given alias X[C] or *[C] return X[*] and *[*] respectively. |
| Place CopyWithoutIndex() const { |
| ASSERT(kind() == kConstantIndexed); |
| return Place(EncodeFlags(kIndexed, kNoRepresentation, kNoSize), instance_, |
| 0); |
| } |
| |
| // Given alias X[ByteOffs|S] and a larger element size S', return |
| // alias X[RoundDown(ByteOffs, S')|S'] - this is the byte offset of a larger |
| // typed array element that contains this typed array element. |
| // In other words this method computes the only possible place with the given |
| // size that can alias this place (due to alignment restrictions). |
| // For example for X[9|k1Byte] and target size k4Bytes we would return |
| // X[8|k4Bytes]. |
| Place ToLargerElement(ElementSize to) const { |
| ASSERT(kind() == kConstantIndexed); |
| ASSERT(element_size() != kNoSize); |
| ASSERT(element_size() < to); |
| return Place(ElementSizeBits::update(to, flags_), instance_, |
| RoundByteOffset(to, index_constant_)); |
| } |
| |
| // Given alias X[ByteOffs|S], smaller element size S' and index from 0 to |
| // S/S' - 1 return alias X[ByteOffs + S'*index|S'] - this is the byte offset |
| // of a smaller typed array element which is contained within this typed |
| // array element. |
| // For example X[8|k4Bytes] contains inside X[8|k2Bytes] (index is 0) and |
| // X[10|k2Bytes] (index is 1). |
| Place ToSmallerElement(ElementSize to, intptr_t index) const { |
| ASSERT(kind() == kConstantIndexed); |
| ASSERT(element_size() != kNoSize); |
| ASSERT(element_size() > to); |
| ASSERT(index >= 0); |
| ASSERT(index < |
| ElementSizeMultiplier(element_size()) / ElementSizeMultiplier(to)); |
| return Place(ElementSizeBits::update(to, flags_), instance_, |
| ByteOffsetToSmallerElement(to, index, index_constant_)); |
| } |
| |
| intptr_t id() const { return id_; } |
| |
| Kind kind() const { return KindBits::decode(flags_); } |
| |
| Representation representation() const { |
| return RepresentationBits::decode(flags_); |
| } |
| |
| Definition* instance() const { |
| ASSERT(DependsOnInstance()); |
| return instance_; |
| } |
| |
| void set_instance(Definition* def) { |
| ASSERT(DependsOnInstance()); |
| instance_ = def->OriginalDefinition(); |
| } |
| |
| const Field& static_field() const { |
| ASSERT(kind() == kStaticField); |
| ASSERT(static_field_->is_static()); |
| return *static_field_; |
| } |
| |
| const Slot& instance_field() const { |
| ASSERT(kind() == kInstanceField); |
| return *instance_field_; |
| } |
| |
| Definition* index() const { |
| ASSERT(kind() == kIndexed); |
| return index_; |
| } |
| |
| ElementSize element_size() const { return ElementSizeBits::decode(flags_); } |
| |
| intptr_t index_constant() const { |
| ASSERT(kind() == kConstantIndexed); |
| return index_constant_; |
| } |
| |
| static const char* DefinitionName(Definition* def) { |
| if (def == nullptr) { |
| return "*"; |
| } else { |
| return Thread::Current()->zone()->PrintToString("v%" Pd, |
| def->ssa_temp_index()); |
| } |
| } |
| |
| const char* ToCString() const { |
| switch (kind()) { |
| case kNone: |
| return "<none>"; |
| |
| case kStaticField: { |
| const char* field_name = |
| String::Handle(static_field().name()).ToCString(); |
| return Thread::Current()->zone()->PrintToString("<%s>", field_name); |
| } |
| |
| case kInstanceField: |
| return Thread::Current()->zone()->PrintToString( |
| "<%s.%s[%p]>", DefinitionName(instance()), instance_field().Name(), |
| &instance_field()); |
| |
| case kIndexed: |
| return Thread::Current()->zone()->PrintToString( |
| "<%s[%s]>", DefinitionName(instance()), DefinitionName(index())); |
| |
| case kConstantIndexed: |
| if (element_size() == kNoSize) { |
| return Thread::Current()->zone()->PrintToString( |
| "<%s[%" Pd "]>", DefinitionName(instance()), index_constant()); |
| } else { |
| return Thread::Current()->zone()->PrintToString( |
| "<%s[%" Pd "|%" Pd "]>", DefinitionName(instance()), |
| index_constant(), ElementSizeMultiplier(element_size())); |
| } |
| } |
| UNREACHABLE(); |
| return "<?>"; |
| } |
| |
| // Fields that are considered immutable by load optimization. |
| // Handle static finals as non-final with precompilation because |
| // they may be reset to uninitialized after compilation. |
| bool IsImmutableField() const { |
| switch (kind()) { |
| case kInstanceField: |
| return instance_field().is_immutable(); |
| default: |
| return false; |
| } |
| } |
| |
| uword Hash() const { |
| return FinalizeHash( |
| CombineHashes(flags_, reinterpret_cast<uword>(instance_)), |
| kBitsPerInt32 - 1); |
| } |
| |
| bool Equals(const Place& other) const { |
| return (flags_ == other.flags_) && (instance_ == other.instance_) && |
| SameField(other); |
| } |
| |
| // Create a zone allocated copy of this place and assign given id to it. |
| static Place* Wrap(Zone* zone, const Place& place, intptr_t id); |
| |
| static bool IsAllocation(Definition* defn) { |
| return (defn != nullptr) && (defn->IsAllocation() || |
| (defn->IsStaticCall() && |
| defn->AsStaticCall()->IsRecognizedFactory())); |
| } |
| |
| static bool IsTypedDataViewAllocation(Definition* defn) { |
| if (defn != nullptr) { |
| if (auto* alloc = defn->AsAllocateObject()) { |
| auto const cid = alloc->cls().id(); |
| return IsTypedDataViewClassId(cid) || |
| IsUnmodifiableTypedDataViewClassId(cid); |
| } |
| } |
| return false; |
| } |
| |
| private: |
| Place(uword flags, Definition* instance, intptr_t selector) |
| : flags_(flags), instance_(instance), raw_selector_(selector), id_(0) {} |
| |
| bool SameField(const Place& other) const { |
| return (kind() == kStaticField) |
| ? (static_field().Original() == other.static_field().Original()) |
| : (raw_selector_ == other.raw_selector_); |
| } |
| |
| void set_representation(Representation rep) { |
| flags_ = RepresentationBits::update(rep, flags_); |
| } |
| |
| void set_kind(Kind kind) { flags_ = KindBits::update(kind, flags_); } |
| |
| void set_element_size(ElementSize scale) { |
| flags_ = ElementSizeBits::update(scale, flags_); |
| } |
| |
| void SetIndex(Definition* index, intptr_t scale, classid_t class_id) { |
| ConstantInstr* index_constant = index->AsConstant(); |
| if ((index_constant != nullptr) && index_constant->value().IsSmi()) { |
| const intptr_t index_value = Smi::Cast(index_constant->value()).Value(); |
| |
| // Places only need to scale the index for typed data objects, as other |
| // types of arrays (for which ElementSizeFor returns kNoSize) cannot be |
| // accessed at different scales. |
| const ElementSize size = ElementSizeFor(class_id); |
| if (size == kNoSize) { |
| set_kind(kConstantIndexed); |
| set_element_size(size); |
| index_constant_ = index_value; |
| return; |
| } |
| |
| // Guard against potential multiplication overflow and negative indices. |
| if ((0 <= index_value) && (index_value < (kMaxInt32 / scale))) { |
| const intptr_t scaled_index = index_value * scale; |
| |
| // If the indexed array is a subclass of PointerBase, then it must be |
| // treated as a view unless the class id of the array is known at |
| // compile time to be an internal typed data object. |
| // |
| // Indexes into untagged pointers only happen for external typed data |
| // objects or dart:ffi's Pointer, but those should also be treated like |
| // views, since anyone who has access to the underlying pointer can |
| // modify the corresponding memory. |
| auto const cid = instance_->Type()->ToCid(); |
| const bool can_be_view = instance_->representation() == kUntagged || |
| !IsTypedDataClassId(cid); |
| |
| // Guard against unaligned byte offsets and access through raw |
| // memory pointer (which can be pointing into another typed data). |
| if (!can_be_view && |
| Utils::IsAligned(scaled_index, ElementSizeMultiplier(size))) { |
| set_kind(kConstantIndexed); |
| set_element_size(size); |
| index_constant_ = scaled_index; |
| return; |
| } |
| } |
| |
| // Fallthrough: create generic _[*] place. |
| } |
| |
| set_kind(kIndexed); |
| index_ = index; |
| } |
| |
| static uword EncodeFlags(Kind kind, Representation rep, ElementSize scale) { |
| ASSERT((kind == kConstantIndexed) || (scale == kNoSize)); |
| return KindBits::encode(kind) | RepresentationBits::encode(rep) | |
| ElementSizeBits::encode(scale); |
| } |
| |
| static ElementSize ElementSizeFor(classid_t class_id) { |
| // Object arrays and strings do not allow accessing them through |
| // different types. No need to attach scale. |
| if (!IsTypedDataBaseClassId(class_id)) return kNoSize; |
| |
| const auto rep = |
| RepresentationUtils::RepresentationOfArrayElement(class_id); |
| if (!RepresentationUtils::IsUnboxed(rep)) return kNoSize; |
| switch (RepresentationUtils::ValueSize(rep)) { |
| case 1: |
| return k1Byte; |
| case 2: |
| return k2Bytes; |
| case 4: |
| return k4Bytes; |
| case 8: |
| return k8Bytes; |
| case 16: |
| return k16Bytes; |
| default: |
| FATAL("Unhandled value size for representation %s", |
| RepresentationUtils::ToCString(rep)); |
| return kNoSize; |
| } |
| } |
| |
| static constexpr intptr_t ElementSizeMultiplier(ElementSize size) { |
| return 1 << (static_cast<intptr_t>(size) - static_cast<intptr_t>(k1Byte)); |
| } |
| |
| static intptr_t RoundByteOffset(ElementSize size, intptr_t offset) { |
| return offset & ~(ElementSizeMultiplier(size) - 1); |
| } |
| |
| static intptr_t ByteOffsetToSmallerElement(ElementSize size, |
| intptr_t index, |
| intptr_t base_offset) { |
| return base_offset + index * ElementSizeMultiplier(size); |
| } |
| |
| class KindBits : public BitField<uword, Kind, 0, 3> {}; |
| class RepresentationBits |
| : public BitField<uword, Representation, KindBits::kNextBit, 11> {}; |
| |
| static constexpr int kNumElementSizeBits = Utils::ShiftForPowerOfTwo( |
| Utils::RoundUpToPowerOfTwo(kLargestElementSize)); |
| class ElementSizeBits : public BitField<uword, |
| ElementSize, |
| RepresentationBits::kNextBit, |
| kNumElementSizeBits> {}; |
| |
| uword flags_; |
| Definition* instance_; |
| union { |
| intptr_t raw_selector_; |
| const Field* static_field_; |
| const Slot* instance_field_; |
| intptr_t index_constant_; |
| Definition* index_; |
| }; |
| |
| intptr_t id_; |
| }; |
| |
| class ZonePlace : public ZoneAllocated { |
| public: |
| explicit ZonePlace(const Place& place) : place_(place) {} |
| |
| Place* place() { return &place_; } |
| |
| private: |
| Place place_; |
| }; |
| |
| Place* Place::Wrap(Zone* zone, const Place& place, intptr_t id) { |
| Place* wrapped = (new (zone) ZonePlace(place))->place(); |
| wrapped->id_ = id; |
| return wrapped; |
| } |
| |
| // Correspondence between places connected through outgoing phi moves on the |
| // edge that targets join. |
| class PhiPlaceMoves : public ZoneAllocated { |
| public: |
| // Record a move from the place with id |from| to the place with id |to| at |
| // the given block. |
| void CreateOutgoingMove(Zone* zone, |
| BlockEntryInstr* block, |
| intptr_t from, |
| intptr_t to) { |
| const intptr_t block_num = block->preorder_number(); |
| moves_.EnsureLength(block_num + 1, nullptr); |
| |
| if (moves_[block_num] == nullptr) { |
| moves_[block_num] = new (zone) ZoneGrowableArray<Move>(5); |
| } |
| |
| moves_[block_num]->Add(Move(from, to)); |
| } |
| |
| class Move { |
| public: |
| Move(intptr_t from, intptr_t to) : from_(from), to_(to) {} |
| |
| intptr_t from() const { return from_; } |
| intptr_t to() const { return to_; } |
| |
| private: |
| intptr_t from_; |
| intptr_t to_; |
| }; |
| |
| typedef const ZoneGrowableArray<Move>* MovesList; |
| |
| MovesList GetOutgoingMoves(BlockEntryInstr* block) const { |
| const intptr_t block_num = block->preorder_number(); |
| return (block_num < moves_.length()) ? moves_[block_num] : nullptr; |
| } |
| |
| private: |
| GrowableArray<ZoneGrowableArray<Move>*> moves_; |
| }; |
| |
| // A map from aliases to a set of places sharing the alias. Additionally |
| // carries a set of places that can be aliased by side-effects, essentially |
| // those that are affected by calls. |
| class AliasedSet : public ZoneAllocated { |
| public: |
| AliasedSet(Zone* zone, |
| PointerSet<Place>* places_map, |
| ZoneGrowableArray<Place*>* places, |
| PhiPlaceMoves* phi_moves, |
| bool print_traces) |
| : zone_(zone), |
| print_traces_(print_traces), |
| places_map_(places_map), |
| places_(*places), |
| phi_moves_(phi_moves), |
| aliases_(5), |
| aliases_map_(), |
| typed_data_access_sizes_(), |
| representatives_(), |
| killed_(), |
| aliased_by_effects_(new(zone) BitVector(zone, places->length())) { |
| InsertAlias(Place::CreateAnyInstanceAnyIndexAlias( |
| zone_, kAnyInstanceAnyIndexAlias)); |
| for (intptr_t i = 0; i < places_.length(); i++) { |
| AddRepresentative(places_[i]); |
| } |
| ComputeKillSets(); |
| } |
| |
| intptr_t LookupAliasId(const Place& alias) { |
| const Place* result = aliases_map_.LookupValue(&alias); |
| return (result != nullptr) ? result->id() : static_cast<intptr_t>(kNoAlias); |
| } |
| |
| BitVector* GetKilledSet(intptr_t alias) { |
| return (alias < killed_.length()) ? killed_[alias] : nullptr; |
| } |
| |
| intptr_t max_place_id() const { return places().length(); } |
| bool IsEmpty() const { return max_place_id() == 0; } |
| |
| BitVector* aliased_by_effects() const { return aliased_by_effects_; } |
| |
| const ZoneGrowableArray<Place*>& places() const { return places_; } |
| |
| Place* LookupCanonical(Place* place) const { |
| return places_map_->LookupValue(place); |
| } |
| |
| void PrintSet(BitVector* set) { |
| bool comma = false; |
| for (BitVector::Iterator it(set); !it.Done(); it.Advance()) { |
| if (comma) { |
| THR_Print(", "); |
| } |
| THR_Print("%s", places_[it.Current()]->ToCString()); |
| comma = true; |
| } |
| } |
| |
| const PhiPlaceMoves* phi_moves() const { return phi_moves_; } |
| |
| void RollbackAliasedIdentities() { |
| for (intptr_t i = 0; i < identity_rollback_.length(); ++i) { |
| identity_rollback_[i]->SetIdentity(AliasIdentity::Unknown()); |
| } |
| } |
| |
| // Returns false if the result of an allocation instruction can't be aliased |
| // by another SSA variable and true otherwise. |
| bool CanBeAliased(Definition* alloc) { |
| if (!Place::IsAllocation(alloc)) { |
| return true; |
| } |
| |
| if (alloc->Identity().IsUnknown()) { |
| ComputeAliasing(alloc); |
| } |
| |
| return !alloc->Identity().IsNotAliased(); |
| } |
| |
| enum { kNoAlias = 0 }; |
| |
| private: |
| enum { |
| // Artificial alias that is used to collect all representatives of the |
| // *[C], X[C] aliases for arbitrary C. |
| kAnyConstantIndexedAlias = 1, |
| |
| // Artificial alias that is used to collect all representatives of |
| // *[C] alias for arbitrary C. |
| kUnknownInstanceConstantIndexedAlias = 2, |
| |
| // Artificial alias that is used to collect all representatives of |
| // X[*] alias for all X. |
| kAnyAllocationIndexedAlias = 3, |
| |
| // *[*] alias. |
| kAnyInstanceAnyIndexAlias = 4 |
| }; |
| |
| // Compute least generic alias for the place and assign alias id to it. |
| void AddRepresentative(Place* place) { |
| if (!place->IsImmutableField()) { |
| const Place* alias = CanonicalizeAlias(place->ToAlias()); |
| EnsureSet(&representatives_, alias->id())->Add(place->id()); |
| |
| // Update cumulative representative sets that are used during |
| // killed sets computation. |
| if (alias->kind() == Place::kConstantIndexed) { |
| if (CanBeAliased(alias->instance())) { |
| EnsureSet(&representatives_, kAnyConstantIndexedAlias) |
| ->Add(place->id()); |
| } |
| |
| if (alias->instance() == nullptr) { |
| EnsureSet(&representatives_, kUnknownInstanceConstantIndexedAlias) |
| ->Add(place->id()); |
| } |
| |
| // Collect all element sizes used to access TypedData arrays in |
| // the function. This is used to skip sizes without representatives |
| // when computing kill sets. |
| if (alias->element_size() != Place::kNoSize) { |
| typed_data_access_sizes_.Add(alias->element_size()); |
| } |
| } else if ((alias->kind() == Place::kIndexed) && |
| CanBeAliased(place->instance())) { |
| EnsureSet(&representatives_, kAnyAllocationIndexedAlias) |
| ->Add(place->id()); |
| } |
| |
| if (!IsIndependentFromEffects(place)) { |
| aliased_by_effects_->Add(place->id()); |
| } |
| } |
| } |
| |
| void ComputeKillSets() { |
| for (intptr_t i = 0; i < aliases_.length(); ++i) { |
| const Place* alias = aliases_[i]; |
| // Add all representatives to the kill set. |
| AddAllRepresentatives(alias->id(), alias->id()); |
| ComputeKillSet(alias); |
| } |
| |
| if (FLAG_trace_load_optimization && print_traces_) { |
| THR_Print("Aliases KILL sets:\n"); |
| for (intptr_t i = 0; i < aliases_.length(); ++i) { |
| const Place* alias = aliases_[i]; |
| BitVector* kill = GetKilledSet(alias->id()); |
| |
| THR_Print("%s: ", alias->ToCString()); |
| if (kill != nullptr) { |
| PrintSet(kill); |
| } |
| THR_Print("\n"); |
| } |
| } |
| } |
| |
| void InsertAlias(const Place* alias) { |
| aliases_map_.Insert(alias); |
| aliases_.Add(alias); |
| } |
| |
| const Place* CanonicalizeAlias(const Place& alias) { |
| const Place* canonical = aliases_map_.LookupValue(&alias); |
| if (canonical == nullptr) { |
| canonical = Place::Wrap(zone_, alias, |
| kAnyInstanceAnyIndexAlias + aliases_.length()); |
| InsertAlias(canonical); |
| } |
| ASSERT(aliases_map_.LookupValue(&alias) == canonical); |
| return canonical; |
| } |
| |
| BitVector* GetRepresentativesSet(intptr_t alias) { |
| return (alias < representatives_.length()) ? representatives_[alias] |
| : nullptr; |
| } |
| |
| BitVector* EnsureSet(GrowableArray<BitVector*>* sets, intptr_t alias) { |
| while (sets->length() <= alias) { |
| sets->Add(nullptr); |
| } |
| |
| BitVector* set = (*sets)[alias]; |
| if (set == nullptr) { |
| (*sets)[alias] = set = new (zone_) BitVector(zone_, max_place_id()); |
| } |
| return set; |
| } |
| |
| void AddAllRepresentatives(const Place* to, intptr_t from) { |
| AddAllRepresentatives(to->id(), from); |
| } |
| |
| void AddAllRepresentatives(intptr_t to, intptr_t from) { |
| BitVector* from_set = GetRepresentativesSet(from); |
| if (from_set != nullptr) { |
| EnsureSet(&killed_, to)->AddAll(from_set); |
| } |
| } |
| |
| void CrossAlias(const Place* to, const Place& from) { |
| const intptr_t from_id = LookupAliasId(from); |
| if (from_id == kNoAlias) { |
| return; |
| } |
| CrossAlias(to, from_id); |
| } |
| |
| void CrossAlias(const Place* to, intptr_t from) { |
| AddAllRepresentatives(to->id(), from); |
| AddAllRepresentatives(from, to->id()); |
| } |
| |
| // When computing kill sets we let less generic alias insert its |
| // representatives into more generic aliases kill set. For example |
| // when visiting alias X[*] instead of searching for all aliases X[C] |
| // and inserting their representatives into kill set for X[*] we update |
| // kill set for X[*] each time we visit new X[C] for some C. |
| // There is an exception however: if both aliases are parametric like *[C] |
| // and X[*] which cross alias when X is an aliased allocation then we use |
| // artificial aliases that contain all possible representatives for the given |
| // alias for any value of the parameter to compute resulting kill set. |
| void ComputeKillSet(const Place* alias) { |
| switch (alias->kind()) { |
| case Place::kIndexed: // Either *[*] or X[*] alias. |
| if (alias->instance() == nullptr) { |
| // *[*] aliases with X[*], X[C], *[C]. |
| AddAllRepresentatives(alias, kAnyConstantIndexedAlias); |
| AddAllRepresentatives(alias, kAnyAllocationIndexedAlias); |
| } else if (CanBeAliased(alias->instance())) { |
| // X[*] aliases with X[C]. |
| // If X can be aliased then X[*] also aliases with *[C], *[*]. |
| CrossAlias(alias, kAnyInstanceAnyIndexAlias); |
| AddAllRepresentatives(alias, kUnknownInstanceConstantIndexedAlias); |
| } |
| break; |
| |
| case Place::kConstantIndexed: // Either X[C] or *[C] alias. |
| if (alias->element_size() != Place::kNoSize) { |
| const bool has_aliased_instance = |
| (alias->instance() != nullptr) && CanBeAliased(alias->instance()); |
| |
| // If this is a TypedData access then X[C|S] aliases larger elements |
| // covering this one X[RoundDown(C, S')|S'] for all S' > S and |
| // all smaller elements being covered by this one X[C'|S'] for |
| // some S' < S and all C' such that C = RoundDown(C', S). |
| // In the loop below it's enough to only propagate aliasing to |
| // larger aliases because propagation is symmetric: smaller aliases |
| // (if there are any) would update kill set for this alias when they |
| // are visited. |
| for (intptr_t i = static_cast<intptr_t>(alias->element_size()) + 1; |
| i <= Place::kLargestElementSize; i++) { |
| // Skip element sizes that a guaranteed to have no representatives. |
| if (!typed_data_access_sizes_.Contains(alias->element_size())) { |
| continue; |
| } |
| |
| // X[C|S] aliases with X[RoundDown(C, S')|S'] and likewise |
| // *[C|S] aliases with *[RoundDown(C, S')|S']. |
| CrossAlias(alias, alias->ToLargerElement( |
| static_cast<Place::ElementSize>(i))); |
| } |
| |
| if (has_aliased_instance) { |
| // If X is an aliased instance then X[C|S] aliases *[C'|S'] for all |
| // related combinations of C' and S'. |
| // Caveat: this propagation is not symmetric (we would not know |
| // to propagate aliasing from *[C'|S'] to X[C|S] when visiting |
| // *[C'|S']) and thus we need to handle both element sizes smaller |
| // and larger than S. |
| const Place no_instance_alias = alias->CopyWithoutInstance(); |
| for (intptr_t i = Place::k1Byte; i <= Place::kLargestElementSize; |
| i++) { |
| // Skip element sizes that a guaranteed to have no |
| // representatives. |
| if (!typed_data_access_sizes_.Contains(alias->element_size())) { |
| continue; |
| } |
| |
| const auto other_size = static_cast<Place::ElementSize>(i); |
| if (other_size > alias->element_size()) { |
| // X[C|S] aliases all larger elements which cover it: |
| // *[RoundDown(C, S')|S'] for S' > S. |
| CrossAlias(alias, |
| no_instance_alias.ToLargerElement(other_size)); |
| } else if (other_size < alias->element_size()) { |
| // X[C|S] aliases all sub-elements of smaller size: |
| // *[C+j*S'|S'] for S' < S and j from 0 to S/S' - 1. |
| const auto num_smaller_elements = |
| 1 << (alias->element_size() - other_size); |
| for (intptr_t j = 0; j < num_smaller_elements; j++) { |
| CrossAlias(alias, |
| no_instance_alias.ToSmallerElement(other_size, j)); |
| } |
| } |
| } |
| } |
| } |
| |
| if (alias->instance() == nullptr) { |
| // *[C] aliases with X[C], X[*], *[*]. |
| AddAllRepresentatives(alias, kAnyAllocationIndexedAlias); |
| CrossAlias(alias, kAnyInstanceAnyIndexAlias); |
| } else { |
| // X[C] aliases with X[*]. |
| // If X can be aliased then X[C] also aliases with *[C], *[*]. |
| CrossAlias(alias, alias->CopyWithoutIndex()); |
| if (CanBeAliased(alias->instance())) { |
| CrossAlias(alias, alias->CopyWithoutInstance()); |
| CrossAlias(alias, kAnyInstanceAnyIndexAlias); |
| } |
| } |
| break; |
| |
| case Place::kStaticField: |
| // Nothing to do. |
| break; |
| |
| case Place::kInstanceField: |
| if (CanBeAliased(alias->instance())) { |
| // X.f alias with *.f. |
| CrossAlias(alias, alias->CopyWithoutInstance()); |
| } |
| break; |
| |
| case Place::kNone: |
| UNREACHABLE(); |
| } |
| } |
| |
| // Returns true if the given load is unaffected by external side-effects. |
| // This essentially means that no stores to the same location can |
| // occur in other functions. |
| bool IsIndependentFromEffects(Place* place) { |
| if (place->IsImmutableField()) { |
| return true; |
| } |
| |
| return ((place->kind() == Place::kInstanceField) || |
| (place->kind() == Place::kConstantIndexed)) && |
| (place->instance() != nullptr) && !CanBeAliased(place->instance()); |
| } |
| |
| // Returns true if there are direct loads from the given place. |
| bool HasLoadsFromPlace(Definition* defn, const Place* place) { |
| ASSERT(place->kind() == Place::kInstanceField); |
| |
| for (Value* use = defn->input_use_list(); use != nullptr; |
| use = use->next_use()) { |
| Instruction* instr = use->instruction(); |
| if (UseIsARedefinition(use) && |
| HasLoadsFromPlace(instr->Cast<Definition>(), place)) { |
| return true; |
| } |
| bool is_load = false, is_store; |
| Place load_place(instr, &is_load, &is_store); |
| |
| if (is_load && load_place.Equals(*place)) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| // Returns true if the given [use] is a redefinition (e.g. RedefinitionInstr, |
| // CheckNull, CheckArrayBound, etc). |
| static bool UseIsARedefinition(Value* use) { |
| Instruction* instr = use->instruction(); |
| return instr->IsDefinition() && |
| (instr->Cast<Definition>()->RedefinedValue() == use); |
| } |
| |
| // Check if any use of the definition can create an alias. |
| // Can add more objects into aliasing_worklist_. |
| bool AnyUseCreatesAlias(Definition* defn) { |
| for (Value* use = defn->input_use_list(); use != nullptr; |
| use = use->next_use()) { |
| Instruction* instr = use->instruction(); |
| if (instr->HasUnknownSideEffects() || |
| (instr->IsLoadField() && |
| instr->AsLoadField()->MayCreateUntaggedAlias()) || |
| (instr->IsStoreIndexed() && |
| (use->use_index() == StoreIndexedInstr::kValuePos)) || |
| instr->IsStoreStaticField() || instr->IsPhi()) { |
| return true; |
| } else if (UseIsARedefinition(use) && |
| AnyUseCreatesAlias(instr->Cast<Definition>())) { |
| return true; |
| } else if (instr->IsStoreField()) { |
| StoreFieldInstr* store = instr->AsStoreField(); |
| |
| if (store->slot().kind() == Slot::Kind::kTypedDataView_typed_data) { |
| // Initialization of TypedDataView.typed_data field creates |
| // aliasing between the view and original typed data, |
| // as the same data can now be accessed via both typed data |
| // view and the original typed data. |
| return true; |
| } |
| |
| if (use->use_index() != StoreFieldInstr::kInstancePos) { |
| ASSERT(use->use_index() == StoreFieldInstr::kValuePos); |
| // If we store this value into an object that is not aliased itself |
| // and we never load again then the store does not create an alias. |
| Definition* instance = |
| store->instance()->definition()->OriginalDefinition(); |
| if (Place::IsAllocation(instance) && |
| !instance->Identity().IsAliased()) { |
| bool is_load, is_store; |
| Place store_place(instr, &is_load, &is_store); |
| |
| if (!HasLoadsFromPlace(instance, &store_place)) { |
| // No loads found that match this store. If it is yet unknown if |
| // the object is not aliased then optimistically assume this but |
| // add it to the worklist to check its uses transitively. |
| if (instance->Identity().IsUnknown()) { |
| instance->SetIdentity(AliasIdentity::NotAliased()); |
| aliasing_worklist_.Add(instance); |
| } |
| continue; |
| } |
| } |
| return true; |
| } |
| } else if (auto* const alloc = instr->AsAllocation()) { |
| // Treat inputs to an allocation instruction exactly as if they were |
| // manually stored using a StoreField instruction. |
| if (alloc->Identity().IsAliased()) { |
| return true; |
| } |
| Place input_place(alloc, use->use_index()); |
| if (HasLoadsFromPlace(alloc, &input_place)) { |
| return true; |
| } |
| if (alloc->Identity().IsUnknown()) { |
| alloc->SetIdentity(AliasIdentity::NotAliased()); |
| aliasing_worklist_.Add(alloc); |
| } |
| } |
| } |
| return false; |
| } |
| |
| void MarkDefinitionAsAliased(Definition* d) { |
| auto* const defn = d->OriginalDefinition(); |
| if (defn->Identity().IsNotAliased()) { |
| defn->SetIdentity(AliasIdentity::Aliased()); |
| identity_rollback_.Add(defn); |
| |
| // Add to worklist to propagate the mark transitively. |
| aliasing_worklist_.Add(defn); |
| } |
| } |
| |
| // Mark any value stored into the given object as potentially aliased. |
| void MarkStoredValuesEscaping(Definition* defn) { |
| // Find all inputs corresponding to fields if allocating an object. |
| if (auto* const alloc = defn->AsAllocation()) { |
| for (intptr_t i = 0; i < alloc->InputCount(); i++) { |
| if (auto* const slot = alloc->SlotForInput(i)) { |
| MarkDefinitionAsAliased(alloc->InputAt(i)->definition()); |
| } |
| } |
| } |
| // Find all stores into this object. |
| for (Value* use = defn->input_use_list(); use != nullptr; |
| use = use->next_use()) { |
| auto instr = use->instruction(); |
| if (UseIsARedefinition(use)) { |
| MarkStoredValuesEscaping(instr->AsDefinition()); |
| continue; |
| } |
| if ((use->use_index() == StoreFieldInstr::kInstancePos) && |
| instr->IsStoreField()) { |
| MarkDefinitionAsAliased(instr->AsStoreField()->value()->definition()); |
| } |
| } |
| } |
| |
| // Determine if the given definition can't be aliased. |
| void ComputeAliasing(Definition* alloc) { |
| ASSERT(Place::IsAllocation(alloc)); |
| ASSERT(alloc->Identity().IsUnknown()); |
| ASSERT(aliasing_worklist_.is_empty()); |
| |
| alloc->SetIdentity(AliasIdentity::NotAliased()); |
| aliasing_worklist_.Add(alloc); |
| |
| while (!aliasing_worklist_.is_empty()) { |
| Definition* defn = aliasing_worklist_.RemoveLast(); |
| ASSERT(Place::IsAllocation(defn)); |
| // If the definition in the worklist was optimistically marked as |
| // not-aliased check that optimistic assumption still holds: check if |
| // any of its uses can create an alias. |
| if (!defn->Identity().IsAliased() && AnyUseCreatesAlias(defn)) { |
| defn->SetIdentity(AliasIdentity::Aliased()); |
| identity_rollback_.Add(defn); |
| } |
| |
| // If the allocation site is marked as aliased conservatively mark |
| // any values stored into the object aliased too. |
| if (defn->Identity().IsAliased()) { |
| MarkStoredValuesEscaping(defn); |
| } |
| } |
| } |
| |
| Zone* zone_; |
| |
| const bool print_traces_; |
| |
| PointerSet<Place>* places_map_; |
| |
| const ZoneGrowableArray<Place*>& places_; |
| |
| const PhiPlaceMoves* phi_moves_; |
| |
| // A list of all seen aliases and a map that allows looking up canonical |
| // alias object. |
| GrowableArray<const Place*> aliases_; |
| PointerSet<const Place> aliases_map_; |
| |
| SmallSet<Place::ElementSize> typed_data_access_sizes_; |
| |
| // Maps alias id to set of ids of places representing the alias. |
| // Place represents an alias if this alias is least generic alias for |
| // the place. |
| // (see ToAlias for the definition of least generic alias). |
| GrowableArray<BitVector*> representatives_; |
| |
| // Maps alias id to set of ids of places aliased. |
| GrowableArray<BitVector*> killed_; |
| |
| // Set of ids of places that can be affected by side-effects other than |
| // explicit stores (i.e. through calls). |
| BitVector* aliased_by_effects_; |
| |
| // Worklist used during alias analysis. |
| GrowableArray<Definition*> aliasing_worklist_; |
| |
| // List of definitions that had their identity set to Aliased. At the end |
| // of load optimization their identity will be rolled back to Unknown to |
| // avoid treating them as Aliased at later stages without checking first |
| // as optimizations can potentially eliminate instructions leading to |
| // aliasing. |
| GrowableArray<Definition*> identity_rollback_; |
| }; |
| |
| static Definition* GetStoredValue(Instruction* instr) { |
| if (instr->IsStoreIndexed()) { |
| return instr->AsStoreIndexed()->value()->definition(); |
| } |
| |
| StoreFieldInstr* store_instance_field = instr->AsStoreField(); |
| if (store_instance_field != nullptr) { |
| return store_instance_field->value()->definition(); |
| } |
| |
| StoreStaticFieldInstr* store_static_field = instr->AsStoreStaticField(); |
| if (store_static_field != nullptr) { |
| return store_static_field->value()->definition(); |
| } |
| |
| UNREACHABLE(); // Should only be called for supported store instructions. |
| return nullptr; |
| } |
| |
| static bool IsPhiDependentPlace(Place* place) { |
| return (place->kind() == Place::kInstanceField) && |
| (place->instance() != nullptr) && place->instance()->IsPhi(); |
| } |
| |
| // For each place that depends on a phi ensure that equivalent places |
| // corresponding to phi input are numbered and record outgoing phi moves |
| // for each block which establish correspondence between phi dependent place |
| // and phi input's place that is flowing in. |
| static PhiPlaceMoves* ComputePhiMoves(PointerSet<Place>* map, |
| ZoneGrowableArray<Place*>* places, |
| bool print_traces) { |
| Thread* thread = Thread::Current(); |
| Zone* zone = thread->zone(); |
| PhiPlaceMoves* phi_moves = new (zone) PhiPlaceMoves(); |
| |
| for (intptr_t i = 0; i < places->length(); i++) { |
| Place* place = (*places)[i]; |
| |
| if (IsPhiDependentPlace(place)) { |
| PhiInstr* phi = place->instance()->AsPhi(); |
| BlockEntryInstr* block = phi->GetBlock(); |
| |
| if (FLAG_trace_optimization && print_traces) { |
| THR_Print("phi dependent place %s\n", place->ToCString()); |
| } |
| |
| Place input_place(*place); |
| for (intptr_t j = 0; j < phi->InputCount(); j++) { |
| input_place.set_instance(phi->InputAt(j)->definition()); |
| |
| Place* result = map->LookupValue(&input_place); |
| if (result == nullptr) { |
| result = Place::Wrap(zone, input_place, places->length()); |
| map->Insert(result); |
| places->Add(result); |
| if (FLAG_trace_optimization && print_traces) { |
| THR_Print(" adding place %s as %" Pd "\n", result->ToCString(), |
| result->id()); |
| } |
| } |
| phi_moves->CreateOutgoingMove(zone, block->PredecessorAt(j), |
| result->id(), place->id()); |
| } |
| } |
| } |
| |
| return phi_moves; |
| } |
| |
| DART_FORCE_INLINE static void SetPlaceId(Instruction* instr, intptr_t id) { |
| instr->SetPassSpecificId(CompilerPass::kCSE, id); |
| } |
| |
| DART_FORCE_INLINE static bool HasPlaceId(const Instruction* instr) { |
| return instr->HasPassSpecificId(CompilerPass::kCSE); |
| } |
| |
| DART_FORCE_INLINE static intptr_t GetPlaceId(const Instruction* instr) { |
| ASSERT(HasPlaceId(instr)); |
| return instr->GetPassSpecificId(CompilerPass::kCSE); |
| } |
| |
| enum CSEMode { kOptimizeLoads, kOptimizeStores }; |
| |
| static AliasedSet* NumberPlaces(FlowGraph* graph, |
| PointerSet<Place>* map, |
| CSEMode mode) { |
| // Loads representing different expression ids will be collected and |
| // used to build per offset kill sets. |
| Zone* zone = graph->zone(); |
| ZoneGrowableArray<Place*>* places = new (zone) ZoneGrowableArray<Place*>(10); |
| |
| bool has_loads = false; |
| bool has_stores = false; |
| for (BlockIterator it = graph->reverse_postorder_iterator(); !it.Done(); |
| it.Advance()) { |
| BlockEntryInstr* block = it.Current(); |
| |
| for (ForwardInstructionIterator instr_it(block); !instr_it.Done(); |
| instr_it.Advance()) { |
| Instruction* instr = instr_it.Current(); |
| Place place(instr, &has_loads, &has_stores); |
| if (place.kind() == Place::kNone) { |
| continue; |
| } |
| |
| Place* result = map->LookupValue(&place); |
| if (result == nullptr) { |
| result = Place::Wrap(zone, place, places->length()); |
| map->Insert(result); |
| places->Add(result); |
| |
| if (FLAG_trace_optimization && graph->should_print()) { |
| THR_Print("numbering %s as %" Pd "\n", result->ToCString(), |
| result->id()); |
| } |
| } |
| |
| SetPlaceId(instr, result->id()); |
| } |
| } |
| |
| if ((mode == kOptimizeLoads) && !has_loads) { |
| return nullptr; |
| } |
| if ((mode == kOptimizeStores) && !has_stores) { |
| return nullptr; |
| } |
| |
| PhiPlaceMoves* phi_moves = |
| ComputePhiMoves(map, places, graph->should_print()); |
| |
| // Build aliasing sets mapping aliases to loads. |
| return new (zone) |
| AliasedSet(zone, map, places, phi_moves, graph->should_print()); |
| } |
| |
| // Load instructions handled by load elimination. |
| static bool IsLoadEliminationCandidate(Instruction* instr) { |
| if (instr->IsDefinition() && |
| instr->AsDefinition()->MayCreateUnsafeUntaggedPointer()) { |
| return false; |
| } |
| if (auto* load = instr->AsLoadField()) { |
| if (load->slot().is_weak()) { |
| return false; |
| } |
| } |
| return instr->IsLoadField() || instr->IsLoadIndexed() || |
| instr->IsLoadStaticField(); |
| } |
| |
| static bool IsLoopInvariantLoad(ZoneGrowableArray<BitVector*>* sets, |
| intptr_t loop_header_index, |
| Instruction* instr) { |
| return IsLoadEliminationCandidate(instr) && (sets != nullptr) && |
| HasPlaceId(instr) && |
| (*sets)[loop_header_index]->Contains(GetPlaceId(instr)); |
| } |
| |
| LICM::LICM(FlowGraph* flow_graph) : flow_graph_(flow_graph) { |
| ASSERT(flow_graph->is_licm_allowed()); |
| } |
| |
| void LICM::Hoist(ForwardInstructionIterator* it, |
| BlockEntryInstr* pre_header, |
| Instruction* current) { |
| if (FLAG_trace_optimization && flow_graph_->should_print()) { |
| THR_Print("Hoisting instruction %s:%" Pd " from B%" Pd " to B%" Pd "\n", |
| current->DebugName(), current->GetDeoptId(), |
| current->GetBlock()->block_id(), pre_header->block_id()); |
| } |
| // Move the instruction out of the loop. |
| current->RemoveEnvironment(); |
| if (it != nullptr) { |
| it->RemoveCurrentFromGraph(); |
| } else { |
| current->RemoveFromGraph(); |
| } |
| GotoInstr* last = pre_header->last_instruction()->AsGoto(); |
| // Using kind kEffect will not assign a fresh ssa temporary index. |
| flow_graph()->InsertBefore(last, current, last->env(), FlowGraph::kEffect); |
| // If the hoisted instruction lazy-deopts, it should continue at the start of |
| // the Goto (of which we copy the deopt-id from). |
| current->env()->MarkAsLazyDeoptToBeforeDeoptId(); |
| current->env()->MarkAsHoisted(); |
| current->CopyDeoptIdFrom(*last); |
| } |
| |
| void LICM::TrySpecializeSmiPhi(PhiInstr* phi, |
| BlockEntryInstr* header, |
| BlockEntryInstr* pre_header) { |
| if (phi->Type()->ToCid() == kSmiCid) { |
| return; |
| } |
| |
| // Check if there is only a single kDynamicCid input to the phi that |
| // comes from the pre-header. |
| const intptr_t kNotFound = -1; |
| intptr_t non_smi_input = kNotFound; |
| for (intptr_t i = 0; i < phi->InputCount(); ++i) { |
| Value* input = phi->InputAt(i); |
| if (input->Type()->ToCid() != kSmiCid) { |
| if ((non_smi_input != kNotFound) || |
| (input->Type()->ToCid() != kDynamicCid)) { |
| // There are multiple kDynamicCid inputs or there is an input that is |
| // known to be non-smi. |
| return; |
| } else { |
| non_smi_input = i; |
| } |
| } |
| } |
| |
| if ((non_smi_input == kNotFound) || |
| (phi->block()->PredecessorAt(non_smi_input) != pre_header)) { |
| return; |
| } |
| |
| CheckSmiInstr* check = nullptr; |
| for (Value* use = phi->input_use_list(); |
| (use != nullptr) && (check == nullptr); use = use->next_use()) { |
| check = use->instruction()->AsCheckSmi(); |
| } |
| |
| if (check == nullptr) { |
| return; |
| } |
| |
| // Host CheckSmi instruction and make this phi smi one. |
| Hoist(nullptr, pre_header, check); |
| |
| // Replace value we are checking with phi's input. |
| check->value()->BindTo(phi->InputAt(non_smi_input)->definition()); |
| check->value()->SetReachingType(phi->InputAt(non_smi_input)->Type()); |
| |
| phi->UpdateType(CompileType::FromCid(kSmiCid)); |
| } |
| |
| void LICM::OptimisticallySpecializeSmiPhis() { |
| if (flow_graph()->function().ProhibitsInstructionHoisting() || |
| CompilerState::Current().is_aot()) { |
| // Do not hoist any: Either deoptimized on a hoisted check, |
| // or compiling precompiled code where we can't do optimistic |
| // hoisting of checks. |
| return; |
| } |
| |
| const ZoneGrowableArray<BlockEntryInstr*>& loop_headers = |
| flow_graph()->GetLoopHierarchy().headers(); |
| |
| for (intptr_t i = 0; i < loop_headers.length(); ++i) { |
| JoinEntryInstr* header = loop_headers[i]->AsJoinEntry(); |
| // Skip loop that don't have a pre-header block. |
| BlockEntryInstr* pre_header = header->ImmediateDominator(); |
| if (pre_header == nullptr) continue; |
| |
| for (PhiIterator it(header); !it.Done(); it.Advance()) { |
| TrySpecializeSmiPhi(it.Current(), header, pre_header); |
| } |
| } |
| } |
| |
| void LICM::Optimize() { |
| if (flow_graph()->function().ProhibitsInstructionHoisting()) { |
| // Do not hoist any. |
| return; |
| } |
| |
| // Compute loops and induction in flow graph. |
| const LoopHierarchy& loop_hierarchy = flow_graph()->GetLoopHierarchy(); |
| const ZoneGrowableArray<BlockEntryInstr*>& loop_headers = |
| loop_hierarchy.headers(); |
| loop_hierarchy.ComputeInduction(); |
| |
| ZoneGrowableArray<BitVector*>* loop_invariant_loads = |
| flow_graph()->loop_invariant_loads(); |
| |
| // Iterate over all loops. |
| for (intptr_t i = 0; i < loop_headers.length(); ++i) { |
| BlockEntryInstr* header = loop_headers[i]; |
| |
| // Skip loops that don't have a pre-header block. |
| BlockEntryInstr* pre_header = header->ImmediateDominator(); |
| if (pre_header == nullptr) { |
| continue; |
| } |
| |
| // Flag that remains true as long as the loop has not seen any instruction |
| // that may have a "visible" effect (write, throw, or other side-effect). |
| bool seen_visible_effect = false; |
| |
| // Iterate over all blocks in the loop. |
| LoopInfo* loop = header->loop_info(); |
| for (BitVector::Iterator loop_it(loop->blocks()); !loop_it.Done(); |
| loop_it.Advance()) { |
| BlockEntryInstr* block = flow_graph()->preorder()[loop_it.Current()]; |
| |
| // Preserve the "visible" effect flag as long as the preorder traversal |
| // sees always-taken blocks. This way, we can only hoist invariant |
| // may-throw instructions that are always seen during the first iteration. |
| if (!seen_visible_effect && !loop->IsAlwaysTaken(block)) { |
| seen_visible_effect = true; |
| } |
| // Iterate over all instructions in the block. |
| for (ForwardInstructionIterator it(block); !it.Done(); it.Advance()) { |
| Instruction* current = it.Current(); |
| |
| // Treat loads of static final fields specially: we can CSE them but |
| // we should not move them around unless the field is initialized. |
| // Otherwise we might move load past the initialization. |
| if (LoadStaticFieldInstr* load = current->AsLoadStaticField()) { |
| if (load->AllowsCSE()) { |
| seen_visible_effect = true; |
| continue; |
| } |
| } |
| |
| // Determine if we can hoist loop invariant code. Even may-throw |
| // instructions can be hoisted as long as its exception is still |
| // the very first "visible" effect of the loop. |
| bool is_loop_invariant = false; |
| if ((current->AllowsCSE() || |
| IsLoopInvariantLoad(loop_invariant_loads, i, current)) && |
| (!seen_visible_effect || !current->MayHaveVisibleEffect())) { |
| is_loop_invariant = true; |
| for (intptr_t i = 0; i < current->InputCount(); ++i) { |
| Definition* input_def = current->InputAt(i)->definition(); |
| if (!input_def->GetBlock()->Dominates(pre_header)) { |
| is_loop_invariant = false; |
| break; |
| } |
| } |
| } |
| |
| // Hoist if all inputs are loop invariant. If not hoisted, any |
| // instruction that writes, may throw, or has an unknown side |
| // effect invalidates the first "visible" effect flag. |
| if (is_loop_invariant) { |
| Hoist(&it, pre_header, current); |
| } else if (!seen_visible_effect && current->MayHaveVisibleEffect()) { |
| seen_visible_effect = true; |
| } |
| } |
| } |
| } |
| } |
| |
| void DelayAllocations::Optimize(FlowGraph* graph) { |
| // Go through all Allocation 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->IsAllocation() && def->env() == nullptr && |
| !moved.HasKey(def)) { |
| auto [block_of_use, use] = DominantUse({block, def}); |
| if (use != nullptr && !use->IsPhi() && |
| IsOneTimeUse(block_of_use, block)) { |
| instr_it.RemoveCurrentFromGraph(); |
| def->InsertBefore(use); |
| moved.Insert(def); |
| } |
| } |
| } |
| } |
| } |
| |
| std::pair<BlockEntryInstr*, Instruction*> DelayAllocations::DominantUse( |
| std::pair<BlockEntryInstr*, Definition*> def_in_block) { |
| const auto block_of_def = def_in_block.first; |
| const auto def = def_in_block.second; |
| |
| // Find the use that dominates all other uses. |
| |
| // Collect all uses. |
| DirectChainedHashMap<IdentitySetKeyValueTrait<Instruction*>> uses; |
| for (Value::Iterator it(def->input_use_list()); !it.Done(); it.Advance()) { |
| Instruction* use = it.Current()->instruction(); |
| uses.Insert(use); |
| } |
| for (Value::Iterator it(def->env_use_list()); !it.Done(); it.Advance()) { |
| Instruction* use = it.Current()->instruction(); |
| uses.Insert(use); |
| } |
| |
| // Returns |true| iff |instr| or any instruction dominating it are either a |
| // a |def| or a use of a |def|. |
| // |
| // Additionally this function computes the block of the |instr| and sets |
| // |*block_of_instr| to that block. |
| auto is_dominated_by_another_use = [&](Instruction* instr, |
| BlockEntryInstr** block_of_instr) { |
| BlockEntryInstr* first_block = nullptr; |
| while (instr != def) { |
| if (uses.HasKey(instr)) { |
| // We hit another use, meaning that this use dominates the given |use|. |
| return true; |
| } |
| if (auto block = instr->AsBlockEntry()) { |
| if (first_block == nullptr) { |
| first_block = block; |
| } |
| instr = block->dominator()->last_instruction(); |
| } else { |
| instr = instr->previous(); |
| } |
| } |
| if (block_of_instr != nullptr) { |
| if (first_block == nullptr) { |
| // We hit |def| while iterating instructions without actually hitting |
| // the start of the block. This means |def| and |use| reside in the |
| // same block. |
| *block_of_instr = block_of_def; |
| } else { |
| *block_of_instr = first_block; |
| } |
| } |
| return false; |
| }; |
| |
| // Find the dominant use. |
| std::pair<BlockEntryInstr*, Instruction*> dominant_use = {nullptr, nullptr}; |
| auto use_it = uses.GetIterator(); |
| while (auto use = use_it.Next()) { |
| bool dominated_by_another_use = false; |
| BlockEntryInstr* block_of_use = nullptr; |
| |
| if (auto phi = (*use)->AsPhi()) { |
| // For phi uses check that the dominant use dominates all |
| // predecessor blocks corresponding to matching phi inputs. |
| ASSERT(phi->InputCount() == phi->block()->PredecessorCount()); |
| dominated_by_another_use = true; |
| for (intptr_t i = 0; i < phi->InputCount(); i++) { |
| if (phi->InputAt(i)->definition() == def) { |
| if (!is_dominated_by_another_use( |
| phi->block()->PredecessorAt(i)->last_instruction(), |
| /*block_of_instr=*/nullptr)) { |
| dominated_by_another_use = false; |
| block_of_use = phi->block(); |
| break; |
| } |
| } |
| } |
| } else { |
| // Start with the instruction before the use, then walk backwards through |
| // blocks in the dominator chain until we hit the definition or |
| // another use. |
| dominated_by_another_use = |
| is_dominated_by_another_use((*use)->previous(), &block_of_use); |
| } |
| |
| if (!dominated_by_another_use) { |
| if (dominant_use.second != nullptr) { |
| // More than one use reached the definition, which means no use |
| // dominates all other uses. |
| return {nullptr, nullptr}; |
| } |
| dominant_use = {block_of_use, *use}; |
| } |
| } |
| |
| return dominant_use; |
| } |
| |
| bool DelayAllocations::IsOneTimeUse(BlockEntryInstr* use_block, |
| BlockEntryInstr* def_block) { |
| // 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. |
| 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) |
| : graph_(graph), |
| aliased_set_(aliased_set), |
| in_(graph_->preorder().length()), |
| out_(graph_->preorder().length()), |
| gen_(graph_->preorder().length()), |
| kill_(graph_->preorder().length()), |
| exposed_values_(graph_->preorder().length()), |
| out_values_(graph_->preorder().length()), |
| phis_(5), |
| worklist_(5), |
| congruency_worklist_(6), |
| in_worklist_(nullptr), |
| forwarded_(false) { |
| const intptr_t num_blocks = graph_->preorder().length(); |
| for (intptr_t i = 0; i < num_blocks; i++) { |
| out_.Add(nullptr); |
| gen_.Add(new (Z) BitVector(Z, aliased_set_->max_place_id())); |
| kill_.Add(new (Z) BitVector(Z, aliased_set_->max_place_id())); |
| in_.Add(new (Z) BitVector(Z, aliased_set_->max_place_id())); |
| |
| exposed_values_.Add(nullptr); |
| out_values_.Add(nullptr); |
| } |
| } |
| |
| ~LoadOptimizer() { aliased_set_->RollbackAliasedIdentities(); } |
| |
| Zone* zone() const { return graph_->zone(); } |
| |
| static bool OptimizeGraph(FlowGraph* graph) { |
| ASSERT(FLAG_load_cse); |
| |
| // For now, bail out for large functions to avoid OOM situations. |
| // TODO(fschneider): Fix the memory consumption issue. |
| if (graph->is_huge_method()) { |
| return false; |
| } |
| |
| PointerSet<Place> map; |
| AliasedSet* aliased_set = NumberPlaces(graph, &map, kOptimizeLoads); |
| if ((aliased_set != nullptr) && !aliased_set->IsEmpty()) { |
| // If any loads were forwarded return true from Optimize to run load |
| // forwarding again. This will allow to forward chains of loads. |
| // This is especially important for context variables as they are built |
| // as loads from loaded context. |
| // TODO(vegorov): renumber newly discovered congruences during the |
| // forwarding to forward chains without running whole pass twice. |
| LoadOptimizer load_optimizer(graph, aliased_set); |
| return load_optimizer.Optimize(); |
| } |
| return false; |
| } |
| |
| private: |
| bool Optimize() { |
| // Initializer calls should be eliminated before ComputeInitialSets() |
| // in order to calculate kill sets more precisely. |
| OptimizeLazyInitialization(); |
| |
| ComputeInitialSets(); |
| ComputeOutSets(); |
| ComputeOutValues(); |
| if (graph_->is_licm_allowed()) { |
| MarkLoopInvariantLoads(); |
| } |
| ForwardLoads(); |
| EmitPhis(); |
| return forwarded_; |
| } |
| |
| bool CallsInitializer(Instruction* instr) { |
| if (auto* load_field = instr->AsLoadField()) { |
| return load_field->calls_initializer(); |
| } else if (auto* load_static = instr->AsLoadStaticField()) { |
| return load_static->calls_initializer(); |
| } |
| return false; |
| } |
| |
| void ClearCallsInitializer(Instruction* instr) { |
| if (auto* load_field = instr->AsLoadField()) { |
| load_field->set_calls_initializer(false); |
| } else if (auto* load_static = instr->AsLoadStaticField()) { |
| load_static->set_calls_initializer(false); |
| } else { |
| UNREACHABLE(); |
| } |
| } |
| |
| bool CanForwardLoadTo(Definition* load, Definition* replacement) { |
| // Loads which check initialization status can only be replaced if |
| // we can guarantee that forwarded value is not a sentinel. |
| return !(CallsInitializer(load) && replacement->Type()->can_be_sentinel()); |
| } |
| |
| // Returns true if given instruction stores the sentinel value. |
| // Such a store doesn't initialize corresponding field. |
| bool IsSentinelStore(Instruction* instr) { |
| Value* value = nullptr; |
| if (auto* store_field = instr->AsStoreField()) { |
| value = store_field->value(); |
| } else if (auto* store_static = instr->AsStoreStaticField()) { |
| value = store_static->value(); |
| } |
| return value != nullptr && value->BindsToConstant() && |
| (value->BoundConstant().ptr() == Object::sentinel().ptr()); |
| } |
| |
| // This optimization pass tries to get rid of lazy initializer calls in |
| // LoadField and LoadStaticField instructions. The "initialized" state of |
| // places is propagated through the flow graph. |
| void OptimizeLazyInitialization() { |
| if (!FLAG_optimize_lazy_initializer_calls) { |
| return; |
| } |
| |
| // 1) Populate 'gen' sets with places which are initialized at each basic |
| // block. Optimize lazy initializer calls within basic block and |
| // figure out if there are lazy initializer calls left to optimize. |
| bool has_lazy_initializer_calls = false; |
| for (BlockIterator block_it = graph_->reverse_postorder_iterator(); |
| !block_it.Done(); block_it.Advance()) { |
| BlockEntryInstr* block = block_it.Current(); |
| BitVector* gen = gen_[block->preorder_number()]; |
| |
| for (ForwardInstructionIterator instr_it(block); !instr_it.Done(); |
| instr_it.Advance()) { |
| Instruction* instr = instr_it.Current(); |
| |
| bool is_load = false, is_store = false; |
| Place place(instr, &is_load, &is_store); |
| |
| if (is_store && !IsSentinelStore(instr)) { |
| gen->Add(GetPlaceId(instr)); |
| } else if (is_load) { |
| const auto place_id = GetPlaceId(instr); |
| if (CallsInitializer(instr)) { |
| if (gen->Contains(place_id)) { |
| ClearCallsInitializer(instr); |
| } else { |
| has_lazy_initializer_calls = true; |
| } |
| } |
| gen->Add(place_id); |
| } |
| } |
| |
| // Spread initialized state through outgoing phis. |
| PhiPlaceMoves::MovesList phi_moves = |
| aliased_set_->phi_moves()->GetOutgoingMoves(block); |
| if (phi_moves != nullptr) { |
| for (intptr_t i = 0, n = phi_moves->length(); i < n; ++i) { |
| const intptr_t from = (*phi_moves)[i].from(); |
| const intptr_t to = (*phi_moves)[i].to(); |
| if ((from != to) && gen->Contains(from)) { |
| gen->Add(to); |
| } |
| } |
| } |
| } |
| |
| if (has_lazy_initializer_calls) { |
| // 2) Propagate initialized state between blocks, calculating |
| // incoming initialized state. Iterate until reaching fixed point. |
| BitVector* temp = new (Z) BitVector(Z, aliased_set_->max_place_id()); |
| bool changed = true; |
| while (changed) { |
| changed = false; |
| |
| for (BlockIterator block_it = graph_->reverse_postorder_iterator(); |
| !block_it.Done(); block_it.Advance()) { |
| BlockEntryInstr* block = block_it.Current(); |
| BitVector* block_in = in_[block->preorder_number()]; |
| BitVector* gen = gen_[block->preorder_number()]; |
| |
| // Incoming initialized state is the intersection of all |
| // outgoing initialized states of predecessors. |
| if (block->IsGraphEntry()) { |
| temp->Clear(); |
| } else { |
| temp->SetAll(); |
| ASSERT(block->PredecessorCount() > 0); |
| for (intptr_t i = 0, pred_count = block->PredecessorCount(); |
| i < pred_count; ++i) { |
| BlockEntryInstr* pred = block->PredecessorAt(i); |
| BitVector* pred_out = gen_[pred->preorder_number()]; |
| temp->Intersect(pred_out); |
| } |
| } |
| |
| if (!temp->Equals(*block_in)) { |
| ASSERT(block_in->SubsetOf(*temp)); |
| block_in->AddAll(temp); |
| gen->AddAll(temp); |
| changed = true; |
| } |
| } |
| } |
| |
| // 3) Single pass through basic blocks to optimize lazy |
| // initializer calls using calculated incoming inter-block |
| // initialized state. |
| for (BlockIterator block_it = graph_->reverse_postorder_iterator(); |
| !block_it.Done(); block_it.Advance()) { |
| BlockEntryInstr* block = block_it.Current(); |
| BitVector* block_in = in_[block->preorder_number()]; |
| |
| for (ForwardInstructionIterator instr_it(block); !instr_it.Done(); |
| instr_it.Advance()) { |
| Instruction* instr = instr_it.Current(); |
| if (CallsInitializer(instr) && |
| block_in->Contains(GetPlaceId(instr))) { |
| ClearCallsInitializer(instr); |
| } |
| } |
| } |
| } |
| |
| // Clear sets which are also used in the main part of load forwarding. |
| for (intptr_t i = 0, n = graph_->preorder().length(); i < n; ++i) { |
| gen_[i]->Clear(); |
| in_[i]->Clear(); |
| } |
| } |
| |
| // Only forward stores to normal arrays, float64, and simd arrays |
| // to loads because other array stores (intXX/uintXX/float32) |
| // may implicitly convert the value stored. |
| bool CanForwardStore(StoreIndexedInstr* array_store) { |
| if (array_store == nullptr) return true; |
| auto const rep = RepresentationUtils::RepresentationOfArrayElement( |
| array_store->class_id()); |
| return !RepresentationUtils::IsUnboxedInteger(rep) && rep != kUnboxedFloat; |
| } |
| |
| static bool AlreadyPinnedByRedefinition(Definition* replacement, |
| Definition* redefinition) { |
| Definition* defn = replacement; |
| if (auto load_field = replacement->AsLoadField()) { |
| defn = load_field->instance()->definition(); |
| } else if (auto load_indexed = replacement->AsLoadIndexed()) { |
| defn = load_indexed->array()->definition(); |
| } |
| |
| Value* unwrapped; |
| while ((unwrapped = defn->RedefinedValue()) != nullptr) { |
| if (defn == redefinition) { |
| return true; |
| } |
| defn = unwrapped->definition(); |
| } |
| |
| return false; |
| } |
| |
| Definition* ReplaceLoad(Definition* load, Definition* replacement) { |
| // When replacing a load from a generic field or from an array element |
| // check if instance we are loading from is redefined. If it is then |
| // we need to ensure that replacement is not going to break the |
| // dependency chain. |
| Definition* redef = nullptr; |
| if (auto load_field = load->AsLoadField()) { |
| auto instance = load_field->instance()->definition(); |
| if (instance->RedefinedValue() != nullptr) { |
| if ((load_field->slot().kind() == |
| Slot::Kind::kGrowableObjectArray_data || |
| (load_field->slot().IsDartField() && |
| !AbstractType::Handle(load_field->slot().field().type()) |
| .IsInstantiated()))) { |
| redef = instance; |
| } |
| } |
| } else if (auto load_indexed = load->AsLoadIndexed()) { |
| if (load_indexed->class_id() == kArrayCid || |
| load_indexed->class_id() == kImmutableArrayCid) { |
| auto instance = load_indexed->array()->definition(); |
| if (instance->RedefinedValue() != nullptr) { |
| redef = instance; |
| } |
| } |
| } |
| if (redef != nullptr && !AlreadyPinnedByRedefinition(replacement, redef)) { |
| // Original load had a redefined instance and replacement does not |
| // depend on the same redefinition. Create a redefinition |
| // of the replacement to keep the dependency chain. |
| auto replacement_redefinition = |
| new (zone()) RedefinitionInstr(new (zone()) Value(replacement)); |
| if (redef->IsDominatedBy(replacement)) { |
| graph_->InsertAfter(redef, replacement_redefinition, /*env=*/nullptr, |
| FlowGraph::kValue); |
| } else { |
| graph_->InsertBefore(load, replacement_redefinition, /*env=*/nullptr, |
| FlowGraph::kValue); |
| } |
| replacement = replacement_redefinition; |
| } |
| |
| load->ReplaceUsesWith(replacement); |
| return replacement; |
| } |
| |
| // Compute sets of loads generated and killed by each block. |
| // Additionally compute upwards exposed and generated loads for each block. |
| // Exposed loads are those that can be replaced if a corresponding |
| // reaching load will be found. |
| // Loads that are locally redundant will be replaced as we go through |
| // instructions. |
| void ComputeInitialSets() { |
| for (BlockIterator block_it = graph_->reverse_postorder_iterator(); |
| !block_it.Done(); block_it.Advance()) { |
| BlockEntryInstr* block = block_it.Current(); |
| const intptr_t preorder_number = block->preorder_number(); |
| |
| BitVector* kill = kill_[preorder_number]; |
| BitVector* gen = gen_[preorder_number]; |
| |
| ZoneGrowableArray<Definition*>* exposed_values = nullptr; |
| ZoneGrowableArray<Definition*>* out_values = nullptr; |
| |
| for (ForwardInstructionIterator instr_it(block); !instr_it.Done(); |
| instr_it.Advance()) { |
| Instruction* instr = instr_it.Current(); |
| |
| bool is_load = false, is_store = false; |
| Place place(instr, &is_load, &is_store); |
| |
| BitVector* killed = nullptr; |
| if (is_store) { |
| const intptr_t alias_id = |
| aliased_set_->LookupAliasId(place.ToAlias()); |
| if (alias_id != AliasedSet::kNoAlias) { |
| killed = aliased_set_->GetKilledSet(alias_id); |
| } else if (!place.IsImmutableField()) { |
| // We encountered unknown alias: this means intrablock load |
| // forwarding refined parameter of this store, for example |
| // |
| // o <- alloc() |
| // a.f <- o |
| // u <- a.f |
| // u.x <- null ;; this store alias is *.x |
| // |
| // after intrablock load forwarding |
| // |
| // o <- alloc() |
| // a.f <- o |
| // o.x <- null ;; this store alias is o.x |
| // |
| // In this case we fallback to using place id recorded in the |
| // instruction that still points to the old place with a more |
| // generic alias. |
| const intptr_t old_alias_id = aliased_set_->LookupAliasId( |
| aliased_set_->places()[GetPlaceId(instr)]->ToAlias()); |
| killed = aliased_set_->GetKilledSet(old_alias_id); |
| } |
| |
| // Find canonical place of store. |
| Place* canonical_place = nullptr; |
| if (CanForwardStore(instr->AsStoreIndexed())) { |
| canonical_place = aliased_set_->LookupCanonical(&place); |
| if (canonical_place != nullptr) { |
| // Is this a redundant store (stored value already resides |
| // in this field)? |
| const intptr_t place_id = canonical_place->id(); |
| if (gen->Contains(place_id)) { |
| ASSERT((out_values != nullptr) && |
| ((*out_values)[place_id] != nullptr)); |
| if ((*out_values)[place_id] == GetStoredValue(instr)) { |
| if (FLAG_trace_optimization && graph_->should_print()) { |
| THR_Print("Removing redundant store to place %" Pd |
| " in block B%" Pd "\n", |
| GetPlaceId(instr), block->block_id()); |
| } |
| instr_it.RemoveCurrentFromGraph(); |
| continue; |
| } |
| } |
| } |
| } |
| |
| // Update kill/gen/out_values (after inspection of incoming values). |
| if (killed != nullptr) { |
| kill->AddAll(killed); |
| // There is no need to clear out_values when clearing GEN set |
| // because only those values that are in the GEN set |
| // will ever be used. |
| gen->RemoveAll(killed); |
| } |
| if (canonical_place != nullptr) { |
| // Store has a corresponding numbered place that might have a |
| // load. Try forwarding stored value to it. |
| gen->Add(canonical_place->id()); |
| if (out_values == nullptr) out_values = CreateBlockOutValues(); |
| (*out_values)[canonical_place->id()] = GetStoredValue(instr); |
| } |
| |
| ASSERT(!instr->IsDefinition() || |
| !IsLoadEliminationCandidate(instr->AsDefinition())); |
| continue; |
| } else if (is_load) { |
| // Check if this load needs renumbering because of the intrablock |
| // load forwarding. |
| const Place* canonical = aliased_set_->LookupCanonical(&place); |
| if ((canonical != nullptr) && |
| (canonical->id() != GetPlaceId(instr->AsDefinition()))) { |
| SetPlaceId(instr->AsDefinition(), canonical->id()); |
| } |
| } |
| |
| // If instruction has effects then kill all loads affected. |
| if (instr->HasUnknownSideEffects()) { |
| kill->AddAll(aliased_set_->aliased_by_effects()); |
| // There is no need to clear out_values when removing values from GEN |
| // set because only those values that are in the GEN set |
| // will ever be used. |
| gen->RemoveAll(aliased_set_->aliased_by_effects()); |
| } |
| |
| Definition* defn = instr->AsDefinition(); |
| if (defn == nullptr) { |
| continue; |
| } |
| |
| if (auto* const alloc = instr->AsAllocation()) { |
| if (!alloc->ObjectIsInitialized()) { |
| // Since the allocated object is uninitialized, we can't forward |
| // any values from it. |
| continue; |
| } |
| for (Value* use = alloc->input_use_list(); use != nullptr; |
| use = use->next_use()) { |
| if (use->use_index() != 0) { |
| // Not a potential immediate load or store, since they take the |
| // instance as the first input. |
| continue; |
| } |
| intptr_t place_id = -1; |
| Definition* forward_def = nullptr; |
| const Slot* slot = nullptr; |
| if (auto* const load = use->instruction()->AsLoadField()) { |
| place_id = GetPlaceId(load); |
| slot = &load->slot(); |
| } else if (auto* const store = use->instruction()->AsStoreField()) { |
| ASSERT(!alloc->IsArrayAllocation()); |
| place_id = GetPlaceId(store); |
| slot = &store->slot(); |
| } else if (use->instruction()->IsLoadIndexed() || |
| use->instruction()->IsStoreIndexed()) { |
| if (!alloc->IsArrayAllocation()) { |
| // Non-array allocations can be accessed with LoadIndexed |
| // and StoreIndex in the unreachable code. |
| continue; |
| } |
| if (alloc->IsAllocateTypedData()) { |
| // Typed data payload elements are unboxed and initialized to |
| // zero, so don't forward a tagged null value. |
| continue; |
| } |
| if (aliased_set_->CanBeAliased(alloc)) { |
| continue; |
| } |
| place_id = GetPlaceId(use->instruction()); |
| if (aliased_set_->places()[place_id]->kind() != |
| Place::kConstantIndexed) { |
| continue; |
| } |
| // Set initial value of array element to null. |
| forward_def = graph_->constant_null(); |
| } else { |
| // Not an immediate load or store. |
| continue; |
| } |
| |
| ASSERT(place_id != -1); |
| if (slot != nullptr) { |
| ASSERT(forward_def == nullptr); |
| // Final fields are initialized in constructors. However, at the |
| // same time we assume that known values of final fields can be |
| // forwarded across side-effects. For an escaping object, one such |
| // side effect can be an uninlined constructor invocation. Thus, |
| // if we add 'null' as known initial values for these fields, |
| // this null will be incorrectly propagated across any uninlined |
| // constructor invocation and used instead of the real value. |
| if (aliased_set_->CanBeAliased(alloc) && slot->IsDartField() && |
| slot->is_immutable()) { |
| continue; |
| } |
| |
| const intptr_t pos = alloc->InputForSlot(*slot); |
| if (pos != -1) { |
| forward_def = alloc->InputAt(pos)->definition(); |
| } else if (!slot->is_tagged()) { |
| // Fields that do not contain tagged values should not |
| // have a tagged null value forwarded for them, similar to |
| // payloads of typed data arrays. |
| continue; |
| } else { |
| // Fields not provided as an input to the instruction are |
| // initialized to null during allocation. |
| forward_def = graph_->constant_null(); |
| } |
| } |
| |
| ASSERT(forward_def != nullptr); |
| gen->Add(place_id); |
| if (out_values == nullptr) out_values = CreateBlockOutValues(); |
| (*out_values)[place_id] = forward_def; |
| } |
| continue; |
| } |
| |
| if (!IsLoadEliminationCandidate(defn)) { |
| continue; |
| } |
| |
| const intptr_t place_id = GetPlaceId(defn); |
| if (gen->Contains(place_id)) { |
| // This is a locally redundant load. |
| ASSERT((out_values != nullptr) && |
| ((*out_values)[place_id] != nullptr)); |
| |
| Definition* replacement = (*out_values)[place_id]; |
| if (CanForwardLoadTo(defn, replacement)) { |
| graph_->EnsureSSATempIndex(defn, replacement); |
| if (FLAG_trace_optimization && graph_->should_print()) { |
| THR_Print("Replacing load v%" Pd " with v%" Pd "\n", |
| defn->ssa_temp_index(), replacement->ssa_temp_index()); |
| } |
| |
| ReplaceLoad(defn, replacement); |
| instr_it.RemoveCurrentFromGraph(); |
| forwarded_ = true; |
| continue; |
| } |
| } else if (!kill->Contains(place_id)) { |
| // This is an exposed load: it is the first representative of a |
| // given expression id and it is not killed on the path from |
| // the block entry. |
| if (exposed_values == nullptr) { |
| const intptr_t kMaxExposedValuesInitialSize = 5; |
| exposed_values = new (Z) ZoneGrowableArray<Definition*>( |
| Utils::Minimum(kMaxExposedValuesInitialSize, |
| aliased_set_->max_place_id())); |
| } |
| |
| exposed_values->Add(defn); |
| } |
| |
| gen->Add(place_id); |
| |
| if (out_values == nullptr) out_values = CreateBlockOutValues(); |
| (*out_values)[place_id] = defn; |
| } |
| |
| exposed_values_[preorder_number] = exposed_values; |
| out_values_[preorder_number] = out_values; |
| } |
| } |
| |
| static void PerformPhiMoves(PhiPlaceMoves::MovesList phi_moves, |
| BitVector* out, |
| BitVector* forwarded_loads) { |
| forwarded_loads->Clear(); |
| |
| for (intptr_t i = 0; i < phi_moves->length(); i++) { |
| const intptr_t from = (*phi_moves)[i].from(); |
| const intptr_t to = (*phi_moves)[i].to(); |
| if (from == to) continue; |
| |
| if (out->Contains(from)) { |
| forwarded_loads->Add(to); |
| } |
| } |
| |
| for (intptr_t i = 0; i < phi_moves->length(); i++) { |
| const intptr_t from = (*phi_moves)[i].from(); |
| const intptr_t to = (*phi_moves)[i].to(); |
| if (from == to) continue; |
| |
| out->Remove(to); |
| } |
| |
| out->AddAll(forwarded_loads); |
| } |
| |
| // Compute OUT sets by propagating them iteratively until fix point |
| // is reached. |
| void ComputeOutSets() { |
| BitVector* temp = new (Z) BitVector(Z, aliased_set_->max_place_id()); |
| BitVector* forwarded_loads = |
| new (Z) BitVector(Z, aliased_set_->max_place_id()); |
| BitVector* temp_out = new (Z) BitVector(Z, aliased_set_->max_place_id()); |
| |
| bool changed = true; |
| while (changed) { |
| changed = false; |
| |
| for (BlockIterator block_it = graph_->reverse_postorder_iterator(); |
| !block_it.Done(); block_it.Advance()) { |
| BlockEntryInstr* block = block_it.Current(); |
| |
| const intptr_t preorder_number = block->preorder_number(); |
| |
| BitVector* block_in = in_[preorder_number]; |
| BitVector* block_out = out_[preorder_number]; |
| BitVector* block_kill = kill_[preorder_number]; |
| BitVector* block_gen = gen_[preorder_number]; |
| |
| // Compute block_in as the intersection of all out(p) where p |
| // is a predecessor of the current block. |
| if (block->IsGraphEntry()) { |
| temp->Clear(); |
| } else { |
| temp->SetAll(); |
| ASSERT(block->PredecessorCount() > 0); |
| for (intptr_t i = 0; i < block->PredecessorCount(); i++) { |
| BlockEntryInstr* pred = block->PredecessorAt(i); |
| BitVector* pred_out = out_[pred->preorder_number()]; |
| if (pred_out == nullptr) continue; |
| PhiPlaceMoves::MovesList phi_moves = |
| aliased_set_->phi_moves()->GetOutgoingMoves(pred); |
| if (phi_moves != nullptr) { |
| // If there are phi moves, perform intersection with |
| // a copy of pred_out where the phi moves are applied. |
| temp_out->CopyFrom(pred_out); |
| PerformPhiMoves(phi_moves, temp_out, forwarded_loads); |
| pred_out = temp_out; |
| } |
| temp->Intersect(pred_out); |
| } |
| } |
| |
| if (!temp->Equals(*block_in) || (block_out == nullptr)) { |
| // If IN set has changed propagate the change to OUT set. |
| block_in->CopyFrom(temp); |
| |
| temp->RemoveAll(block_kill); |
| temp->AddAll(block_gen); |
| |
| if ((block_out == nullptr) || !block_out->Equals(*temp)) { |
| if (block_out == nullptr) { |
| block_out = out_[preorder_number] = |
| new (Z) BitVector(Z, aliased_set_->max_place_id()); |
| } |
| block_out->CopyFrom(temp); |
| changed = true; |
| } |
| } |
| } |
| } |
| } |
| |
| // Compute out_values mappings by propagating them in reverse postorder once |
| // through the graph. Generate phis on back edges where eager merge is |
| // impossible. |
| // No replacement is done at this point and thus any out_value[place_id] is |
| // changed at most once: from nullptr to an actual value. |
| // When merging incoming loads we might need to create a phi. |
| // These phis are not inserted at the graph immediately because some of them |
| // might become redundant after load forwarding is done. |
| void ComputeOutValues() { |
| GrowableArray<PhiInstr*> pending_phis(5); |
| ZoneGrowableArray<Definition*>* temp_forwarded_values = nullptr; |
| |
| for (BlockIterator block_it = graph_->reverse_postorder_iterator(); |
| !block_it.Done(); block_it.Advance()) { |
| BlockEntryInstr* block = block_it.Current(); |
| |
| const bool can_merge_eagerly = CanMergeEagerly(block); |
| |
| const intptr_t preorder_number = block->preorder_number(); |
| |
| ZoneGrowableArray<Definition*>* block_out_values = |
| out_values_[preorder_number]; |
| |
| // If OUT set has changed then we have new values available out of |
| // the block. Compute these values creating phi where necessary. |
| for (BitVector::Iterator it(out_[preorder_number]); !it.Done(); |
| it.Advance()) { |
| const intptr_t place_id = it.Current(); |
| |
| if (block_out_values == nullptr) { |
| out_values_[preorder_number] = block_out_values = |
| CreateBlockOutValues(); |
| } |
| |
| if ((*block_out_values)[place_id] == nullptr) { |
| ASSERT(block->PredecessorCount() > 0); |
| Definition* in_value = can_merge_eagerly |
| ? MergeIncomingValues(block, place_id) |
| : nullptr; |
| if ((in_value == nullptr) && |
| (in_[preorder_number]->Contains(place_id))) { |
| PhiInstr* phi = new (Z) |
| PhiInstr(block->AsJoinEntry(), block->PredecessorCount()); |
| SetPlaceId(phi, place_id); |
| pending_phis.Add(phi); |
| in_value = phi; |
| } |
| (*block_out_values)[place_id] = in_value; |
| } |
| } |
| |
| // If the block has outgoing phi moves perform them. Use temporary list |
| // of values to ensure that cyclic moves are performed correctly. |
| PhiPlaceMoves::MovesList phi_moves = |
| aliased_set_->phi_moves()->GetOutgoingMoves(block); |
| if ((phi_moves != nullptr) && (block_out_values != nullptr)) { |
| if (temp_forwarded_values == nullptr) { |
| temp_forwarded_values = CreateBlockOutValues(); |
| } |
| |
| for (intptr_t i = 0; i < phi_moves->length(); i++) { |
| const intptr_t from = (*phi_moves)[i].from(); |
| const intptr_t to = (*phi_moves)[i].to(); |
| if (from == to) continue; |
| |
| (*temp_forwarded_values)[to] = (*block_out_values)[from]; |
| } |
| |
| for (intptr_t i = 0; i < phi_moves->length(); i++) { |
| const intptr_t from = (*phi_moves)[i].from(); |
| const intptr_t to = (*phi_moves)[i].to(); |
| if (from == to) continue; |
| |
| (*block_out_values)[to] = (*temp_forwarded_values)[to]; |
| } |
| } |
| |
| if (FLAG_trace_load_optimization && graph_->should_print()) { |
| THR_Print("B%" Pd "\n", block->block_id()); |
| THR_Print(" IN: "); |
| aliased_set_->PrintSet(in_[preorder_number]); |
| THR_Print("\n"); |
| |
| THR_Print(" KILL: "); |
| aliased_set_->PrintSet(kill_[preorder_number]); |
| THR_Print("\n"); |
| |
| THR_Print(" OUT: "); |
| aliased_set_->PrintSet(out_[preorder_number]); |
| THR_Print("\n"); |
| } |
| } |
| |
| // All blocks were visited. Fill pending phis with inputs |
| // that flow on back edges. |
| for (intptr_t i = 0; i < pending_phis.length(); i++) { |
| FillPhiInputs(pending_phis[i]); |
| } |
| } |
| |
| bool CanMergeEagerly(BlockEntryInstr* block) { |
| for (intptr_t i = 0; i < block->PredecessorCount(); i++) { |
| BlockEntryInstr* pred = block->PredecessorAt(i); |
| if (pred->postorder_number() < block->postorder_number()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| void MarkLoopInvariantLoads() { |
| const ZoneGrowableArray<BlockEntryInstr*>& loop_headers = |
| graph_->GetLoopHierarchy().headers(); |
| |
| ZoneGrowableArray<BitVector*>* invariant_loads = |
| new (Z) ZoneGrowableArray<BitVector*>(loop_headers.length()); |
| |
| for (intptr_t i = 0; i < loop_headers.length(); i++) { |
| BlockEntryInstr* header = loop_headers[i]; |
| BlockEntryInstr* pre_header = header->ImmediateDominator(); |
| if (pre_header == nullptr) { |
| invariant_loads->Add(nullptr); |
| continue; |
| } |
| |
| BitVector* loop_gen = new (Z) BitVector(Z, aliased_set_->max_place_id()); |
| for (BitVector::Iterator loop_it(header->loop_info()->blocks()); |
| !loop_it.Done(); loop_it.Advance()) { |
| const intptr_t preorder_number = loop_it.Current(); |
| loop_gen->AddAll(gen_[preorder_number]); |
| } |
| |
| for (BitVector::Iterator loop_it(header->loop_info()->blocks()); |
| !loop_it.Done(); loop_it.Advance()) { |
| const intptr_t preorder_number = loop_it.Current(); |
| loop_gen->RemoveAll(kill_[preorder_number]); |
| } |
| |
| if (FLAG_trace_optimization && graph_->should_print()) { |
| for (BitVector::Iterator it(loop_gen); !it.Done(); it.Advance()) { |
| THR_Print("place %s is loop invariant for B%" Pd "\n", |
| aliased_set_->places()[it.Current()]->ToCString(), |
| header->block_id()); |
| } |
| } |
| |
| invariant_loads->Add(loop_gen); |
| } |
| |
| graph_->set_loop_invariant_loads(invariant_loads); |
| } |
| |
| // Compute incoming value for the given expression id. |
| // Will create a phi if different values are incoming from multiple |
| // predecessors. |
| Definition* MergeIncomingValues(BlockEntryInstr* block, intptr_t place_id) { |
| // First check if the same value is coming in from all predecessors. |
| static Definition* const kDifferentValuesMarker = |
| reinterpret_cast<Definition*>(-1); |
| Definition* incoming = nullptr; |
| for (intptr_t i = 0; i < block->PredecessorCount(); i++) { |
| BlockEntryInstr* pred = block->PredecessorAt(i); |
| ZoneGrowableArray<Definition*>* pred_out_values = |
| out_values_[pred->preorder_number()]; |
| if ((pred_out_values == nullptr) || |
| ((*pred_out_values)[place_id] == nullptr)) { |
| return nullptr; |
| } else if (incoming == nullptr) { |
| incoming = (*pred_out_values)[place_id]; |
| } else if (incoming != (*pred_out_values)[place_id]) { |
| incoming = kDifferentValuesMarker; |
| } |
| } |
| |
| if (incoming != kDifferentValuesMarker) { |
| ASSERT(incoming != nullptr); |
| return incoming; |
| } |
| |
| // Incoming values are different. Phi is required to merge. |
| PhiInstr* phi = |
| new (Z) PhiInstr(block->AsJoinEntry(), block->PredecessorCount()); |
| SetPlaceId(phi, place_id); |
| FillPhiInputs(phi); |
| return phi; |
| } |
| |
| void FillPhiInputs(PhiInstr* phi) { |
| BlockEntryInstr* block = phi->GetBlock(); |
| const intptr_t place_id = GetPlaceId(phi); |
| |
| for (intptr_t i = 0; i < block->PredecessorCount(); i++) { |
| BlockEntryInstr* pred = block->PredecessorAt(i); |
| ZoneGrowableArray<Definition*>* pred_out_values = |
| out_values_[pred->preorder_number()]; |
| ASSERT((*pred_out_values)[place_id] != nullptr); |
| |
| // Sets of outgoing values are not linked into use lists so |
| // they might contain values that were replaced and removed |
| // from the graph by this iteration. |
| // To prevent using them we additionally mark definitions themselves |
| // as replaced and store a pointer to the replacement. |
| Definition* replacement = (*pred_out_values)[place_id]->Replacement(); |
| Value* input = new (Z) Value(replacement); |
| phi->SetInputAt(i, input); |
| replacement->AddInputUse(input); |
| // If any input is untagged, then the Phi should be marked as untagged. |
| if (replacement->representation() == kUntagged) { |
| phi->set_representation(kUntagged); |
| } |
| } |
| |
| graph_->AllocateSSAIndex(phi); |
| phis_.Add(phi); // Postpone phi insertion until after load forwarding. |
| |
| if (FLAG_support_il_printer && FLAG_trace_load_optimization && |
| graph_->should_print()) { |
| THR_Print("created pending phi %s for %s at B%" Pd "\n", phi->ToCString(), |
| aliased_set_->places()[place_id]->ToCString(), |
| block->block_id()); |
| } |
| } |
| |
| // Iterate over basic blocks and replace exposed loads with incoming |
| // values. |
| void ForwardLoads() { |
| for (BlockIterator block_it = graph_->reverse_postorder_iterator(); |
| !block_it.Done(); block_it.Advance()) { |
| BlockEntryInstr* block = block_it.Current(); |
| |
| ZoneGrowableArray<Definition*>* loads = |
| exposed_values_[block->preorder_number()]; |
| if (loads == nullptr) continue; // No exposed loads. |
| |
| BitVector* in = in_[block->preorder_number()]; |
| |
| for (intptr_t i = 0; i < loads->length(); i++) { |
| Definition* load = (*loads)[i]; |
| if (!in->Contains(GetPlaceId(load))) continue; // No incoming value. |
| |
| Definition* replacement = MergeIncomingValues(block, GetPlaceId(load)); |
| ASSERT(replacement != nullptr); |
| |
| // Sets of outgoing values are not linked into use lists so |
| // they might contain values that were replace and removed |
| // from the graph by this iteration. |
| // To prevent using them we additionally mark definitions themselves |
| // as replaced and store a pointer to the replacement. |
| replacement = replacement->Replacement(); |
| |
| if ((load != replacement) && CanForwardLoadTo(load, replacement)) { |
| graph_->EnsureSSATempIndex(load, replacement); |
| |
| if (FLAG_trace_optimization && graph_->should_print()) { |
| THR_Print("Replacing load v%" Pd " with v%" Pd "\n", |
| load->ssa_temp_index(), replacement->ssa_temp_index()); |
| } |
| |
| replacement = ReplaceLoad(load, replacement); |
| load->RemoveFromGraph(); |
| load->SetReplacement(replacement); |
| forwarded_ = true; |
| } |
| } |
| } |
| } |
| |
| // Check if the given phi take the same value on all code paths. |
| // Eliminate it as redundant if this is the case. |
| // When analyzing phi operands assumes that only generated during |
| // this load phase can be redundant. They can be distinguished because |
| // they are not marked alive. |
| // TODO(vegorov): move this into a separate phase over all phis. |
| bool EliminateRedundantPhi(PhiInstr* phi) { |
| Definition* value = nullptr; // Possible value of this phi. |
| |
| worklist_.Clear(); |
| if (in_worklist_ == nullptr) { |
| in_worklist_ = new (Z) BitVector(Z, graph_->current_ssa_temp_index()); |
| } else { |
| in_worklist_->Clear(); |
| } |
| |
| worklist_.Add(phi); |
| in_worklist_->Add(phi->ssa_temp_index()); |
| |
| for (intptr_t i = 0; i < worklist_.length(); i++) { |
| PhiInstr* phi = worklist_[i]; |
| |
| for (intptr_t i = 0; i < phi->InputCount(); i++) { |
| Definition* input = phi->InputAt(i)->definition(); |
| if (input == phi) continue; |
| |
| PhiInstr* phi_input = input->AsPhi(); |
| if ((phi_input != nullptr) && !phi_input->is_alive()) { |
| if (!in_worklist_->Contains(phi_input->ssa_temp_index())) { |
| worklist_.Add(phi_input); |
| in_worklist_->Add(phi_input->ssa_temp_index()); |
| } |
| continue; |
| } |
| |
| if (value == nullptr) { |
| value = input; |
| } else if (value != input) { |
| return false; // This phi is not redundant. |
| } |
| } |
| } |
| |
| // All phis in the worklist are redundant and have the same computed |
| // value on all code paths. |
| ASSERT(value != nullptr); |
| for (intptr_t i = 0; i < worklist_.length(); i++) { |
| worklist_[i]->ReplaceUsesWith(value); |
| } |
| |
| return true; |
| } |
| |
| // Returns true if definitions are congruent assuming their inputs |
| // are congruent. |
| bool CanBeCongruent(Definition* a, Definition* b) { |
| return (a->tag() == b->tag()) && |
| ((a->IsPhi() && (a->GetBlock() == b->GetBlock())) || |
| (a->AllowsCSE() && a->AttributesEqual(*b))); |
| } |
| |
| // Given two definitions check if they are congruent under assumption that |
| // their inputs will be proven congruent. If they are - add them to the |
| // worklist to check their inputs' congruency. |
| // Returns true if pair was added to the worklist or is already in the |
| // worklist and false if a and b are not congruent. |
| bool AddPairToCongruencyWorklist(Definition* a, Definition* b) { |
| if (!CanBeCongruent(a, b)) { |
| return false; |
| } |
| |
| // If a is already in the worklist check if it is being compared to b. |
| // Give up if it is not. |
| if (in_worklist_->Contains(a->ssa_temp_index())) { |
| for (intptr_t i = 0; i < congruency_worklist_.length(); i += 2) { |
| if (a == congruency_worklist_[i]) { |
| return (b == congruency_worklist_[i + 1]); |
| } |
| } |
| UNREACHABLE(); |
| } else if (in_worklist_->Contains(b->ssa_temp_index())) { |
| return AddPairToCongruencyWorklist(b, a); |
| } |
| |
| congruency_worklist_.Add(a); |
| congruency_worklist_.Add(b); |
| in_worklist_->Add(a->ssa_temp_index()); |
| return true; |
| } |
| |
| bool AreInputsCongruent(Definition* a, Definition* b) { |
| ASSERT(a->tag() == b->tag()); |
| ASSERT(a->InputCount() == b->InputCount()); |
| for (intptr_t j = 0; j < a->InputCount(); j++) { |
| Definition* inputA = a->InputAt(j)->definition(); |
| Definition* inputB = b->InputAt(j)->definition(); |
| |
| if (inputA != inputB) { |
| if (!AddPairToCongruencyWorklist(inputA, inputB)) { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| // Returns true if instruction dom dominates instruction other. |
| static bool Dominates(Instruction* dom, Instruction* other) { |
| BlockEntryInstr* dom_block = dom->GetBlock(); |
| BlockEntryInstr* other_block = other->GetBlock(); |
| |
| if (dom_block == other_block) { |
| for (Instruction* current = dom->next(); current != nullptr; |
| current = current->next()) { |
| if (current == other) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| return dom_block->Dominates(other_block); |
| } |
| |
| // Replace the given phi with another if they are congruent. |
| // Returns true if succeeds. |
| bool ReplacePhiWith(PhiInstr* phi, PhiInstr* replacement) { |
| ASSERT(phi->InputCount() == replacement->InputCount()); |
| ASSERT(phi->block() == replacement->block()); |
| |
| congruency_worklist_.Clear(); |
| if (in_worklist_ == nullptr) { |
| in_worklist_ = new (Z) BitVector(Z, graph_->current_ssa_temp_index()); |
| } else { |
| in_worklist_->Clear(); |
| } |
| |
| // During the comparison worklist contains pairs of definitions to be |
| // compared. |
| if (!AddPairToCongruencyWorklist(phi, replacement)) { |
| return false; |
| } |
| |
| // Process the worklist. It might grow during each comparison step. |
| for (intptr_t i = 0; i < congruency_worklist_.length(); i += 2) { |
| if (!AreInputsCongruent(congruency_worklist_[i], |
| congruency_worklist_[i + 1])) { |
| return false; |
| } |
| } |
| |
| // At this point worklist contains pairs of congruent definitions. |
| // Replace the one member of the pair with another maintaining proper |
| // domination relation between definitions and uses. |
| for (intptr_t i = 0; i < congruency_worklist_.length(); i += 2) { |
| Definition* a = congruency_worklist_[i]; |
| Definition* b = congruency_worklist_[i + 1]; |
| |
| // If these definitions are not phis then we need to pick up one |
| // that dominates another as the replacement: if a dominates b swap them. |
| // Note: both a and b are used as a phi input at the same block B which |
| // means a dominates B and b dominates B, which guarantees that either |
| // a dominates b or b dominates a. |
| if (!a->IsPhi()) { |
| if (Dominates(a, b)) { |
| Definition* t = a; |
| a = b; |
| b = t; |
| } |
| ASSERT(Dominates(b, a)); |
| } |
| |
| if (FLAG_support_il_printer && FLAG_trace_load_optimization && |
| graph_->should_print()) { |
| THR_Print("Replacing %s with congruent %s\n", a->ToCString(), |
| b->ToCString()); |
| } |
| |
| a->ReplaceUsesWith(b); |
| if (a->IsPhi()) { |
| // We might be replacing a phi introduced by the load forwarding |
| // that is not inserted in the graph yet. |
| ASSERT(b->IsPhi()); |
| PhiInstr* phi_a = a->AsPhi(); |
| if (phi_a->is_alive()) { |
| phi_a->mark_dead(); |
| phi_a->block()->RemovePhi(phi_a); |
| phi_a->UnuseAllInputs(); |
| } |
| } else { |
| a->RemoveFromGraph(); |
| } |
| } |
| |
| return true; |
| } |
| |
| // Insert the given phi into the graph. Attempt to find an equal one in the |
| // target block first. |
| // Returns true if the phi was inserted and false if it was replaced. |
| bool EmitPhi(PhiInstr* phi) { |
| for (PhiIterator it(phi->block()); !it.Done(); it.Advance()) { |
| if (ReplacePhiWith(phi, it.Current())) { |
| return false; |
| } |
| } |
| |
| phi->mark_alive(); |
| phi->block()->InsertPhi(phi); |
| return true; |
| } |
| |
| // Phis have not yet been inserted into the graph but they have uses of |
| // their inputs. Insert the non-redundant ones and clear the input uses |
| // of the redundant ones. |
| void EmitPhis() { |
| // First eliminate all redundant phis. |
| for (intptr_t i = 0; i < phis_.length(); i++) { |
| PhiInstr* phi = phis_[i]; |
| if (!phi->HasUses() || EliminateRedundantPhi(phi)) { |
| phi->UnuseAllInputs(); |
| phis_[i] = nullptr; |
| } |
| } |
| |
| // Now emit phis or replace them with equal phis already present in the |
| // graph. |
| for (intptr_t i = 0; i < phis_.length(); i++) { |
| PhiInstr* phi = phis_[i]; |
| if ((phi != nullptr) && (!phi->HasUses() || !EmitPhi(phi))) { |
| phi->UnuseAllInputs(); |
| } |
| } |
| } |
| |
| ZoneGrowableArray<Definition*>* CreateBlockOutValues() { |
| ZoneGrowableArray<Definition*>* out = |
| new (Z) ZoneGrowableArray<Definition*>(aliased_set_->max_place_id()); |
| for (intptr_t i = 0; i < aliased_set_->max_place_id(); i++) { |
| out->Add(nullptr); |
| } |
| return out; |
| } |
| |
| FlowGraph* graph_; |
| PointerSet<Place>* map_; |
| |
| // Mapping between field offsets in words and expression ids of loads from |
| // that offset. |
| AliasedSet* aliased_set_; |
| |
| // Per block sets of expression ids for loads that are: incoming (available |
| // on the entry), outgoing (available on the exit), generated and killed. |
| GrowableArray<BitVector*> in_; |
| GrowableArray<BitVector*> out_; |
| GrowableArray<BitVector*> gen_; |
| GrowableArray<BitVector*> kill_; |
| |
| // Per block list of upwards exposed loads. |
| GrowableArray<ZoneGrowableArray<Definition*>*> exposed_values_; |
| |
| // Per block mappings between expression ids and outgoing definitions that |
| // represent those ids. |
| GrowableArray<ZoneGrowableArray<Definition*>*> out_values_; |
| |
| // List of phis generated during ComputeOutValues and ForwardLoads. |
| // Some of these phis might be redundant and thus a separate pass is |
| // needed to emit only non-redundant ones. |
| GrowableArray<PhiInstr*> phis_; |
| |
| // Auxiliary worklist used by redundant phi elimination. |
| GrowableArray<PhiInstr*> worklist_; |
| GrowableArray<Definition*> congruency_worklist_; |
| BitVector* in_worklist_; |
| |
| // True if any load was eliminated. |
| bool forwarded_; |
| |
| DISALLOW_COPY_AND_ASSIGN(LoadOptimizer); |
| }; |
| |
| bool DominatorBasedCSE::Optimize(FlowGraph* graph, |
| bool run_load_optimization /* = true */) { |
| bool changed = false; |
| if (FLAG_load_cse && run_load_optimization) { |
| changed = LoadOptimizer::OptimizeGraph(graph) || changed; |
| } |
| |
| CSEInstructionSet map; |
| changed = OptimizeRecursive(graph, graph->graph_entry(), &map) || changed; |
| |
| return changed; |
| } |
| |
| bool DominatorBasedCSE::OptimizeRecursive(FlowGraph* graph, |
| BlockEntryInstr* block, |
| CSEInstructionSet* map) { |
| bool changed = false; |
| for (ForwardInstructionIterator it(block); !it.Done(); it.Advance()) { |
| Instruction* current = it.Current(); |
| if (current->AllowsCSE()) { |
| Instruction* replacement = map->Lookup(current); |
| |
| if (replacement != nullptr) { |
| // Replace current with lookup result. |
| ASSERT(replacement->AllowsCSE()); |
| graph->ReplaceCurrentInstruction(&it, current, replacement); |
| changed = true; |
| continue; |
| } |
| |
| map->Insert(current); |
| } |
| } |
| |
| // Process children in the dominator tree recursively. |
| intptr_t num_children = block->dominated_blocks().length(); |
| if (num_children != 0) { |
| graph->thread()->CheckForSafepoint(); |
| } |
| for (intptr_t i = 0; i < num_children; ++i) { |
| BlockEntryInstr* child = block->dominated_blocks()[i]; |
| if (i < num_children - 1) { |
| // Copy map. |
| CSEInstructionSet child_map(*map); |
| changed = OptimizeRecursive(graph, child, &child_map) || changed; |
| } else { |
| // Reuse map for the last child. |
| changed = OptimizeRecursive(graph, child, map) || changed; |
| } |
| } |
| return changed; |
| } |
| |
| class StoreOptimizer : public LivenessAnalysis { |
| public: |
| StoreOptimizer(FlowGraph* graph, |
| AliasedSet* aliased_set, |
| PointerSet<Place>* map) |
| : LivenessAnalysis(aliased_set->max_place_id(), graph->postorder()), |
| graph_(graph), |
| map_(map), |
| aliased_set_(aliased_set), |
| exposed_stores_(graph_->postorder().length()) { |
| const intptr_t num_blocks = graph_->postorder().length(); |
| for (intptr_t i = 0; i < num_blocks; i++) { |
| exposed_stores_.Add(nullptr); |
| } |
| } |
| |
| static void OptimizeGraph(FlowGraph* graph) { |
| ASSERT(FLAG_load_cse); |
| |
| // For now, bail out for large functions to avoid OOM situations. |
| // TODO(fschneider): Fix the memory consumption issue. |
| if (graph->is_huge_method()) { |
| return; |
| } |
| |
| PointerSet<Place> map; |
| AliasedSet* aliased_set = NumberPlaces(graph, &map, kOptimizeStores); |
| if ((aliased_set != nullptr) && !aliased_set->IsEmpty()) { |
| StoreOptimizer store_optimizer(graph, aliased_set, &map); |
| store_optimizer.Optimize(); |
| } |
| } |
| |
| private: |
| void Optimize() { |
| Analyze(); |
| if (FLAG_trace_load_optimization && graph_->should_print()) { |
| Dump(); |
| } |
| EliminateDeadStores(); |
| } |
| |
| bool CanEliminateStore(Instruction* instr) { |
| if (CompilerState::Current().is_aot()) { |
| // Tracking initializing stores is important for proper shared boxes |
| // initialization, which doesn't happen in AOT and for |
| // LowerContextAllocation optimization which creates unitialized objects |
| // which is also JIT-only currently. |
| return true; |
| } |
| switch (instr->tag()) { |
| case Instruction::kStoreField: { |
| StoreFieldInstr* store_instance = instr->AsStoreField(); |
| // Can't eliminate stores that initialize fields. |
| return !store_instance->is_initialization(); |
| } |
| case Instruction::kStoreIndexed: |
| case Instruction::kStoreStaticField: |
| return true; |
| default: |
| UNREACHABLE(); |
| return false; |
| } |
| } |
| |
| virtual void ComputeInitialSets() { |
| Zone* zone = graph_->zone(); |
| BitVector* all_places = |
| new (zone) BitVector(zone, aliased_set_->max_place_id()); |
| all_places->SetAll(); |
| |
| BitVector* all_aliased_places = nullptr; |
| if (CompilerState::Current().is_aot()) { |
| all_aliased_places = |
| new (zone) BitVector(zone, aliased_set_->max_place_id()); |
| } |
| const auto& places = aliased_set_->places(); |
| for (intptr_t i = 0; i < places.length(); i++) { |
| Place* place = places[i]; |
| if (place->DependsOnInstance()) { |
| Definition* instance = place->instance(); |
| // Find escaping places by inspecting definition allocation |
| // [AliasIdentity] field, which is populated above by |
| // [AliasedSet::ComputeAliasing]. |
| if ((all_aliased_places != nullptr) && |
| (!Place::IsAllocation(instance) || |
| instance->Identity().IsAliased())) { |
| all_aliased_places->Add(i); |
| } |
| if (instance != nullptr) { |
| // Avoid incorrect propagation of "dead" state beyond definition |
| // of the instance. Otherwise it may eventually reach stores into |
| // this place via loop backedge. |
| live_in_[instance->GetBlock()->postorder_number()]->Add(i); |
| } |
| } else { |
| if (all_aliased_places != nullptr) { |
| all_aliased_places->Add(i); |
| } |
| } |
| if (place->kind() == Place::kIndexed) { |
| Definition* index = place->index(); |
| if (index != nullptr) { |
| // Avoid incorrect propagation of "dead" state beyond definition |
| // of the index. Otherwise it may eventually reach stores into |
| // this place via loop backedge. |
| live_in_[index->GetBlock()->postorder_number()]->Add(i); |
| } |
| } |
| } |
| |
| for (BlockIterator block_it = graph_->postorder_iterator(); |
| !block_it.Done(); block_it.Advance()) { |
| BlockEntryInstr* block = block_it.Current(); |
| const intptr_t postorder_number = block->postorder_number(); |
| |
| BitVector* kill = kill_[postorder_number]; |
| BitVector* live_in = live_in_[postorder_number]; |
| BitVector* live_out = live_out_[postorder_number]; |
| |
| ZoneGrowableArray<Instruction*>* exposed_stores = nullptr; |
| |
| // Iterate backwards starting at the last instruction. |
| for (BackwardInstructionIterator instr_it(block); !instr_it.Done(); |
| instr_it.Advance()) { |
| Instruction* instr = instr_it.Current(); |
| |
| bool is_load = false; |
| bool is_store = false; |
| Place place(instr, &is_load, &is_store); |
| if (place.IsImmutableField()) { |
| // Loads/stores of final fields do not participate. |
| continue; |
| } |
| |
| // Handle stores. |
| if (is_store) { |
| if (kill->Contains(GetPlaceId(instr))) { |
| if (!live_in->Contains(GetPlaceId(instr)) && |
| CanEliminateStore(instr)) { |
| if (FLAG_trace_optimization && graph_->should_print()) { |
| THR_Print("Removing dead store to place %" Pd " in block B%" Pd |
| "\n", |
| GetPlaceId(instr), block->block_id()); |
| } |
| instr_it.RemoveCurrentFromGraph(); |
| } |
| } else if (!live_in->Contains(GetPlaceId(instr))) { |
| // Mark this store as down-ward exposed: They are the only |
| // candidates for the global store elimination. |
| if (exposed_stores == nullptr) { |
| const intptr_t kMaxExposedStoresInitialSize = 5; |
| exposed_stores = new (zone) ZoneGrowableArray<Instruction*>( |
| Utils::Minimum(kMaxExposedStoresInitialSize, |
| aliased_set_->max_place_id())); |
| } |
| exposed_stores->Add(instr); |
| } |
| // Interfering stores kill only loads from the same place. |
| kill->Add(GetPlaceId(instr)); |
| live_in->Remove(GetPlaceId(instr)); |
| continue; |
| } |
| |
| if (instr->IsThrow() || instr->IsReThrow() || instr->IsReturnBase()) { |
| // Initialize live-out for exit blocks since it won't be computed |
| // otherwise during the fixed point iteration. |
| live_out->CopyFrom(all_places); |
| } |
| |
| // Handle side effects, deoptimization and function return. |
| if (CompilerState::Current().is_aot()) { |
| if (instr->HasUnknownSideEffects() || instr->IsReturnBase()) { |
| // An instruction that returns or has unknown side effects |
| // is treated as if it loads from all places. |
| live_in->CopyFrom(all_places); |
| continue; |
| } else if (instr->MayThrow()) { |
| if (block->try_index() == kInvalidTryIndex) { |
| // Outside of a try-catch block, an instruction that may throw |
| // is only treated as if it loads from escaping places. |
| live_in->AddAll(all_aliased_places); |
| } else { |
| // Inside of a try-catch block, an instruction that may throw |
| // is treated as if it loads from all places. |
| live_in->CopyFrom(all_places); |
| } |
| continue; |
| } |
| } else { // jit |
| // Similar optimization to the above could be implemented in JIT |
| // as long as deoptimization side-effects are taken into account. |
| // Conceptually variables in deoptimization environment for |
| // "MayThrow" instructions have to be also added to the |
| // [live_in] set as they can be considered as escaping (to |
| // unoptimized code). However those deoptimization environment |
| // variables include also non-escaping(not aliased) ones, so |
| // how to deal with that needs to be figured out. |
| if (instr->HasUnknownSideEffects() || instr->CanDeoptimize() || |
| instr->MayThrow() || instr->IsReturnBase()) { |
| // Instructions that return from the function, instructions with |
| // side effects and instructions that can deoptimize are considered |
| // as loads from all places. |
| live_in->CopyFrom(all_places); |
| continue; |
| } |
| } |
| |
| // Handle loads. |
| Definition* defn = instr->AsDefinition(); |
| if ((defn != nullptr) && IsLoadEliminationCandidate(defn)) { |
| const intptr_t alias = aliased_set_->LookupAliasId(place.ToAlias()); |
| live_in->AddAll(aliased_set_->GetKilledSet(alias)); |
| continue; |
| } |
| } |
| exposed_stores_[postorder_number] = exposed_stores; |
| } |
| if (FLAG_trace_load_optimization && graph_->should_print()) { |
| Dump(); |
| THR_Print("---\n"); |
| } |
| } |
| |
| void EliminateDeadStores() { |
| // Iteration order does not matter here. |
| for (BlockIterator block_it = graph_->postorder_iterator(); |
| !block_it.Done(); block_it.Advance()) { |
| BlockEntryInstr* block = block_it.Current(); |
| const intptr_t postorder_number = block->postorder_number(); |
| |
| BitVector* live_out = live_out_[postorder_number]; |
| |
| ZoneGrowableArray<Instruction*>* exposed_stores = |
| exposed_stores_[postorder_number]; |
| if (exposed_stores == nullptr) continue; // No exposed stores. |
| |
| // Iterate over candidate stores. |
| for (intptr_t i = 0; i < exposed_stores->length(); ++i) { |
| Instruction* instr = (*exposed_stores)[i]; |
| bool is_load = false; |
| bool is_store = false; |
| Place place(instr, &is_load, &is_store); |
| ASSERT(!is_load && is_store); |
| if (place.IsImmutableField()) { |
| // Final field do not participate in dead store elimination. |
| continue; |
| } |
| // Eliminate a downward exposed store if the corresponding place is not |
| // in live-out. |
| if (!live_out->Contains(GetPlaceId(instr)) && |
| CanEliminateStore(instr)) { |
| if (FLAG_trace_optimization && graph_->should_print()) { |
| THR_Print("Removing dead store to place %" Pd " block B%" Pd "\n", |
| GetPlaceId(instr), block->block_id()); |
| } |
| instr->RemoveFromGraph(/* ignored */ false); |
| } |
| } |
| } |
| } |
| |
| FlowGraph* graph_; |
| PointerSet<Place>* map_; |
| |
| // Mapping between field offsets in words and expression ids of loads from |
| // that offset. |
| AliasedSet* aliased_set_; |
| |
| // Per block list of downward exposed stores. |
| GrowableArray<ZoneGrowableArray<Instruction*>*> exposed_stores_; |
| |
| DISALLOW_COPY_AND_ASSIGN(StoreOptimizer); |
| }; |
| |
| void DeadStoreElimination::Optimize(FlowGraph* graph) { |
| if (FLAG_dead_store_elimination && FLAG_load_cse) { |
| StoreOptimizer::OptimizeGraph(graph); |
| } |
| } |
| |
| // |
| // Allocation Sinking |
| // |
| |
| static bool IsValidLengthForAllocationSinking( |
| ArrayAllocationInstr* array_alloc) { |
| const intptr_t kMaxAllocationSinkingNumElements = 32; |
| if (!array_alloc->HasConstantNumElements()) { |
| return false; |
| } |
| const intptr_t length = array_alloc->GetConstantNumElements(); |
| return (length >= 0) && (length <= kMaxAllocationSinkingNumElements); |
| } |
| |
| // Returns true if the given instruction is an allocation that |
| // can be sunk by the Allocation Sinking pass. |
| static bool IsSupportedAllocation(Instruction* instr) { |
| return instr->IsAllocation() && |
| (!instr->IsArrayAllocation() || |
| IsValidLengthForAllocationSinking(instr->AsArrayAllocation())); |
| } |
| |
| // Check if the use is safe for allocation sinking. Allocation sinking |
| // candidates can only be used as inputs to store and allocation instructions: |
| // |
| // - any store into the allocation candidate itself is unconditionally safe |
| // as it just changes the rematerialization state of this candidate; |
| // - store into another object is only safe if the other object is |
| // an allocation candidate. |
| // - use as input to another allocation is only safe if the other allocation |
| // is a candidate. |
| // |
| // We use a simple fix-point algorithm to discover the set of valid candidates |
| // (see CollectCandidates method), that's why this IsSafeUse can operate in two |
| // modes: |
| // |
| // - optimistic, when every allocation is assumed to be an allocation |
| // sinking candidate; |
| // - strict, when only marked allocations are assumed to be allocation |
| // sinking candidates. |
| // |
| // Fix-point algorithm in CollectCandidates first collects a set of allocations |
| // optimistically and then checks each collected candidate strictly and unmarks |
| // invalid candidates transitively until only strictly valid ones remain. |
| bool AllocationSinking::IsSafeUse(Value* use, SafeUseCheck check_type) { |
| ASSERT(IsSupportedAllocation(use->definition())); |
| |
| if (use->instruction()->IsMaterializeObject()) { |
| return true; |
| } |
| |
| if (auto* const alloc = use->instruction()->AsAllocation()) { |
| return IsSupportedAllocation(alloc) && |
| ((check_type == kOptimisticCheck) || |
| alloc->Identity().IsAllocationSinkingCandidate()); |
| } |
| |
| if (auto* store = use->instruction()->AsStoreField()) { |
| if (use == store->value()) { |
| Definition* instance = store->instance()->definition(); |
| return IsSupportedAllocation(instance) && |
| ((check_type == kOptimisticCheck) || |
| instance->Identity().IsAllocationSinkingCandidate()); |
| } |
| return true; |
| } |
| |
| if (auto* store = use->instruction()->AsStoreIndexed()) { |
| if (use == store->index()) { |
| return false; |
| } |
| if (use == store->array()) { |
| if (!store->index()->BindsToSmiConstant()) { |
| return false; |
| } |
| const intptr_t index = store->index()->BoundSmiConstant(); |
| if (index < 0 || index >= use->definition() |
| ->AsArrayAllocation() |
| ->GetConstantNumElements()) { |
| return false; |
| } |
| if (auto* alloc_typed_data = use->definition()->AsAllocateTypedData()) { |
| if (store->class_id() != alloc_typed_data->class_id() || |
| !store->aligned() || |
| store->index_scale() != compiler::target::Instance::ElementSizeFor( |
| alloc_typed_data->class_id())) { |
| return false; |
| } |
| } |
| } |
| if (use == store->value()) { |
| Definition* instance = store->array()->definition(); |
| return IsSupportedAllocation(instance) && |
| ((check_type == kOptimisticCheck) || |
| instance->Identity().IsAllocationSinkingCandidate()); |
| } |
| return true; |
| } |
| |
| return false; |
| } |
| |
| // Right now we are attempting to sink allocation only into |
| // deoptimization exit. So candidate should only be used in StoreField |
| // instructions that write into fields of the allocated object. |
| bool AllocationSinking::IsAllocationSinkingCandidate(Definition* alloc, |
| SafeUseCheck check_type) { |
| for (Value* use = alloc->input_use_list(); use != nullptr; |
| use = use->next_use()) { |
| if (!IsSafeUse(use, check_type)) { |
| if (FLAG_support_il_printer && FLAG_trace_optimization && |
| flow_graph_->should_print()) { |
| THR_Print("use of %s at %s is unsafe for allocation sinking\n", |
| alloc->ToCString(), use->instruction()->ToCString()); |
| } |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| // If the given use is a store into an object then return an object we are |
| // storing into. |
| static Definition* StoreDestination(Value* use) { |
| if (auto* const alloc = use->instruction()->AsAllocation()) { |
| return alloc; |
| } |
| if (auto* const store = use->instruction()->AsStoreField()) { |
| return store->instance()->definition(); |
| } |
| if (auto* const store = use->instruction()->AsStoreIndexed()) { |
| return store->array()->definition(); |
| } |
| return nullptr; |
| } |
| |
| // If the given instruction is a load from an object, then return an object |
| // we are loading from. |
| static Definition* LoadSource(Definition* instr) { |
| if (auto load = instr->AsLoadField()) { |
| return load->instance()->definition(); |
| } |
| if (auto load = instr->AsLoadIndexed()) { |
| return load->array()->definition(); |
| } |
| return nullptr; |
| } |
| |
| // Remove the given allocation from the graph. It is not observable. |
| // If deoptimization occurs the object will be materialized. |
| void AllocationSinking::EliminateAllocation(Definition* alloc) { |
| ASSERT(IsAllocationSinkingCandidate(alloc, kStrictCheck)); |
| |
| if (FLAG_trace_optimization && flow_graph_->should_print()) { |
| THR_Print("removing allocation from the graph: v%" Pd "\n", |
| alloc->ssa_temp_index()); |
| } |
| |
| // As an allocation sinking candidate, remove stores to this candidate. |
| // Do this in a two-step process, as this allocation may be used multiple |
| // times in a single instruction (e.g., as the instance and the value in |
| // a StoreField). This means multiple entries may be removed from the |
| // use list when removing instructions, not just the current one, so |
| // Value::Iterator cannot be safely used. |
| GrowableArray<Instruction*> stores_to_remove; |
| for (Value* use = alloc->input_use_list(); use != nullptr; |
| use = use->next_use()) { |
| Instruction* const instr = use->instruction(); |
| Definition* const instance = StoreDestination(use); |
| // All uses of a candidate should be stores or other allocations. |
| ASSERT(instance != nullptr); |
| if (instance == alloc) { |
| // An allocation instruction cannot be a direct input to itself. |
| ASSERT(!instr->IsAllocation()); |
| stores_to_remove.Add(instr); |
| } else { |
| // The candidate is being stored into another candidate, either through |
| // a store instruction or as the input to a to-be-eliminated allocation, |
| // so this instruction will be removed with the other candidate. |
| ASSERT(candidates_.Contains(instance)); |
| } |
| } |
| |
| for (auto* const store : stores_to_remove) { |
| // Avoid calling RemoveFromGraph() more than once on the same instruction. |
| if (store->previous() != nullptr) { |
| store->RemoveFromGraph(); |
| } |
| } |
| |
| // There should be no environment uses. The pass replaced them with |
| // MaterializeObject instructions. |
| #ifdef DEBUG |
| for (Value* use = alloc->env_use_list(); use != nullptr; |
| use = use->next_use()) { |
| ASSERT(use->instruction()->IsMaterializeObject()); |
| } |
| #endif |
| |
| alloc->RemoveFromGraph(); |
| } |
| |
| // Find allocation instructions that can be potentially eliminated and |
| // rematerialized at deoptimization exits if needed. See IsSafeUse |
| // for the description of algorithm used below. |
| void AllocationSinking::CollectCandidates() { |
| // Optimistically collect all potential candidates. |
| for (BlockIterator block_it = flow_graph_->reverse_postorder_iterator(); |
| !block_it.Done(); block_it.Advance()) { |
| BlockEntryInstr* block = block_it.Current(); |
| for (ForwardInstructionIterator it(block); !it.Done(); it.Advance()) { |
| Instruction* current = it.Current(); |
| if (!IsSupportedAllocation(current)) { |
| continue; |
| } |
| |
| Definition* alloc = current->Cast<Definition>(); |
| if (IsAllocationSinkingCandidate(alloc, kOptimisticCheck)) { |
| alloc->SetIdentity(AliasIdentity::AllocationSinkingCandidate()); |
| candidates_.Add(alloc); |
| } |
| } |
| } |
| |
| // Transitively unmark all candidates that are not strictly valid. |
| bool changed; |
| do { |
| changed = false; |
| for (intptr_t i = 0; i < candidates_.length(); i++) { |
| Definition* alloc = candidates_[i]; |
| if (alloc->Identity().IsAllocationSinkingCandidate()) { |
| if (!IsAllocationSinkingCandidate(alloc, kStrictCheck)) { |
| alloc->SetIdentity(AliasIdentity::Unknown()); |
| changed = true; |
| } |
| } |
| } |
| } while (changed); |
| |
| // Shrink the list of candidates removing all unmarked ones. |
| intptr_t j = 0; |
| for (intptr_t i = 0; i < candidates_.length(); i++) { |
| Definition* alloc = candidates_[i]; |
| if (alloc->Identity().IsAllocationSinkingCandidate()) { |
| if (FLAG_trace_optimization && flow_graph_->should_print()) { |
| THR_Print("discovered allocation sinking candidate: v%" Pd "\n", |
| alloc->ssa_temp_index()); |
| } |
| |
| if (j != i) { |
| candidates_[j] = alloc; |
| } |
| j++; |
| } |
| } |
| candidates_.TruncateTo(j); |
| } |
| |
| // If materialization references an allocation sinking candidate then replace |
| // this reference with a materialization which should have been computed for |
| // this side-exit. CollectAllExits should have collected this exit. |
| void AllocationSinking::NormalizeMaterializations() { |
| for (intptr_t i = 0; i < candidates_.length(); i++) { |
| Definition* alloc = candidates_[i]; |
| |
| Value* next_use; |
| for (Value* use = alloc->input_use_list(); use != nullptr; use = next_use) { |
| next_use = use->next_use(); |
| if (use->instruction()->IsMaterializeObject()) { |
| use->BindTo(MaterializationFor(alloc, use->instruction())); |
| } |
| } |
| } |
| } |
| |
| // We transitively insert materializations at each deoptimization exit that |
| // might see the given allocation (see ExitsCollector). Some of these |
| // materializations are not actually used and some fail to compute because |
| // they are inserted in the block that is not dominated by the allocation. |
| // Remove unused materializations from the graph. |
| void AllocationSinking::RemoveUnusedMaterializations() { |
| intptr_t j = 0; |
| for (intptr_t i = 0; i < materializations_.length(); i++) { |
| MaterializeObjectInstr* mat = materializations_[i]; |
| if ((mat->input_use_list() == nullptr) && |
| (mat->env_use_list() == nullptr)) { |
| // Check if this materialization failed to compute and remove any |
| // unforwarded loads. There were no loads from any allocation sinking |
| // candidate in the beginning so it is safe to assume that any encountered |
| // load was inserted by CreateMaterializationAt. |
| for (intptr_t i = 0; i < mat->InputCount(); i++) { |
| Definition* load = mat->InputAt(i)->definition(); |
| if (LoadSource(load) == mat->allocation()) { |
| load->ReplaceUsesWith(flow_graph_->constant_null()); |
| load->RemoveFromGraph(); |
| } |
| } |
| mat->RemoveFromGraph(); |
| } else { |
| if (j != i) { |
| materializations_[j] = mat; |
| } |
| j++; |
| } |
| } |
| materializations_.TruncateTo(j); |
| } |
| |
| // Some candidates might stop being eligible for allocation sinking after |
| // the load forwarding because they flow into phis that load forwarding |
| // inserts. Discover such allocations and remove them from the list |
| // of allocation sinking candidates undoing all changes that we did |
| // in preparation for sinking these allocations. |
| void AllocationSinking::DiscoverFailedCandidates() { |
| // Transitively unmark all candidates that are not strictly valid. |
| bool changed; |
| do { |
| changed = false; |
| for (intptr_t i = 0; i < candidates_.length(); i++) { |
| Definition* alloc = candidates_[i]; |
| if (alloc->Identity().IsAllocationSinkingCandidate()) { |
| if (!IsAllocationSinkingCandidate(alloc, kStrictCheck)) { |
| alloc->SetIdentity(AliasIdentity::Unknown()); |
| changed = true; |
| } |
| } |
| } |
| } while (changed); |
| |
| // Remove all failed candidates from the candidates list. |
| intptr_t j = 0; |
| for (intptr_t i = 0; i < candidates_.length(); i++) { |
| Definition* alloc = candidates_[i]; |
| if (!alloc->Identity().IsAllocationSinkingCandidate()) { |
| if (FLAG_trace_optimization && flow_graph_->should_print()) { |
| THR_Print("allocation v%" Pd " can't be eliminated\n", |
| alloc->ssa_temp_index()); |
| } |
| |
| #ifdef DEBUG |
| for (Value* use = alloc->env_use_list(); use != nullptr; |
| use = use->next_use()) { |
| ASSERT(use->instruction()->IsMaterializeObject()); |
| } |
| #endif |
| |
| // All materializations will be removed from the graph. Remove inserted |
| // loads first and detach materializations from allocation's environment |
| // use list: we will reconstruct it when we start removing |
| // materializations. |
| alloc->set_env_use_list(nullptr); |
| for (Value* use = alloc->input_use_list(); use != nullptr; |
| use = use->next_use()) { |
| if (use->instruction()->IsLoadField() || |
| use->instruction()->IsLoadIndexed()) { |
| Definition* load = use->instruction()->AsDefinition(); |
| load->ReplaceUsesWith(flow_graph_->constant_null()); |
| load->RemoveFromGraph(); |
| } else { |
| ASSERT(use->instruction()->IsMaterializeObject() || |
| use->instruction()->IsPhi() || |
| use->instruction()->IsStoreField() || |
| use->instruction()->IsStoreIndexed()); |
| } |
| } |
| } else { |
| if (j != i) { |
| candidates_[j] = alloc; |
| } |
| j++; |
| } |
| } |
| |
| if (j != candidates_.length()) { // Something was removed from candidates. |
| intptr_t k = 0; |
| for (intptr_t i = 0; i < materializations_.length(); i++) { |
| MaterializeObjectInstr* mat = materializations_[i]; |
| if (!mat->allocation()->Identity().IsAllocationSinkingCandidate()) { |
| // Restore environment uses of the allocation that were replaced |
| // by this materialization and drop materialization. |
| mat->ReplaceUsesWith(mat->allocation()); |
| mat->RemoveFromGraph(); |
| } else { |
| if (k != i) { |
| materializations_[k] = mat; |
| } |
| k++; |
| } |
| } |
| materializations_.TruncateTo(k); |
| } |
| |
| candidates_.TruncateTo(j); |
| } |
| |
| void AllocationSinking::Optimize() { |
| // Allocation sinking depends on load forwarding, so give up early if load |
| // forwarding is disabled. |
| if (!FLAG_load_cse || flow_graph_->is_huge_method()) { |
| return; |
| } |
| |
| CollectCandidates(); |
| |
| // Insert MaterializeObject instructions that will describe the state of the |
| // object at all deoptimization points. Each inserted materialization looks |
| // like this (where v_0 is allocation that we are going to eliminate): |
| // v_1 <- LoadField(v_0, field_1) |
| // ... |
| // v_N <- LoadField(v_0, field_N) |
| // v_{N+1} <- MaterializeObject(field_1 = v_1, ..., field_N = v_N) |
| // |
| // For typed data objects materialization looks like this: |
| // v_1 <- LoadIndexed(v_0, index_1) |
| // ... |
| // v_N <- LoadIndexed(v_0, index_N) |
| // v_{N+1} <- MaterializeObject([index_1] = v_1, ..., [index_N] = v_N) |
| // |
| // For arrays materialization looks like this: |
| // v_1 <- LoadIndexed(v_0, index_1) |
| // ... |
| // v_N <- LoadIndexed(v_0, index_N) |
| // v_{N+1} <- LoadField(v_0, Array.type_arguments) |
| // v_{N+2} <- MaterializeObject([index_1] = v_1, ..., [index_N] = v_N, |
| // type_arguments = v_{N+1}) |
| // |
| for (intptr_t i = 0; i < candidates_.length(); i++) { |
| InsertMaterializations(candidates_[i]); |
| } |
| |
| // Run load forwarding to eliminate LoadField/LoadIndexed instructions |
| // inserted above. |
| // |
| // All loads will be successfully eliminated because: |
| // a) they use fields/constant indices and thus provide precise aliasing |
| // information |
| // b) candidate does not escape and thus its fields/elements are not |
| // affected by external effects from calls. |
| LoadOptimizer::OptimizeGraph(flow_graph_); |
| |
| NormalizeMaterializations(); |
| |
| RemoveUnusedMaterializations(); |
| |
| // If any candidates are no longer eligible for allocation sinking abort |
| // the optimization for them and undo any changes we did in preparation. |
| DiscoverFailedCandidates(); |
| |
| // At this point we have computed the state of object at each deoptimization |
| // point and we can eliminate it. Loads inserted above were forwarded so there |
| // are no uses of the allocation outside other candidates to eliminate, just |
| // as in the beginning of the pass. |
| for (intptr_t i = 0; i < candidates_.length(); i++) { |
| EliminateAllocation(candidates_[i]); |
| } |
| |
| // Process materializations and unbox their arguments: materializations |
| // are part of the environment and can materialize boxes for double/mint/simd |
| // values when needed. |
| // TODO(vegorov): handle all box types here. |
| for (intptr_t i = 0; i < materializations_.length(); i++) { |
| MaterializeObjectInstr* mat = materializations_[i]; |
| for (intptr_t j = 0; j < mat->InputCount(); j++) { |
| Definition* defn = mat->InputAt(j)->definition(); |
| if (defn->IsBox()) { |
| mat->InputAt(j)->BindTo(defn->InputAt(0)->definition()); |
| } |
| } |
| } |
| } |
| |
| // Remove materializations from the graph. Register allocator will treat them |
| // as part of the environment not as a real instruction. |
| void AllocationSinking::DetachMaterializations() { |
| for (MaterializeObjectInstr* mat : materializations_) { |
| mat->previous()->LinkTo(mat->next()); |
| mat->set_next(nullptr); |
| mat->set_previous(nullptr); |
| } |
| } |
| |
| // Add a field/offset to the list of fields if it is not yet present there. |
| static void AddSlot(ZoneGrowableArray<const Slot*>* slots, const Slot& slot) { |
| if (!slots->Contains(&slot)) { |
| slots->Add(&slot); |
| } |
| } |
| |
| // Find deoptimization exit for the given materialization assuming that all |
| // materializations are emitted right before the instruction which is a |
| // deoptimization exit. |
| static Instruction* ExitForMaterialization(MaterializeObjectInstr* mat) { |
| while (mat->next()->IsMaterializeObject()) { |
| mat = mat->next()->AsMaterializeObject(); |
| } |
| return mat->next(); |
| } |
| |
| // Given the deoptimization exit find first materialization that was inserted |
| // before it. |
| static Instruction* FirstMaterializationAt(Instruction* exit) { |
| while (exit->previous()->IsMaterializeObject()) { |
| exit = exit->previous(); |
| } |
| return exit; |
| } |
| |
| // Given the allocation and deoptimization exit try to find MaterializeObject |
| // instruction corresponding to this allocation at this exit. |
| MaterializeObjectInstr* AllocationSinking::MaterializationFor( |
| Definition* alloc, |
| Instruction* exit) { |
| if (exit->IsMaterializeObject()) { |
| exit = ExitForMaterialization(exit->AsMaterializeObject()); |
| } |
| |
| for (MaterializeObjectInstr* mat = exit->previous()->AsMaterializeObject(); |
| mat != nullptr; mat = mat->previous()->AsMaterializeObject()) { |
| if (mat->allocation() == alloc) { |
| return mat; |
| } |
| } |
| |
| return nullptr; |
| } |
| |
| static InnerPointerAccess AccessForSlotInAllocatedObject(Definition* alloc, |
| const Slot& slot) { |
| if (slot.representation() != kUntagged) { |
| return InnerPointerAccess::kNotUntagged; |
| } |
| if (!slot.may_contain_inner_pointer()) { |
| return InnerPointerAccess::kCannotBeInnerPointer; |
| } |
| if (slot.IsIdentical(Slot::PointerBase_data())) { |
| // The data field of external arrays is not unsafe, as it never contains |
| // a GC-movable address. |
| if (alloc->IsAllocateObject() && |
| IsExternalPayloadClassId(alloc->AsAllocateObject()->cls().id())) { |
| return InnerPointerAccess::kCannotBeInnerPointer; |
| } |
| } |
| return InnerPointerAccess::kMayBeInnerPointer; |
| } |
| |
| // Insert MaterializeObject instruction for the given allocation before |
| // the given instruction that can deoptimize. |
| void AllocationSinking::CreateMaterializationAt( |
| Instruction* exit, |
| Definition* alloc, |
| const ZoneGrowableArray<const Slot*>& slots) { |
| InputsArray values(slots.length()); |
| |
| // All loads should be inserted before the first materialization so that |
| // IR follows the following pattern: loads, materializations, deoptimizing |
| // instruction. |
| Instruction* load_point = FirstMaterializationAt(exit); |
| |
| // Insert load instruction for every field and element. |
| for (auto slot : slots) { |
| Definition* load = nullptr; |
| if (slot->IsArrayElement()) { |
| intptr_t array_cid, index; |
| if (alloc->IsCreateArray()) { |
| array_cid = kArrayCid; |
| index = |
| compiler::target::Array::index_at_offset(slot->offset_in_bytes()); |
| } else if (auto alloc_typed_data = alloc->AsAllocateTypedData()) { |
| array_cid = alloc_typed_data->class_id(); |
| index = slot->offset_in_bytes() / |
| compiler::target::Instance::ElementSizeFor(array_cid); |
| } else { |
| UNREACHABLE(); |
| } |
| load = new (Z) LoadIndexedInstr( |
| new (Z) Value(alloc), |
| new (Z) Value( |
| flow_graph_->GetConstant(Smi::ZoneHandle(Z, Smi::New(index)))), |
| /*index_unboxed=*/false, |
| /*index_scale=*/compiler::target::Instance::ElementSizeFor(array_cid), |
| array_cid, kAlignedAccess, DeoptId::kNone, alloc->source()); |
| } else { |
| InnerPointerAccess access = AccessForSlotInAllocatedObject(alloc, *slot); |
| ASSERT(access != InnerPointerAccess::kMayBeInnerPointer); |
| load = new (Z) |
| LoadFieldInstr(new (Z) Value(alloc), *slot, access, alloc->source()); |
| } |
| flow_graph_->InsertBefore(load_point, load, nullptr, FlowGraph::kValue); |
| values.Add(new (Z) Value(load)); |
| } |
| |
| const Class* cls = nullptr; |
| intptr_t length_or_shape = -1; |
| if (auto instr = alloc->AsAllocateObject()) { |
| cls = &(instr->cls()); |
| } else if (alloc->IsAllocateClosure()) { |
| cls = &Class::ZoneHandle( |
| flow_graph_->isolate_group()->object_store()->closure_class()); |
| } else if (auto instr = alloc->AsAllocateContext()) { |
| cls = &Class::ZoneHandle(Object::context_class()); |
| length_or_shape = instr->num_context_variables(); |
| } else if (auto instr = alloc->AsAllocateUninitializedContext()) { |
| cls = &Class::ZoneHandle(Object::context_class()); |
| length_or_shape = instr->num_context_variables(); |
| } else if (auto instr = alloc->AsCreateArray()) { |
| cls = &Class::ZoneHandle( |
| flow_graph_->isolate_group()->object_store()->array_class()); |
| length_or_shape = instr->GetConstantNumElements(); |
| } else if (auto instr = alloc->AsAllocateTypedData()) { |
| cls = &Class::ZoneHandle( |
| flow_graph_->isolate_group()->class_table()->At(instr->class_id())); |
| length_or_shape = instr->GetConstantNumElements(); |
| } else if (auto instr = alloc->AsAllocateRecord()) { |
| cls = &Class::ZoneHandle( |
| flow_graph_->isolate_group()->class_table()->At(kRecordCid)); |
| length_or_shape = instr->shape().AsInt(); |
| } else if (auto instr = alloc->AsAllocateSmallRecord()) { |
| cls = &Class::ZoneHandle( |
| flow_graph_->isolate_group()->class_table()->At(kRecordCid)); |
| length_or_shape = instr->shape().AsInt(); |
| } else { |
| UNREACHABLE(); |
| } |
| MaterializeObjectInstr* mat = new (Z) MaterializeObjectInstr( |
| alloc->AsAllocation(), *cls, length_or_shape, slots, std::move(values)); |
| |
| flow_graph_->InsertBefore(exit, mat, nullptr, FlowGraph::kValue); |
| |
| // Replace all mentions of this allocation with a newly inserted |
| // MaterializeObject instruction. |
| // We must preserve the identity: all mentions are replaced by the same |
| // materialization. |
| exit->ReplaceInEnvironment(alloc, mat); |
| |
| // Mark MaterializeObject as an environment use of this allocation. |
| // This will allow us to discover it when we are looking for deoptimization |
| // exits for another allocation that potentially flows into this one. |
| Value* val = new (Z) Value(alloc); |
| val->set_instruction(mat); |
| alloc->AddEnvUse(val); |
| |
| // Record inserted materialization. |
| materializations_.Add(mat); |
| } |
| |
| // Add given instruction to the list of the instructions if it is not yet |
| // present there. |
| template <typename T> |
| void AddInstruction(GrowableArray<T*>* list, T* value) { |
| ASSERT(!value->IsGraphEntry() && !value->IsFunctionEntry()); |
| for (intptr_t i = 0; i < list->length(); i++) { |
| if ((*list)[i] == value) { |
| return; |
| } |
| } |
| list->Add(value); |
| } |
| |
| // Transitively collect all deoptimization exits that might need this allocation |
| // rematerialized. It is not enough to collect only environment uses of this |
| // allocation because it can flow into other objects that will be |
| // dematerialized and that are referenced by deopt environments that |
| // don't contain this allocation explicitly. |
| void AllocationSinking::ExitsCollector::Collect(Definition* alloc) { |
| for (Value* use = alloc->env_use_list(); use != nullptr; |
| use = use->next_use()) { |
| if (use->instruction()->IsMaterializeObject()) { |
| AddInstruction(&exits_, ExitForMaterialization( |
| use->instruction()->AsMaterializeObject())); |
| } else { |
| AddInstruction(&exits_, use->instruction()); |
| } |
| } |
| |
| // Check if this allocation is stored into any other allocation sinking |
| // candidate and put it on worklist so that we conservatively collect all |
| // exits for that candidate as well because they potentially might see |
| // this object. |
| for (Value* use = alloc->input_use_list(); use != nullptr; |
| use = use->next_use()) { |
| Definition* obj = StoreDestination(use); |
| if ((obj != nullptr) && (obj != alloc)) { |
| AddInstruction(&worklist_, obj); |
| } |
| } |
| } |
| |
| void AllocationSinking::ExitsCollector::CollectTransitively(Definition* alloc) { |
| exits_.TruncateTo(0); |
| worklist_.TruncateTo(0); |
| |
| worklist_.Add(alloc); |
| |
| // Note: worklist potentially will grow while we are iterating over it. |
| // We are not removing allocations from the worklist not to waste space on |
| // the side maintaining BitVector of already processed allocations: worklist |
| // is expected to be very small thus linear search in it is just as efficient |
| // as a bitvector. |
| for (intptr_t i = 0; i < worklist_.length(); i++) { |
| Collect(worklist_[i]); |
| } |
| } |
| |
| static bool IsDataFieldOfTypedDataView(Definition* alloc, const Slot& slot) { |
| if (!slot.IsIdentical(Slot::PointerBase_data())) return false; |
| // Internal typed data objects use AllocateTypedData. |
| if (!alloc->IsAllocateObject()) return false; |
| auto const cid = alloc->AsAllocateObject()->cls().id(); |
| return IsTypedDataViewClassId(cid) || IsUnmodifiableTypedDataViewClassId(cid); |
| } |
| |
| void AllocationSinking::InsertMaterializations(Definition* alloc) { |
| // Collect all fields and array elements that are written for this instance. |
| auto slots = new (Z) ZoneGrowableArray<const Slot*>(5); |
| |
| for (Value* use = alloc->input_use_list(); use != nullptr; |
| use = use->next_use()) { |
| if (StoreDestination(use) == alloc) { |
| // Allocation instructions cannot be used in as inputs to themselves. |
| ASSERT(!use->instruction()->AsAllocation()); |
| if (auto store = use->instruction()->AsStoreField()) { |
| // Only add the data field slot for external arrays. For views, it is |
| // recomputed using the other fields in DeferredObject::Fill(). |
| if (!IsDataFieldOfTypedDataView(alloc, store->slot())) { |
| AddSlot(slots, store->slot()); |
| } |
| } else if (auto store = use->instruction()->AsStoreIndexed()) { |
| const intptr_t index = store->index()->BoundSmiConstant(); |
| intptr_t offset = -1; |
| if (alloc->IsCreateArray()) { |
| offset = compiler::target::Array::element_offset(index); |
| } else if (alloc->IsAllocateTypedData()) { |
| offset = index * store->index_scale(); |
| } else { |
| UNREACHABLE(); |
| } |
| AddSlot(slots, |
| Slot::GetArrayElementSlot(flow_graph_->thread(), offset)); |
| } |
| } |
| } |
| |
| if (auto* const allocation = alloc->AsAllocation()) { |
| for (intptr_t pos = 0; pos < allocation->InputCount(); pos++) { |
| if (auto* const slot = allocation->SlotForInput(pos)) { |
| // Don't add slots for immutable length slots if not already added |
| // above, as they are already represented as the number of elements in |
| // the MaterializeObjectInstr. |
| if (!slot->IsImmutableLengthSlot()) { |
| AddSlot(slots, *slot); |
| } |
| } |
| } |
| } |
| |
| // Collect all instructions that mention this object in the environment. |
| exits_collector_.CollectTransitively(alloc); |
| |
| // Insert materializations at environment uses. |
| for (intptr_t i = 0; i < exits_collector_.exits().length(); i++) { |
| CreateMaterializationAt(exits_collector_.exits()[i], alloc, *slots); |
| } |
| } |
| |
| // TryCatchAnalyzer tries to reduce the state that needs to be synchronized |
| // on entry to the catch by discovering Parameter-s which are never used |
| // or which are always constant. |
| // |
| // This analysis is similar to dead/redundant phi elimination because |
| // Parameter instructions serve as "implicit" phis. |
| // |
| // Caveat: when analyzing which Parameter-s are redundant we limit ourselves to |
| // constant values because CatchBlockEntry-s are hanging out directly from |
| // GraphEntry and thus they are only dominated by constants from GraphEntry - |
| // thus we can't replace Parameter with arbitrary Definition which is not a |
| // Constant even if we know that this Parameter is redundant and would always |
| // evaluate to that Definition. |
| class TryCatchAnalyzer : public ValueObject { |
| public: |
| explicit TryCatchAnalyzer(FlowGraph* flow_graph, bool is_aot) |
| : flow_graph_(flow_graph), |
| is_aot_(is_aot), |
| // Initial capacity is selected based on trivial examples. |
| worklist_(flow_graph, /*initial_capacity=*/10) {} |
| |
| // Run analysis and eliminate dead/redundant Parameter-s. |
| void Optimize(); |
| |
| private: |
| // In precompiled mode we can eliminate all parameters that have no real uses |
| // and subsequently clear out corresponding slots in the environments assigned |
| // to instructions that can throw an exception which would be caught by |
| // the corresponding CatchEntryBlock. |
| // |
| // Computing "dead" parameters is essentially a fixed point algorithm because |
| // Parameter value can flow into another Parameter via an environment attached |
| // to an instruction that can throw. |
| // |
| // Note: this optimization pass assumes that environment values are only |
| // used during catching, that is why it should only be used in AOT mode. |
| void OptimizeDeadParameters() { |
| ASSERT(is_aot_); |
| |
| NumberCatchEntryParameters(); |
| ComputeIncomingValues(); |
| CollectAliveParametersOrPhis(); |
| PropagateLivenessToInputs(); |
| EliminateDeadParameters(); |
| } |
| |
| static intptr_t GetParameterId(const Instruction* instr) { |
| return instr->GetPassSpecificId(CompilerPass::kTryCatchOptimization); |
| } |
| |
| static void SetParameterId(Instruction* instr, intptr_t id) { |
| instr->SetPassSpecificId(CompilerPass::kTryCatchOptimization, id); |
| } |
| |
| static bool HasParameterId(Instruction* instr) { |
| return instr->HasPassSpecificId(CompilerPass::kTryCatchOptimization); |
| } |
| |
| // Assign sequential ids to each ParameterInstr in each CatchEntryBlock. |
| // Collect reverse mapping from try indexes to corresponding catches. |
| void NumberCatchEntryParameters() { |
| for (auto catch_entry : flow_graph_->graph_entry()->catch_entries()) { |
| const GrowableArray<Definition*>& idefs = |
| *catch_entry->initial_definitions(); |
| for (auto idef : idefs) { |
| if (idef->IsParameter()) { |
| SetParameterId(idef, parameter_info_.length()); |
| parameter_info_.Add(new ParameterInfo(idef->AsParameter())); |
| } |
| } |
| |
| catch_by_index_.EnsureLength(catch_entry->catch_try_index() + 1, nullptr); |
| catch_by_index_[catch_entry->catch_try_index()] = catch_entry; |
| } |
| } |
| |
| // Compute potential incoming values for each Parameter in each catch block |
| // by looking into environments assigned to MayThrow instructions within |
| // blocks covered by the corresponding catch. |
| void ComputeIncomingValues() { |
| for (auto block : flow_graph_->reverse_postorder()) { |
| if (block->try_index() == kInvalidTryIndex) { |
| continue; |
| } |
| |
| ASSERT(block->try_index() < catch_by_index_.length()); |
| auto catch_entry = catch_by_index_[block->try_index()]; |
| const auto& idefs = *catch_entry->initial_definitions(); |
| |
| for (ForwardInstructionIterator instr_it(block); !instr_it.Done(); |
| instr_it.Advance()) { |
| Instruction* current = instr_it.Current(); |
| if (!current->MayThrow()) continue; |
| |
| Environment* env = current->env()->Outermost(); |
| ASSERT(env != nullptr); |
| |
| for (intptr_t env_idx = 0; env_idx < idefs.length(); ++env_idx) { |
| if (ParameterInstr* param = idefs[env_idx]->AsParameter()) { |
| Definition* defn = env->ValueAt(env_idx)->definition(); |
| |
| // Add defn as an incoming value to the parameter if it is not |
| // already present in the list. |
| bool found = false; |
| for (auto other_defn : |
| parameter_info_[GetParameterId(param)]->incoming) { |
| if (other_defn == defn) { |
| found = true; |
| break; |
| } |
| } |
| if (!found) { |
| parameter_info_[GetParameterId(param)]->incoming.Add(defn); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| // Find all parameters (and phis) that are definitely alive - because they |
| // have non-phi uses and place them into worklist. |
| // |
| // Note: phis that only have phi (and environment) uses would be marked as |
| // dead. |
| void CollectAliveParametersOrPhis() { |
| for (auto block : flow_graph_->reverse_postorder()) { |
| if (JoinEntryInstr* join = block->AsJoinEntry()) { |
| if (join->phis() == nullptr) continue; |
| |
| for (auto phi : *join->phis()) { |
| phi->mark_dead(); |
| if (HasActualUse(phi)) { |
| MarkLive(phi); |
| } |
| } |
| } |
| } |
| |
| for (auto info : parameter_info_) { |
| if (HasActualUse(info->instr)) { |
| MarkLive(info->instr); |
| } |
| } |
| } |
| |
| // Propagate liveness from live parameters and phis to other parameters and |
| // phis transitively. |
| void PropagateLivenessToInputs() { |
| while (!worklist_.IsEmpty()) { |
| Definition* defn = worklist_.RemoveLast(); |
| if (ParameterInstr* param = defn->AsParameter()) { |
| auto s = parameter_info_[GetParameterId(param)]; |
| for (auto input : s->incoming) { |
| MarkLive(input); |
| } |
| } else if (PhiInstr* phi = defn->AsPhi()) { |
| for (intptr_t i = 0; i < phi->InputCount(); i++) { |
| MarkLive(phi->InputAt(i)->definition()); |
| } |
| } |
| } |
| } |
| |
| // Mark definition as live if it is a dead Phi or a dead Parameter and place |
| // them into worklist. |
| void MarkLive(Definition* defn) { |
| if (PhiInstr* phi = defn->AsPhi()) { |
| if (!phi->is_alive()) { |
| phi->mark_alive(); |
| worklist_.Add(phi); |
| } |
| } else if (ParameterInstr* param = defn->AsParameter()) { |
| if (HasParameterId(param)) { |
| auto input_s = parameter_info_[GetParameterId(param)]; |
| if (!input_s->alive) { |
| input_s->alive = true; |
| worklist_.Add(param); |
| } |
| } |
| } else if (UnboxInstr* unbox = defn->AsUnbox()) { |
| MarkLive(unbox->value()->definition()); |
| } |
| } |
| |
| // Replace all dead parameters with null value and clear corresponding |
| // slots in environments. |
| void EliminateDeadParameters() { |
| for (auto info : parameter_info_) { |
| if (!info->alive) { |
| info->instr->ReplaceUsesWith(flow_graph_->constant_null()); |
| } |
| } |
| |
| for (auto block : flow_graph_->reverse_postorder()) { |
| if (block->try_index() == -1) continue; |
| |
| auto catch_entry = catch_by_index_[block->try_index()]; |
| const auto& idefs = *catch_entry->initial_definitions(); |
| |
| for (ForwardInstructionIterator instr_it(block); !instr_it.Done(); |
| instr_it.Advance()) { |
| Instruction* current = instr_it.Current(); |
| if (!current->MayThrow()) continue; |
| |
| Environment* env = current->env()->Outermost(); |
| RELEASE_ASSERT(env != nullptr); |
| |
| for (intptr_t env_idx = 0; env_idx < idefs.length(); ++env_idx) { |
| if (ParameterInstr* param = idefs[env_idx]->AsParameter()) { |
| if (!parameter_info_[GetParameterId(param)]->alive) { |
| env->ValueAt(env_idx)->BindToEnvironment( |
| flow_graph_->constant_null()); |
| } |
| } |
| } |
| } |
| } |
| |
| DeadCodeElimination::RemoveDeadAndRedundantPhisFromTheGraph(flow_graph_); |
| } |
| |
| // Returns true if definition has a use in an instruction which is not a phi. |
| // Skip over Unbox instructions which may be inserted for unused phis. |
| static bool HasActualUse(Definition* defn) { |
| for (Value* use = defn->input_use_list(); use != nullptr; |
| use = use->next_use()) { |
| Instruction* use_instruction = use->instruction(); |
| if (UnboxInstr* unbox = use_instruction->AsUnbox()) { |
| if (HasActualUse(unbox)) { |
| return true; |
| } |
| } else if (!use_instruction->IsPhi()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| struct ParameterInfo : public ZoneAllocated { |
| explicit ParameterInfo(ParameterInstr* instr) : instr(instr) {} |
| |
| ParameterInstr* instr; |
| bool alive = false; |
| GrowableArray<Definition*> incoming; |
| }; |
| |
| FlowGraph* const flow_graph_; |
| const bool is_aot_; |
| |
| // Additional information for each Parameter from each CatchBlockEntry. |
| // Parameter-s are numbered and their number is stored in |
| // Instruction::place_id() field which is otherwise not used for anything |
| // at this stage. |
| GrowableArray<ParameterInfo*> parameter_info_; |
| |
| // Mapping from catch_try_index to corresponding CatchBlockEntry-s. |
| GrowableArray<CatchBlockEntryInstr*> catch_by_index_; |
| |
| // Worklist for live Phi and Parameter instructions which need to be |
| // processed by PropagateLivenessToInputs. |
| DefinitionWorklist worklist_; |
| }; |
| |
| void OptimizeCatchEntryStates(FlowGraph* flow_graph, bool is_aot) { |
| if (flow_graph->graph_entry()->catch_entries().is_empty()) { |
| return; |
| } |
| |
| TryCatchAnalyzer analyzer(flow_graph, is_aot); |
| analyzer.Optimize(); |
| } |
| |
| void TryCatchAnalyzer::Optimize() { |
| // Analyze catch entries and remove "dead" Parameter instructions. |
| if (is_aot_) { |
| OptimizeDeadParameters(); |
| } |
| |
| // For every catch-block: Iterate over all call instructions inside the |
| // corresponding try-block and figure out for each environment value if it |
| // is the same constant at all calls. If yes, replace the initial definition |
| // at the catch-entry with this constant. |
| const GrowableArray<CatchBlockEntryInstr*>& catch_entries = |
| flow_graph_->graph_entry()->catch_entries(); |
| |
| for (auto catch_entry : catch_entries) { |
| // Initialize cdefs with the original initial definitions (ParameterInstr). |
| // The following representation is used: |
| // ParameterInstr => unknown |
| // ConstantInstr => known constant |
| // nullptr => non-constant |
| GrowableArray<Definition*>* idefs = catch_entry->initial_definitions(); |
| GrowableArray<Definition*> cdefs(idefs->length()); |
| cdefs.AddArray(*idefs); |
| |
| // exception_var and stacktrace_var are never constant. In asynchronous or |
| // generator functions they may be context-allocated in which case they are |
| // not tracked in the environment anyway. |
| |
| cdefs[flow_graph_->EnvIndex(catch_entry->raw_exception_var())] = nullptr; |
| cdefs[flow_graph_->EnvIndex(catch_entry->raw_stacktrace_var())] = nullptr; |
| |
| for (BlockIterator block_it = flow_graph_->reverse_postorder_iterator(); |
| !block_it.Done(); block_it.Advance()) { |
| BlockEntryInstr* block = block_it.Current(); |
| if (block->try_index() == catch_entry->catch_try_index()) { |
| for (ForwardInstructionIterator instr_it(block); !instr_it.Done(); |
| instr_it.Advance()) { |
| Instruction* current = instr_it.Current(); |
| if (current->MayThrow()) { |
| Environment* env = current->env()->Outermost(); |
| ASSERT(env != nullptr); |
| for (intptr_t env_idx = 0; env_idx < cdefs.length(); ++env_idx) { |
| if (cdefs[env_idx] != nullptr && !cdefs[env_idx]->IsConstant() && |
| env->ValueAt(env_idx)->BindsToConstant()) { |
| // If the recorded definition is not a constant, record this |
| // definition as the current constant definition. |
| cdefs[env_idx] = env->ValueAt(env_idx)->definition(); |
| } |
| if (cdefs[env_idx] != env->ValueAt(env_idx)->definition()) { |
| // Non-constant definitions are reset to nullptr. |
| cdefs[env_idx] = nullptr; |
| } |
| } |
| } |
| } |
| } |
| } |
| for (intptr_t j = 0; j < idefs->length(); ++j) { |
| if (cdefs[j] != nullptr && cdefs[j]->IsConstant()) { |
| Definition* old = (*idefs)[j]; |
| ConstantInstr* orig = cdefs[j]->AsConstant(); |
| ConstantInstr* copy = |
| new (flow_graph_->zone()) ConstantInstr(orig->value()); |
| flow_graph_->AllocateSSAIndex(copy); |
| old->ReplaceUsesWith(copy); |
| copy->set_previous(old->previous()); // partial link |
| (*idefs)[j] = copy; |
| } |
| } |
| } |
| } |
| |
| // Returns true iff this definition is used in a non-phi instruction. |
| static bool HasRealUse(Definition* def) { |
| // Environment uses are real (non-phi) uses. |
| if (def->env_use_list() != nullptr) return true; |
| |
| for (Value::Iterator it(def->input_use_list()); !it.Done(); it.Advance()) { |
| if (!it.Current()->instruction()->IsPhi()) return true; |
| } |
| return false; |
| } |
| |
| void DeadCodeElimination::EliminateDeadPhis(FlowGraph* flow_graph) { |
| GrowableArray<PhiInstr*> live_phis; |
| for (BlockIterator b = flow_graph->postorder_iterator(); !b.Done(); |
| b.Advance()) { |
| JoinEntryInstr* join = b.Current()->AsJoinEntry(); |
| if (join != nullptr) { |
| for (PhiIterator it(join); !it.Done(); it.Advance()) { |
| PhiInstr* phi = it.Current(); |
| // Phis that have uses and phis inside try blocks are |
| // marked as live. |
| if (HasRealUse(phi)) { |
| live_phis.Add(phi); |
| phi->mark_alive(); |
| } else { |
| phi->mark_dead(); |
| } |
| } |
| } |
| } |
| |
| while (!live_phis.is_empty()) { |
| PhiInstr* phi = live_phis.RemoveLast(); |
| for (intptr_t i = 0; i < phi->InputCount(); i++) { |
| Value* val = phi->InputAt(i); |
| PhiInstr* used_phi = val->definition()->AsPhi(); |
| if ((used_phi != nullptr) && !used_phi->is_alive()) { |
| used_phi->mark_alive(); |
| live_phis.Add(used_phi); |
| } |
| } |
| } |
| |
| RemoveDeadAndRedundantPhisFromTheGraph(flow_graph); |
| } |
| |
| void DeadCodeElimination::RemoveDeadAndRedundantPhisFromTheGraph( |
| FlowGraph* flow_graph) { |
| for (auto block : flow_graph->postorder()) { |
| if (JoinEntryInstr* join = block->AsJoinEntry()) { |
| if (join->phis_ == nullptr) continue; |
| |
| // Eliminate dead phis and compact the phis_ array of the block. |
| intptr_t to_index = 0; |
| for (intptr_t i = 0; i < join->phis_->length(); ++i) { |
| PhiInstr* phi = (*join->phis_)[i]; |
| if (phi != nullptr) { |
| if (!phi->is_alive()) { |
| phi->ReplaceUsesWith(flow_graph->constant_null()); |
| phi->UnuseAllInputs(); |
| (*join->phis_)[i] = nullptr; |
| if (FLAG_trace_optimization && flow_graph->should_print()) { |
| THR_Print("Removing dead phi v%" Pd "\n", phi->ssa_temp_index()); |
| } |
| } else { |
| (*join->phis_)[to_index++] = phi; |
| } |
| } |
| } |
| if (to_index == 0) { |
| join->phis_ = nullptr; |
| } else { |
| join->phis_->TruncateTo(to_index); |
| } |
| } |
| } |
| } |
| |
| void DeadCodeElimination::EliminateDeadCode(FlowGraph* flow_graph) { |
| GrowableArray<Instruction*> worklist; |
| BitVector live(flow_graph->zone(), flow_graph->current_ssa_temp_index()); |
| |
| // Mark all instructions with side-effects as live. |
| for (BlockIterator block_it = flow_graph->reverse_postorder_iterator(); |
| !block_it.Done(); block_it.Advance()) { |
| BlockEntryInstr* block = block_it.Current(); |
| for (ForwardInstructionIterator it(block); !it.Done(); it.Advance()) { |
| Instruction* current = it.Current(); |
| ASSERT(!current->IsMoveArgument()); |
| // TODO(alexmarkov): take control dependencies into account and |
| // eliminate dead branches/conditions. |
| if (!current->CanEliminate(block)) { |
| worklist.Add(current); |
| if (Definition* def = current->AsDefinition()) { |
| if (def->HasSSATemp()) { |
| live.Add(def->ssa_temp_index()); |
| } |
| } |
| } |
| } |
| } |
| |
| // Iteratively follow inputs of instructions in the work list. |
| while (!worklist.is_empty()) { |
| Instruction* current = worklist.RemoveLast(); |
| for (intptr_t i = 0, n = current->InputCount(); i < n; ++i) { |
| Definition* input = current->InputAt(i)->definition(); |
| ASSERT(input->HasSSATemp()); |
| if (!live.Contains(input->ssa_temp_index())) { |
| worklist.Add(input); |
| live.Add(input->ssa_temp_index()); |
| } |
| } |
| for (intptr_t i = 0, n = current->ArgumentCount(); i < n; ++i) { |
| Definition* input = current->ArgumentAt(i); |
| ASSERT(input->HasSSATemp()); |
| if (!live.Contains(input->ssa_temp_index())) { |
| worklist.Add(input); |
| live.Add(input->ssa_temp_index()); |
| } |
| } |
| if (current->env() != nullptr) { |
| for (Environment::DeepIterator it(current->env()); !it.Done(); |
| it.Advance()) { |
| Definition* input = it.CurrentValue()->definition(); |
| ASSERT(!input->IsMoveArgument()); |
| if (input->HasSSATemp() && !live.Contains(input->ssa_temp_index())) { |
| worklist.Add(input); |
| live.Add(input->ssa_temp_index()); |
| } |
| } |
| } |
| } |
| |
| // Remove all instructions which are not marked as live. |
| for (BlockIterator block_it = flow_graph->reverse_postorder_iterator(); |
| !block_it.Done(); block_it.Advance()) { |
| BlockEntryInstr* block = block_it.Current(); |
| if (JoinEntryInstr* join = block->AsJoinEntry()) { |
| for (PhiIterator it(join); !it.Done(); it.Advance()) { |
| PhiInstr* current = it.Current(); |
| if (!live.Contains(current->ssa_temp_index())) { |
| it.RemoveCurrentFromGraph(); |
| } |
| } |
| } |
| for (ForwardInstructionIterator it(block); !it.Done(); it.Advance()) { |
| Instruction* current = it.Current(); |
| if (!current->CanEliminate(block)) { |
| continue; |
| } |
| ASSERT(!current->IsMoveArgument()); |
| ASSERT((current->ArgumentCount() == 0) || !current->HasMoveArguments()); |
| if (Definition* def = current->AsDefinition()) { |
| if (def->HasSSATemp() && live.Contains(def->ssa_temp_index())) { |
| continue; |
| } |
| } |
| it.RemoveCurrentFromGraph(); |
| } |
| } |
| } |
| |
| // Returns true if function is marked with vm:unsafe:no-interrupts pragma. |
| static bool IsMarkedWithNoInterrupts(const Function& function) { |
| Object& options = Object::Handle(); |
| return Library::FindPragma(dart::Thread::Current(), |
| /*only_core=*/false, function, |
| Symbols::vm_unsafe_no_interrupts(), |
| /*multiple=*/false, &options); |
| } |
| |
| void CheckStackOverflowElimination::EliminateStackOverflow(FlowGraph* graph) { |
| const bool should_remove_all = IsMarkedWithNoInterrupts(graph->function()); |
| if (should_remove_all) { |
| for (auto entry : graph->reverse_postorder()) { |
| for (ForwardInstructionIterator it(entry); !it.Done(); it.Advance()) { |
| if (it.Current()->IsCheckStackOverflow()) { |
| it.RemoveCurrentFromGraph(); |
| } |
| } |
| } |
| return; |
| } |
| |
| CheckStackOverflowInstr* first_stack_overflow_instr = nullptr; |
| for (auto entry : graph->reverse_postorder()) { |
| for (ForwardInstructionIterator it(entry); !it.Done(); it.Advance()) { |
| Instruction* current = it.Current(); |
| |
| if (CheckStackOverflowInstr* instr = current->AsCheckStackOverflow()) { |
| if (first_stack_overflow_instr == nullptr) { |
| first_stack_overflow_instr = instr; |
| ASSERT(!first_stack_overflow_instr->in_loop()); |
| } |
| continue; |
| } |
| |
| if (current->IsBranch()) { |
| current = current->AsBranch()->comparison(); |
| } |
| |
| if (current->HasUnknownSideEffects()) { |
| return; |
| } |
| } |
| } |
| |
| if (first_stack_overflow_instr != nullptr) { |
| first_stack_overflow_instr->RemoveFromGraph(); |
| } |
| } |
| |
| } // namespace dart |