[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:
Tobias Tebbi 2022-09-01 12:06:31 +02:00 committed by V8 LUCI CQ
parent a55ecfaf7f
commit eed7eaff06
2 changed files with 116 additions and 115 deletions

View File

@ -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,

View File

@ -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