Commit 6e8d4f55 authored by Tobias Tebbi's avatar Tobias Tebbi Committed by V8 LUCI CQ

[turboshaft] add operation use counts

Some optimizations need to know if an operation has multiple uses,
for example to avoid extending live-ranges.
However, maintaining full use-lists is expensive memory-wise and
not really needed in this case, where we only need to distinguish
between 1 or more uses.
Therefore, we only count the number of uses. To save even more memory,
we use the 1 byte currently left for alignment padding in the
operation header and put the count there.
With a single byte, we cannot count beyond 255, but for the use-case
at hand, this is enough. When reaching 255, we no longer track the
use-count.
Nodes with so many uses should be rare and their use-count will usually not go down to 1 again, so this does not loose much precision.

Another possible future use of these counts is reserving memory for
full use-lists.

This CL also removes mutable access to node inputs, as this would need
to update use-counts and is not actually needed currently.

Bug: v8:12783
Change-Id: Idd2035c6f8ced6317e3aec0c42eecd1383e86248
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3863266
Auto-Submit: Tobias Tebbi <tebbi@chromium.org>
Commit-Queue: Tobias Tebbi <tebbi@chromium.org>
Reviewed-by: 's avatarNico Hartmann <nicohartmann@chromium.org>
Cr-Commit-Position: refs/heads/main@{#82867}
parent a27a527b
......@@ -405,13 +405,16 @@ class Graph {
return operations_.Allocate(slot_count);
}
void RemoveLast() { operations_.RemoveLast(); }
void RemoveLast() {
DecrementInputUses(*AllOperations().rbegin());
operations_.RemoveLast();
}
template <class Op, class... Args>
V8_INLINE OpIndex Add(Args... args) {
OpIndex result = next_operation_index();
Op& op = Op::New(this, args...);
USE(op);
IncrementInputUses(op);
DCHECK_EQ(result, Index(op));
#ifdef DEBUG
for (OpIndex input : op.inputs()) {
......@@ -426,8 +429,16 @@ class Graph {
static_assert((std::is_base_of<Operation, Op>::value));
static_assert(std::is_trivially_destructible<Op>::value);
OperationBuffer::ReplaceScope replace_scope(&operations_, replaced);
Op::New(this, args...);
const Operation& old_op = Get(replaced);
DecrementInputUses(old_op);
auto old_uses = old_op.saturated_use_count;
Op* new_op;
{
OperationBuffer::ReplaceScope replace_scope(&operations_, replaced);
new_op = &Op::New(this, args...);
}
new_op->saturated_use_count = old_uses;
IncrementInputUses(*new_op);
}
V8_INLINE Block* NewBlock(Block::Kind kind) {
......@@ -654,6 +665,32 @@ class Graph {
return true;
}
template <class Op>
void IncrementInputUses(const Op& op) {
for (OpIndex input : op.inputs()) {
Operation& input_op = Get(input);
auto uses = input_op.saturated_use_count;
if (V8_LIKELY(uses != Operation::kUnknownUseCount)) {
input_op.saturated_use_count = uses + 1;
}
}
}
template <class Op>
void DecrementInputUses(const Op& op) {
for (OpIndex input : op.inputs()) {
Operation& input_op = Get(input);
auto uses = input_op.saturated_use_count;
DCHECK_GT(uses, 0);
// Do not decrement if we already reached the threshold. In this case, we
// don't know the exact number of uses anymore and shouldn't assume
// anything.
if (V8_LIKELY(uses != Operation::kUnknownUseCount)) {
input_op.saturated_use_count = uses - 1;
}
}
}
OperationBuffer operations_;
ZoneVector<Block*> bound_blocks_;
ZoneVector<Block*> all_blocks_;
......
......@@ -273,14 +273,22 @@ std::ostream& operator<<(std::ostream& os, OpProperties opProperties);
// `OpIndex` inputs.
struct alignas(OpIndex) Operation {
const Opcode opcode;
// The number of uses of this operation in the current graph.
// Instead of overflowing, we saturate the value if it reaches the maximum. In
// this case, the true number of uses is unknown.
// We use such a small type to save memory and because nodes with a high
// number of uses are rare. Additionally, we usually only care if the number
// of uses is 0, 1 or bigger than 1.
uint8_t saturated_use_count = 0;
static constexpr uint8_t kUnknownUseCount =
std::numeric_limits<uint8_t>::max();
const uint16_t input_count;
// The inputs are stored adjacent in memory, right behind the `Operation`
// object.
base::Vector<OpIndex> inputs();
base::Vector<const OpIndex> inputs() const;
V8_INLINE OpIndex& input(size_t i) { return inputs()[i]; }
V8_INLINE OpIndex input(size_t i) const { return inputs()[i]; }
static size_t StorageSlotCount(Opcode opcode, size_t input_count);
......@@ -1651,13 +1659,6 @@ constexpr size_t kOperationSizeDividedBySizeofOpIndexTable[kNumberOfOpcodes] = {
#undef OPERATION_SIZE
};
inline base::Vector<OpIndex> Operation::inputs() {
// This is actually undefined behavior, since we use the `this` pointer to
// access an adjacent object.
OpIndex* ptr = reinterpret_cast<OpIndex*>(
reinterpret_cast<char*>(this) + kOperationSizeTable[OpcodeIndex(opcode)]);
return {ptr, input_count};
}
inline base::Vector<const OpIndex> Operation::inputs() const {
// This is actually undefined behavior, since we use the `this` pointer to
// access an adjacent object.
......
......@@ -32,7 +32,11 @@ struct AnalyzerBase {
const Graph& graph;
void Run() {}
bool OpIsUsed(OpIndex i) const { return true; }
bool OpIsUsed(OpIndex i) const {
const Operation& op = graph.Get(i);
return op.saturated_use_count > 0 ||
op.properties().is_required_when_unused;
}
explicit AnalyzerBase(const Graph& graph, Zone* phase_zone)
: phase_zone(phase_zone), graph(graph) {}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment