[compiler] eliminate quadratic behavior of store-store elimination in straight-line code
Bug: v8:13247 Change-Id: Ia1e82ef106914481e20076ac1ada9ba79e23c5a2 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3865547 Reviewed-by: Patrick Thier <pthier@chromium.org> Auto-Submit: Tobias Tebbi <tebbi@chromium.org> Commit-Queue: Patrick Thier <pthier@chromium.org> Commit-Queue: Tobias Tebbi <tebbi@chromium.org> Cr-Commit-Position: refs/heads/main@{#82902}
This commit is contained in:
parent
a55ecfaf7f
commit
eed7eaff06
@ -15,6 +15,14 @@ namespace v8 {
|
||||
namespace internal {
|
||||
namespace compiler {
|
||||
|
||||
// A fast and possibly incomplete equality check. If it returns false, the
|
||||
// values are certainly not equal, otherwise we do not know. The template is
|
||||
// intended to be specialized for types with expensive equality checks.
|
||||
template <class T>
|
||||
struct may_be_unequal {
|
||||
bool operator()(const T& a, const T& b) { return a != b; }
|
||||
};
|
||||
|
||||
// PersistentMap is a persistent map datastructure based on hash trees (a binary
|
||||
// tree using the bits of a hash value as addresses). The map is a conceptually
|
||||
// infinite: All keys are initially mapped to a default value, values are
|
||||
@ -55,6 +63,8 @@ class PersistentMap {
|
||||
|
||||
struct FocusedTree;
|
||||
|
||||
friend struct may_be_unequal<PersistentMap<Key, Value, Hasher>>;
|
||||
|
||||
public:
|
||||
// Depth of the last added element. This is a cheap estimate for the size of
|
||||
// the hash tree.
|
||||
@ -74,6 +84,10 @@ class PersistentMap {
|
||||
|
||||
// Add or overwrite an existing key-value pair.
|
||||
void Set(Key key, Value value);
|
||||
// Modify an entry in-place, avoiding repeated search.
|
||||
// `F` is a functional that expects a `Value*` parameter to modify it.
|
||||
template <class F>
|
||||
void Modify(Key key, F f);
|
||||
|
||||
bool operator==(const PersistentMap& other) const {
|
||||
if (tree_ == other.tree_) return true;
|
||||
@ -154,6 +168,14 @@ class PersistentMap {
|
||||
Zone* zone_;
|
||||
};
|
||||
|
||||
template <class Key, class Value, class Hasher>
|
||||
struct may_be_unequal<PersistentMap<Key, Value, Hasher>> {
|
||||
bool operator()(const PersistentMap<Key, Value, Hasher>& a,
|
||||
const PersistentMap<Key, Value, Hasher>& b) {
|
||||
return a.tree_ != b.tree_;
|
||||
}
|
||||
};
|
||||
|
||||
// This structure represents a hash tree with one focused path to a specific
|
||||
// leaf. For the focused leaf, it stores key, value and key hash. The path is
|
||||
// defined by the hash bits of the focused leaf. In a traditional tree
|
||||
@ -375,13 +397,23 @@ class PersistentMap<Key, Value, Hasher>::double_iterator {
|
||||
};
|
||||
|
||||
template <class Key, class Value, class Hasher>
|
||||
void PersistentMap<Key, Value, Hasher>::Set(Key key, Value value) {
|
||||
void PersistentMap<Key, Value, Hasher>::Set(Key key, Value new_value) {
|
||||
Modify(key, [&](Value* value) { *value = std::move(new_value); });
|
||||
}
|
||||
|
||||
template <class Key, class Value, class Hasher>
|
||||
template <class F>
|
||||
void PersistentMap<Key, Value, Hasher>::Modify(Key key, F f) {
|
||||
static_assert(std::is_void_v<decltype(f(std::declval<Value*>()))>);
|
||||
HashValue key_hash = HashValue(Hasher()(key));
|
||||
std::array<const FocusedTree*, kHashBits> path;
|
||||
int length = 0;
|
||||
const FocusedTree* old = FindHash(key_hash, &path, &length);
|
||||
ZoneMap<Key, Value>* more = nullptr;
|
||||
if (!(GetFocusedValue(old, key) != value)) return;
|
||||
const Value& old_value = GetFocusedValue(old, key);
|
||||
Value new_value = old_value;
|
||||
f(&new_value);
|
||||
if (!may_be_unequal<Value>()(old_value, new_value)) return;
|
||||
if (old && !(old->more == nullptr && old->key_value.key() == key)) {
|
||||
more = zone_->New<ZoneMap<Key, Value>>(zone_);
|
||||
if (old->more) {
|
||||
@ -391,12 +423,12 @@ void PersistentMap<Key, Value, Hasher>::Set(Key key, Value value) {
|
||||
more->emplace(old->key_value.key(), old->key_value.value());
|
||||
}
|
||||
more->erase(key);
|
||||
more->emplace(key, value);
|
||||
more->emplace(key, new_value);
|
||||
}
|
||||
size_t size = sizeof(FocusedTree) +
|
||||
std::max(0, length - 1) * sizeof(const FocusedTree*);
|
||||
FocusedTree* tree = new (zone_->Allocate<FocusedTree>(size))
|
||||
FocusedTree{KeyValue(std::move(key), std::move(value)),
|
||||
FocusedTree{KeyValue(std::move(key), std::move(new_value)),
|
||||
static_cast<int8_t>(length),
|
||||
key_hash,
|
||||
more,
|
||||
|
@ -46,24 +46,6 @@ namespace {
|
||||
|
||||
using StoreOffset = uint32_t;
|
||||
|
||||
struct UnobservableStore {
|
||||
NodeId id_;
|
||||
StoreOffset offset_;
|
||||
bool maybe_gc_observable_ = false;
|
||||
|
||||
bool operator==(const UnobservableStore other) const {
|
||||
return (id_ == other.id_) && (offset_ == other.offset_);
|
||||
}
|
||||
|
||||
bool operator<(const UnobservableStore other) const {
|
||||
return (id_ < other.id_) || (id_ == other.id_ && offset_ < other.offset_);
|
||||
}
|
||||
};
|
||||
|
||||
size_t hash_value(const UnobservableStore& p) {
|
||||
return base::hash_combine(p.id_, p.offset_);
|
||||
}
|
||||
|
||||
// Instances of UnobservablesSet are immutable. They represent either a set of
|
||||
// UnobservableStores, or the "unvisited empty set".
|
||||
//
|
||||
@ -75,19 +57,9 @@ size_t hash_value(const UnobservableStore& p) {
|
||||
// space in the zone in the case of non-unvisited UnobservablesSets. Copying
|
||||
// an UnobservablesSet allocates no memory.
|
||||
class UnobservablesSet final {
|
||||
private:
|
||||
enum ObservableState {
|
||||
kObservable = 0, // We haven't seen a store to this offset before.
|
||||
kUnobservable = 1, // Stores to the same offset can be eliminated.
|
||||
kGCObservable = 2 // Stores to the same offset can only be eliminated,
|
||||
// if they are not initializing or transitioning.
|
||||
};
|
||||
|
||||
using KeyT = UnobservableStore;
|
||||
using ValueT = ObservableState;
|
||||
|
||||
public:
|
||||
using SetT = PersistentMap<KeyT, ValueT>;
|
||||
using InnerSetT = PersistentMap<NodeId, bool>;
|
||||
using SetT = PersistentMap<StoreOffset, InnerSetT>;
|
||||
|
||||
// Creates a new UnobservablesSet, with the null set.
|
||||
static UnobservablesSet Unvisited() { return UnobservablesSet(); }
|
||||
@ -99,10 +71,6 @@ class UnobservablesSet final {
|
||||
UnobservablesSet& operator=(const UnobservablesSet& other)
|
||||
V8_NOEXCEPT = default;
|
||||
|
||||
// Computes the intersection of two states.
|
||||
ObservableState Intersect(const ObservableState state1,
|
||||
const ObservableState state2) const;
|
||||
|
||||
// Computes the intersection of two UnobservablesSets. If one of the sets is
|
||||
// empty, will return empty.
|
||||
UnobservablesSet Intersect(const UnobservablesSet& other,
|
||||
@ -111,13 +79,11 @@ class UnobservablesSet final {
|
||||
// Returns a set that it is the current one, plus the observation obs passed
|
||||
// as parameter. If said obs it's already unobservable, we don't have to
|
||||
// create a new one.
|
||||
UnobservablesSet Add(UnobservableStore obs, Zone* zone) const;
|
||||
UnobservablesSet Add(NodeId node, StoreOffset offset, Zone* zone) const;
|
||||
|
||||
// Returns a set that it is the current one, except for all of the
|
||||
// observations with offset off. This is done by creating a new set and
|
||||
// copying all observations with different offsets.
|
||||
// This can probably be done better if the observations are stored first by
|
||||
// offset and then by node.
|
||||
// We are removing all nodes with offset off since different nodes may
|
||||
// alias one another, and currently we don't have the means to know if
|
||||
// two nodes are definitely the same value.
|
||||
@ -127,11 +93,14 @@ class UnobservablesSet final {
|
||||
// by GC.
|
||||
UnobservablesSet MarkGCObservable(Zone* zone) const;
|
||||
|
||||
const SetT* set() const { return set_; }
|
||||
|
||||
bool IsUnvisited() const { return set_ == nullptr; }
|
||||
bool IsUnvisited() const {
|
||||
DCHECK_EQ(unobservable_ == nullptr, gc_observable_ == nullptr);
|
||||
return unobservable_ == nullptr;
|
||||
}
|
||||
bool IsEmpty() const {
|
||||
return set_ == nullptr || set_->begin() == set_->end();
|
||||
if (IsUnvisited()) return true;
|
||||
return unobservable_->begin() == unobservable_->end() &&
|
||||
gc_observable_->begin() == gc_observable_->end();
|
||||
}
|
||||
|
||||
// We need to guarantee that objects are fully initialized and fields are in
|
||||
@ -139,23 +108,19 @@ class UnobservablesSet final {
|
||||
// Therefore initializing or transitioning stores are observable if they are
|
||||
// observable by GC. All other stores are not relevant for correct GC
|
||||
// behaviour and can be eliminated even if they are observable by GC.
|
||||
bool IsUnobservable(UnobservableStore obs,
|
||||
bool IsUnobservable(NodeId node, StoreOffset offset,
|
||||
bool maybe_initializing_or_transitioning) const {
|
||||
if (set_ == nullptr) return false;
|
||||
ObservableState state = set_->Get(obs);
|
||||
switch (state) {
|
||||
case kUnobservable:
|
||||
return true;
|
||||
case kObservable:
|
||||
return false;
|
||||
case kGCObservable:
|
||||
return !maybe_initializing_or_transitioning;
|
||||
if (unobservable_ != nullptr && unobservable_->Get(offset).Get(node)) {
|
||||
return true;
|
||||
}
|
||||
UNREACHABLE();
|
||||
if (!maybe_initializing_or_transitioning && IsGCObservable(node, offset)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsGCObservable(UnobservableStore obs) const {
|
||||
return set_ != nullptr && set_->Get(obs) == kGCObservable;
|
||||
bool IsGCObservable(NodeId node, StoreOffset offset) const {
|
||||
return gc_observable_ != nullptr && gc_observable_->Get(offset).Get(node);
|
||||
}
|
||||
|
||||
bool operator==(const UnobservablesSet& other) const {
|
||||
@ -163,7 +128,8 @@ class UnobservablesSet final {
|
||||
return IsEmpty() && other.IsEmpty();
|
||||
} else {
|
||||
// Both pointers guaranteed not to be nullptrs.
|
||||
return *set() == *(other.set());
|
||||
return *unobservable_ == *(other.unobservable_) &&
|
||||
*gc_observable_ == *other.gc_observable_;
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,23 +139,15 @@ class UnobservablesSet final {
|
||||
|
||||
private:
|
||||
UnobservablesSet() = default;
|
||||
explicit UnobservablesSet(const SetT* set) : set_(set) {}
|
||||
explicit UnobservablesSet(const SetT* unobservable, const SetT* gc_observable)
|
||||
: unobservable_(unobservable), gc_observable_(gc_observable) {}
|
||||
|
||||
static SetT* NewSet(Zone* zone) {
|
||||
return zone->New<UnobservablesSet::SetT>(zone, kObservable);
|
||||
return zone->New<UnobservablesSet::SetT>(zone, InnerSetT(zone));
|
||||
}
|
||||
|
||||
static void SetAdd(SetT* set, const KeyT& key) {
|
||||
set->Set(key, kUnobservable);
|
||||
}
|
||||
static void SetErase(SetT* set, const KeyT& key) {
|
||||
set->Set(key, kObservable);
|
||||
}
|
||||
static void SetGCObservable(SetT* set, const KeyT& key) {
|
||||
set->Set(key, kGCObservable);
|
||||
}
|
||||
|
||||
const SetT* set_ = nullptr;
|
||||
const SetT* unobservable_ = nullptr;
|
||||
const SetT* gc_observable_ = nullptr;
|
||||
};
|
||||
|
||||
class RedundantStoreFinder final {
|
||||
@ -316,9 +274,9 @@ UnobservablesSet RedundantStoreFinder::RecomputeSet(
|
||||
const FieldAccess& access = FieldAccessOf(node->op());
|
||||
StoreOffset offset = ToOffset(access);
|
||||
|
||||
UnobservableStore observation = {stored_to->id(), offset};
|
||||
const bool is_not_observable = uses.IsUnobservable(
|
||||
observation, access.maybe_initializing_or_transitioning_store);
|
||||
const bool is_not_observable =
|
||||
uses.IsUnobservable(stored_to->id(), offset,
|
||||
access.maybe_initializing_or_transitioning_store);
|
||||
|
||||
if (is_not_observable) {
|
||||
TRACE(" #%d is StoreField[+%d,%s](#%d), unobservable", node->id(),
|
||||
@ -329,7 +287,7 @@ UnobservablesSet RedundantStoreFinder::RecomputeSet(
|
||||
} else {
|
||||
const bool is_gc_observable =
|
||||
access.maybe_initializing_or_transitioning_store &&
|
||||
uses.IsGCObservable(observation);
|
||||
uses.IsGCObservable(stored_to->id(), offset);
|
||||
// A GC observable store could have been unobservable in a previous
|
||||
// visit. This is possible if the node that previously shadowed the
|
||||
// initializing store is now unobservable, due to additional stores
|
||||
@ -350,7 +308,7 @@ UnobservablesSet RedundantStoreFinder::RecomputeSet(
|
||||
node->id(), offset,
|
||||
MachineReprToString(access.machine_type.representation()),
|
||||
stored_to->id(), is_gc_observable ? " by GC" : "");
|
||||
return uses.Add(observation, temp_zone());
|
||||
return uses.Add(stored_to->id(), offset, temp_zone());
|
||||
}
|
||||
}
|
||||
case IrOpcode::kLoadField: {
|
||||
@ -503,17 +461,7 @@ UnobservablesSet RedundantStoreFinder::RecomputeUseIntersection(Node* node) {
|
||||
}
|
||||
|
||||
UnobservablesSet UnobservablesSet::VisitedEmpty(Zone* zone) {
|
||||
return UnobservablesSet(NewSet(zone));
|
||||
}
|
||||
|
||||
UnobservablesSet::ObservableState UnobservablesSet::Intersect(
|
||||
const ObservableState state1, const ObservableState state2) const {
|
||||
if (state1 == state2) return state1;
|
||||
if (state1 == kObservable || state2 == kObservable) return kObservable;
|
||||
if (state1 == kGCObservable || state2 == kGCObservable) {
|
||||
return kGCObservable;
|
||||
}
|
||||
UNREACHABLE();
|
||||
return UnobservablesSet(NewSet(zone), NewSet(zone));
|
||||
}
|
||||
|
||||
UnobservablesSet UnobservablesSet::Intersect(const UnobservablesSet& other,
|
||||
@ -521,52 +469,73 @@ UnobservablesSet UnobservablesSet::Intersect(const UnobservablesSet& other,
|
||||
Zone* zone) const {
|
||||
if (IsEmpty() || other.IsEmpty()) return empty;
|
||||
|
||||
UnobservablesSet::SetT* intersection = NewSet(zone);
|
||||
for (auto triple : set()->Zip(*other.set())) {
|
||||
ObservableState new_state =
|
||||
Intersect(std::get<1>(triple), std::get<2>(triple));
|
||||
intersection->Set(std::get<0>(triple), new_state);
|
||||
UnobservablesSet::SetT* new_unobservable = NewSet(zone);
|
||||
UnobservablesSet::SetT* new_gc_observable = NewSet(zone);
|
||||
|
||||
for (auto [offset, inner_unobservable1, inner_unobservable2] :
|
||||
unobservable_->Zip(*other.unobservable_)) {
|
||||
for (auto [node, value1, value2] :
|
||||
inner_unobservable1.Zip(inner_unobservable2)) {
|
||||
if (value1 && value2) {
|
||||
new_unobservable->Modify(offset, [node = node](InnerSetT* inner) {
|
||||
inner->Set(node, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return UnobservablesSet(intersection);
|
||||
for (auto [offset, inner_gc_observable1, inner_gc_observable2] :
|
||||
gc_observable_->Zip(*other.gc_observable_)) {
|
||||
for (auto [node, value, other_value] :
|
||||
inner_gc_observable1.Zip(inner_gc_observable2)) {
|
||||
if ((value || unobservable_->Get(offset).Get(node)) &&
|
||||
(other_value || other.unobservable_->Get(offset).Get(node))) {
|
||||
new_gc_observable->Modify(offset, [node = node](InnerSetT* inner) {
|
||||
inner->Set(node, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return UnobservablesSet(new_unobservable, new_gc_observable);
|
||||
}
|
||||
|
||||
UnobservablesSet UnobservablesSet::Add(UnobservableStore obs,
|
||||
UnobservablesSet UnobservablesSet::Add(NodeId node, StoreOffset offset,
|
||||
Zone* zone) const {
|
||||
if (set()->Get(obs) == kUnobservable) return *this;
|
||||
if (unobservable_->Get(offset).Get(node)) return *this;
|
||||
|
||||
UnobservablesSet::SetT* new_set = NewSet(zone);
|
||||
*new_set = *set();
|
||||
SetAdd(new_set, obs);
|
||||
UnobservablesSet::SetT* new_unobservable = zone->New<SetT>(*unobservable_);
|
||||
new_unobservable->Modify(
|
||||
offset, [node](auto inner_set) { inner_set->Set(node, true); });
|
||||
|
||||
return UnobservablesSet(new_set);
|
||||
return UnobservablesSet(new_unobservable, gc_observable_);
|
||||
}
|
||||
|
||||
UnobservablesSet UnobservablesSet::RemoveSameOffset(StoreOffset offset,
|
||||
Zone* zone) const {
|
||||
UnobservablesSet::SetT* new_set = NewSet(zone);
|
||||
*new_set = *set();
|
||||
UnobservablesSet::SetT* new_unobservable = zone->New<SetT>(*unobservable_);
|
||||
new_unobservable->Set(offset, InnerSetT(zone));
|
||||
|
||||
// Remove elements with the given offset.
|
||||
for (auto entry : *new_set) {
|
||||
const UnobservableStore& obs = entry.first;
|
||||
if (obs.offset_ == offset) SetErase(new_set, obs);
|
||||
}
|
||||
UnobservablesSet::SetT* new_gc_observable = zone->New<SetT>(*gc_observable_);
|
||||
new_gc_observable->Set(offset, InnerSetT(zone));
|
||||
|
||||
return UnobservablesSet(new_set);
|
||||
return UnobservablesSet(new_unobservable, new_gc_observable);
|
||||
}
|
||||
|
||||
UnobservablesSet UnobservablesSet::MarkGCObservable(Zone* zone) const {
|
||||
UnobservablesSet::SetT* new_set = NewSet(zone);
|
||||
*new_set = *set();
|
||||
UnobservablesSet::SetT* new_gc_observable = zone->New<SetT>(*gc_observable_);
|
||||
|
||||
// Mark all elements as observable by GC.
|
||||
for (auto entry : *new_set) {
|
||||
const UnobservableStore& obs = entry.first;
|
||||
SetGCObservable(new_set, obs);
|
||||
for (auto [offset, inner_unobservable] : *unobservable_) {
|
||||
new_gc_observable->Modify(offset, [inner_unobservable = inner_unobservable](
|
||||
InnerSetT* inner_gc_observable) {
|
||||
for (auto [node, value] : inner_unobservable) {
|
||||
DCHECK_EQ(value, true);
|
||||
inner_gc_observable->Set(node, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return UnobservablesSet(new_set);
|
||||
return UnobservablesSet(NewSet(zone), new_gc_observable);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
Loading…
Reference in New Issue
Block a user