// Copyright 2019 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file.
class BytecodeArgument { public:
BytecodeArgument(int offset, int length) : offset_(offset), length_(length) {}
int offset() const { return offset_; } int length() const { return length_; }
private: // TODO(jgruber): This should store {offset,type} as well. // TODO(jgruber): Consider changing offset_ to be relative to the current // bytecode instead of the start of the bytecode sequence that is being // optimized. It is confusing that src/dst offsets have different semantics. int offset_; int length_;
};
// Describes a bytecode operand for use in a peephole sequence. struct OpInfo {
uint16_t offset;
BytecodeOperandType type;
constexpr int size() const { return Bytecodes::Size(type); }
Type type() const { return type_; } int new_offset() const { return op_info_.offset; }
BytecodeOperandType new_operand_type() const { return op_info_.type; } int new_length() const { return op_info_.size(); }
private:
Type type_;
OpInfo op_info_;
};
struct BytecodeArgumentCheck : public BytecodeArgument { enum CheckType { kCheckAddress = 0, kCheckValue };
CheckType type; int check_offset; int check_length;
BytecodeArgumentCheck(int offset, int length, int check_offset)
: BytecodeArgument(offset, length),
type(kCheckAddress),
check_offset(check_offset) {}
BytecodeArgumentCheck(int offset, int length, int check_offset, int check_length)
: BytecodeArgument(offset, length),
type(kCheckValue),
check_offset(check_offset),
check_length(check_length) {}
};
// Trie-Node for storing bytecode sequences we want to optimize. class BytecodeSequenceNode { public: explicit BytecodeSequenceNode(std::optional<Bytecode> bytecode); // Adds a new node as child of the current node if it isn't a child already.
BytecodeSequenceNode& FollowedBy(Bytecode bytecode); // Marks the end of a sequence and sets optimized bytecode to replace all // bytecodes of the sequence with.
BytecodeSequenceNode& ReplaceWith(Bytecode bytecode); // Maps arguments of bytecodes in the sequence to the optimized bytecode. // Order of invocation determines order of arguments in the optimized // bytecode. // Invoking this method is only allowed on nodes that mark the end of a valid // sequence (i.e. after ReplaceWith()). // to_op_info: Operand info of the argument in the optimized bytecode. // from_bytecode_sequence_index: Zero-based index of the referred bytecode // within the sequence (e.g. the bytecode passed to CreateSequence() has // index 0). // from_op_info: Operand info of the argument in the referred bytecode.
BytecodeSequenceNode& MapArgument(OpInfo to_op_info, int from_bytecode_sequence_index,
OpInfo from_op_info);
// Emits the offset after the whole sequence. // This should be used for every sequence that doesn't end in an unconditional // jump. The offset isn't statically known, as bytecodes might be preserved // after the sequence if they were jump targets from bytecodes outside the // sequence. The emitted offset is after these potentially preserved // bytecodes.
BytecodeSequenceNode& EmitOffsetAfterSequence(OpInfo op_info); // Verifies that we've created mappings in the order they are specified. bool BytecodeArgumentMappingCreatedInOrder(OpInfo op_info); // Adds a check to the sequence node making it only a valid sequence when the // argument of the current bytecode at the specified offset matches the offset // to check against. // op_info: Operand info of the argument to check. // check_byte_offset: Zero-based offset relative to the beginning of the // sequence that needs to match the value given by argument_offset. (e.g. // check_byte_offset 0 matches the address of the first bytecode in the // sequence).
BytecodeSequenceNode& IfArgumentEqualsOffset(OpInfo op_info, int check_byte_offset);
// Adds a check to the sequence node making it only a valid sequence when the // argument of the current bytecode at the specified offset matches the // argument of another bytecode in the sequence. // This is similar to IfArgumentEqualsOffset, except that this method matches // the values of both arguments.
BytecodeSequenceNode& IfArgumentEqualsValueAtOffset(
OpInfo this_op_info, int other_bytecode_index_in_sequence,
OpInfo other_op_info);
// Marks an argument as unused. // All arguments that are not mapped explicitly have to be marked as unused. // bytecode_index_in_sequence: Zero-based index of the referred bytecode // within the sequence (e.g. the bytecode passed to CreateSequence() has // index 0). // op_info: Operand info of the argument to ignore.
BytecodeSequenceNode& IgnoreArgument(int bytecode_index_in_sequence,
OpInfo op_info); // Checks if the current node is valid for the sequence. I.e. all conditions // set by IfArgumentEqualsOffset and IfArgumentEquals are fulfilled by this // node for the actual bytecode sequence. bool CheckArguments(const uint8_t* bytecode, int pc) const; // Returns whether this node marks the end of a valid sequence (i.e. can be // replaced with an optimized bytecode). bool IsSequence() const; // Returns the length of the sequence in bytes. int SequenceLength() const; // Returns the optimized bytecode for the node.
Bytecode OptimizedBytecode() const; // Returns the child of the current node matching the given bytecode or // nullptr if no such child is found.
BytecodeSequenceNode* Find(Bytecode bytecode) const; // Returns number of arguments mapped to the current node. // Invoking this method is only allowed on nodes that mark the end of a valid // sequence (i.e. if IsSequence())
size_t ArgumentSize() const; // Returns the argument-mapping of the argument at index. // Invoking this method is only allowed on nodes that mark the end of a valid // sequence (i.e. if IsSequence())
BytecodeArgumentMapping ArgumentMapping(size_t index) const; // Returns an iterator to begin of ignored arguments. // Invoking this method is only allowed on nodes that mark the end of a valid // sequence (i.e. if IsSequence())
std::vector<BytecodeArgument>::const_iterator ArgumentIgnoredBegin() const; // Returns an iterator to end of ignored arguments. // Invoking this method is only allowed on nodes that mark the end of a valid // sequence (i.e. if IsSequence())
std::vector<BytecodeArgument>::const_iterator ArgumentIgnoredEnd() const; // Returns whether the current node has ignored argument or not. bool HasIgnoredArguments() const;
private: // Returns a node in the sequence specified by its index within the sequence.
BytecodeSequenceNode& GetNodeByIndexInSequence(int index_in_sequence);
std::optional<Bytecode> bytecode_;
std::optional<Bytecode> bytecode_replacement_; int index_in_sequence_; int start_offset_;
BytecodeSequenceNode* parent_;
std::unordered_map<Bytecode, std::unique_ptr<BytecodeSequenceNode>> children_;
std::vector<BytecodeArgumentMapping> argument_mapping_;
std::vector<BytecodeArgumentCheck> argument_check_;
std::vector<BytecodeArgument> argument_ignored_; // True, iff the node has been fully created (including conditions). This // implies that it is invalid to add further conditions. // Note that it currently *is* valid to define the replacement bytecode and // its arguments mappings after a node has been sealed. bool sealed_ = false;
};
class BytecodePeephole { public: // Parses bytecode and fills the internal buffer with the potentially // optimized bytecode. Returns true when optimizations were performed, false // otherwise. staticbool OptimizeBytecode(Zone* zone, const BytecodeWriter* src_writer,
BytecodeWriter* dst_writer);
// Checks for optimization candidates at pc and emits optimized bytecode to // the internal buffer. Returns the length of replaced bytecodes in bytes. int TryOptimizeSequence(const uint8_t* bytecode, int bytecode_length, int start_pc); // Emits optimized bytecode to the internal buffer. start_pc points to the // start of the sequence in bytecode and last_node is the last // BytecodeSequenceNode of the matching sequence found. void EmitOptimization(int start_pc, const uint8_t* bytecode, const BytecodeSequenceNode& last_node); // Adds a relative jump destination fixup at pos. // Jump destination fixups are used to find offsets in the new bytecode that // can be jumped to. void AddJumpDestinationFixup(int fixup, int pos); // Sets an absolute jump destination fixup at pos. void SetJumpDestinationFixup(int fixup, int pos); // Updates all jump targets in the new bytecode. void FixJumps(); void EmitArgument(int start_pc, const uint8_t* bytecode,
BytecodeArgumentMapping arg); int pc() const;
Zone* zone() const;
// TODO(jgruber): We should also replace all of these raw offsets with // OpInfo. That should allow us to not expose the "raw" Emit publicly in the // Writer. // Number of times a jump destination is used within the bytecode. // Key: Jump destination (offset in old bytecode). // Value: Number of times jump destination is used.
ZoneMap<int, int> jump_usage_counts_; // Maps offsets in old bytecode to fixups of destinations (delta to new // bytecode). // Key: Offset in old bytecode from where the fixup is valid. // Value: Delta to map jump destinations from old bytecode to new bytecode in // bytes.
ZoneMap<int, int> jump_destination_fixups_;
Zone* const zone_;
// Points at the first pc in src_writer that has not yet been emitted. Used // for batch copying unchanged regions of the incoming bytecodes. int next_src_pc_to_emit_;
BytecodeSequenceNode& BytecodeSequenceNode::FollowedBy(Bytecode bytecode) {
sealed_ = true; if (children_.find(bytecode) == children_.end()) { auto new_node = std::make_unique<BytecodeSequenceNode>(bytecode); // If node is not the first in the sequence, set offsets and parent. if (bytecode_.has_value()) {
new_node->start_offset_ =
start_offset_ + Bytecodes::Size(bytecode_.value());
new_node->index_in_sequence_ = index_in_sequence_ + 1;
new_node->parent_ = this;
}
children_[bytecode] = std::move(new_node);
}
BytecodeSequenceNode* node = children_[bytecode].get(); // If this fails, the node was previously created as part of another // sequence. We can reuse it, but only if there are no condition for both the // previous and the current use. // TODO(jgruber): We could also reuse the node if non-empty previous and // current conditions are identical, but that's harder to check. // TODO(jgruber): Ideally conditions would become part of the tree (ie nodes // with different conditions are siblings), but this changes runtime behavior // of the peephole scanning algorithm to DFS. Sequence creation would also // need to handle deduplication. All possible, but not trivial.
DCHECK(node->argument_check_.empty()); return *node;
}
bool BytecodeSequenceNode::BytecodeArgumentMappingCreatedInOrder(
OpInfo op_info) {
DCHECK(IsSequence()); if (argument_mapping_.empty()) returntrue;
const BytecodeArgumentMapping& m = argument_mapping_.back(); int offset_after_last = m.new_offset() + m.new_length(); // TODO(jgruber): It'd be more precise to distinguish between special and // basic operand types, but we currently don't expose that information // except through templates. int dst_size = op_info.size(); int alignment = std::min(dst_size, kBytecodeAlignment); return RoundUp(offset_after_last, alignment) == op_info.offset;
}
BytecodeSequenceNode& BytecodeSequenceNode::IfArgumentEqualsOffset(
OpInfo op_info, int check_byte_offset) {
DCHECK(!sealed_); int size = op_info.size(); int offset = op_info.offset;
BytecodeSequenceNode& BytecodeSequenceNode::IgnoreArgument( int bytecode_index_in_sequence, OpInfo op_info) { int size = op_info.size(); int offset = op_info.offset;
BytecodePeephole::BytecodePeephole(Zone* zone, const BytecodeWriter* src_writer,
BytecodeWriter* dst_writer)
: dst_writer_(dst_writer),
src_writer_(src_writer),
jump_usage_counts_(zone),
jump_destination_fixups_(zone),
zone_(zone),
next_src_pc_to_emit_(0) {
dst_writer_->buffer().reserve(src_writer_->length()); // Prepare jump usage counts. for (auto jump_edge : src_writer_->jump_edges()) { int jump_destination = jump_edge.second;
jump_usage_counts_[jump_destination]++;
} // Sentinel fixups at beginning of bytecode (position -1) so we don't have to // check for end of iterator inside the fixup loop. // In general fixups are deltas of original offsets of jump // sources/destinations (in the old bytecode) to find them in the new // bytecode. All jump targets are fixed after the new bytecode is fully // emitted in the internal buffer.
jump_destination_fixups_.emplace(-1, 0); // Sentinel fixups at end of (old) bytecode so we don't have to check for // end of iterator inside the fixup loop.
DCHECK_LE(src_writer_->length(), std::numeric_limits<int>::max());
jump_destination_fixups_.emplace(static_cast<int>(src_writer_->length()), 0);
}
void BytecodePeepholeSequences::DefineStandardSequences() { using B = Bytecode; #define I(BYTECODE, OPERAND) \
OpInfo::Get<BytecodeOperands<BYTECODE>, \
BytecodeOperands<BYTECODE>::Operand::OPERAND>() #define T(OPERAND) I(Target, OPERAND)
// Commonly used sequences can be found by creating regexp bytecode traces // (--trace-regexp-bytecodes) and using v8/tools/regexp-sequences.py.
// TODO(pthier): It might make sense for short sequences like this one to only // optimize them if the resulting optimization is not longer than the current // one. This could be the case if there are jumps inside the sequence and we // have to replicate parts of the sequence. A method to mark such sequences // might be useful.
{ static constexpr auto Target = B::kSkipUntilChar;
CreateSequence(B::kLoadCurrentCharacter)
.FollowedBy(B::kCheckCharacter)
.FollowedBy(B::kAdvanceCpAndGoto)
.IfArgumentEqualsOffset(I(B::kAdvanceCpAndGoto, on_goto), 0)
.ReplaceWith(Target)
.MapArgument(T(cp_offset), 0, I(B::kLoadCurrentCharacter, cp_offset))
.MapArgument(T(advance_by), 2, I(B::kAdvanceCpAndGoto, by))
.MapArgument(T(character), 1, I(B::kCheckCharacter, character))
.MapArgument(T(on_match), 1, I(B::kCheckCharacter, on_equal))
.MapArgument(T(on_no_match), 0, I(B::kLoadCurrentCharacter, on_failure))
.IgnoreArgument(2, I(B::kAdvanceCpAndGoto, on_goto));
}
#undef I #undef T
} bool BytecodePeephole::OptimizeBytecode(Zone* zone, const BytecodeWriter* src_writer,
BytecodeWriter* dst_writer) {
BytecodePeephole p(zone, src_writer, dst_writer);
const uint8_t* bytecode = src_writer->buffer().data(); // TODO(375937549): Convert length to uint32_t. int length = static_cast<int>(src_writer->length());
int old_pc = 0; bool did_optimize = false;
while (old_pc < length) { int replaced_len = p.TryOptimizeSequence(bytecode, length, old_pc); if (replaced_len > 0) {
old_pc += replaced_len;
did_optimize = true;
} else { int bc_len = Bytecodes::Size(bytecode[old_pc]);
old_pc += bc_len;
}
}
if (did_optimize) { // If we optimized anything, we must flush the remaining unoptimized bytes. // If we didn't optimize anything, we leave the dst_writer empty and the // caller will continue using src_writer (effectively a no-op pass). if (old_pc > p.next_src_pc_to_emit_) {
dst_writer->EmitRawBytecodeStream(src_writer, p.next_src_pc_to_emit_,
old_pc - p.next_src_pc_to_emit_);
}
p.FixJumps();
}
int BytecodePeephole::TryOptimizeSequence(const uint8_t* bytecode, int bytecode_length, int start_pc) { const BytecodeSequenceNode* seq_node = GetStandardSequences()->sequences(); const BytecodeSequenceNode* valid_seq_end = nullptr;
int current_pc = start_pc;
// Check for the longest valid sequence matching any of the pre-defined // sequences in the Trie data structure. while (current_pc < bytecode_length) {
seq_node = seq_node->Find(Bytecodes::FromByte(bytecode[current_pc])); if (seq_node == nullptr) break; if (!seq_node->CheckArguments(bytecode, start_pc)) break;
if (seq_node->IsSequence()) valid_seq_end = seq_node;
current_pc += Bytecodes::Size(bytecode[current_pc]);
}
if (valid_seq_end) {
EmitOptimization(start_pc, bytecode, *valid_seq_end); return valid_seq_end->SequenceLength();
}
return0;
}
void BytecodePeephole::EmitOptimization(int start_pc, const uint8_t* bytecode, const BytecodeSequenceNode& last_node) { // Flush any sequence of bytecodes which we haven't emitted yet. if (start_pc > next_src_pc_to_emit_) {
dst_writer_->EmitRawBytecodeStream(src_writer_, next_src_pc_to_emit_,
start_pc - next_src_pc_to_emit_);
} constint sequence_length = last_node.SequenceLength();
next_src_pc_to_emit_ = start_pc + sequence_length;
// Update usage counts for all jumps originating in the sequence we are about // to replace. Counts for the target locations are decremented here. Emitting // kJumpTarget below will again increment for newly emitted destinations. auto edge_it = src_writer_->jump_edges().lower_bound(start_pc); while (edge_it != src_writer_->jump_edges().end() &&
edge_it->first < start_pc + sequence_length) { int target = edge_it->second; auto count_it = jump_usage_counts_.find(target);
DCHECK_NE(count_it, jump_usage_counts_.end());
count_it->second--;
edge_it++;
}
int optimized_start_pc = pc(); // List of offsets in the optimized sequence that need to be patched to the // offset value right after the optimized sequence.
ZoneLinkedList<uint32_t> after_sequence_offsets(zone());
const Bytecode bc = last_node.OptimizedBytecode();
dst_writer_->EmitBytecode(bc);
for (size_t arg_idx = 0; arg_idx < last_node.ArgumentSize(); arg_idx++) {
BytecodeArgumentMapping arg_map = last_node.ArgumentMapping(arg_idx); if (arg_map.type() == BytecodeArgumentMapping::Type::kDefault) { if (arg_map.new_operand_type() == BytecodeOperandType::kJumpTarget) { int target = GetArgumentValue(bytecode, start_pc + arg_map.offset(),
arg_map.length());
jump_usage_counts_[target]++;
}
EmitArgument(start_pc, bytecode, arg_map);
} else {
DCHECK_EQ(arg_map.type(),
BytecodeArgumentMapping::Type::kOffsetAfterSequence);
after_sequence_offsets.push_back(optimized_start_pc +
arg_map.new_offset()); // Reserve space to overwrite later with the pc after this sequence.
dst_writer_->Emit<uint32_t>(0, arg_map.new_offset());
}
}
// Final alignment.
dst_writer_->Finalize(bc);
DCHECK_EQ(pc(), optimized_start_pc + Bytecodes::Size(bc));
int fixup_length = Bytecodes::Size(bc) - sequence_length;
// Check if there are any jumps inside the old sequence. // If so we have to keep the bytecodes that are jumped to around. auto jump_destination_candidate = jump_usage_counts_.upper_bound(start_pc); int jump_candidate_destination = jump_destination_candidate->first; int jump_candidate_count = jump_destination_candidate->second; // Jump destinations only jumped to from inside the sequence will be ignored. while (jump_destination_candidate != jump_usage_counts_.end() &&
jump_candidate_count == 0) {
++jump_destination_candidate;
jump_candidate_destination = jump_destination_candidate->first;
jump_candidate_count = jump_destination_candidate->second;
}
int preserve_from = start_pc + sequence_length; if (jump_destination_candidate != jump_usage_counts_.end() &&
jump_candidate_destination < start_pc + sequence_length) {
preserve_from = jump_candidate_destination; // Check if any jump in the sequence we are preserving has a jump // destination inside the optimized sequence before the current position we // want to preserve. If so we have to preserve all bytecodes starting at // this jump destination. for (auto jump_iter = src_writer_->jump_edges().lower_bound(preserve_from);
jump_iter != src_writer_->jump_edges().end() &&
jump_iter->first /* jump source */ < start_pc + sequence_length;
++jump_iter) { int jump_destination = jump_iter->second; if (jump_destination > start_pc && jump_destination < preserve_from) {
preserve_from = jump_destination;
}
}
// We preserve everything to the end of the sequence. This is conservative // since it would be enough to preserve all bytecodes up to an unconditional // jump. int preserve_length = start_pc + sequence_length - preserve_from;
fixup_length += preserve_length; // All jump targets after the start of the optimized sequence need to be // fixed relative to the length of the optimized sequence including // bytecodes we preserved.
AddJumpDestinationFixup(fixup_length, start_pc + 1); // Jumps to the sequence we preserved need absolute fixup as they could // occur before or after the sequence.
SetJumpDestinationFixup(pc() - preserve_from, preserve_from);
dst_writer_->EmitRawBytecodeStream(src_writer_, preserve_from,
preserve_length);
} else {
AddJumpDestinationFixup(fixup_length, start_pc + 1);
}
for (uint32_t offset : after_sequence_offsets) {
DCHECK_EQ(dst_writer_->buffer()[offset], 0);
dst_writer_->OverwriteValue<uint32_t>(pc(), offset); // Register the offset in jump_edges_ so that subsequent peephole passes // adjust it when bytecodes shift.
dst_writer_->jump_edges().emplace(offset, start_pc + sequence_length);
}
}
void BytecodePeephole::AddJumpDestinationFixup(int fixup, int pos) { auto previous_fixup = jump_destination_fixups_.lower_bound(pos);
DCHECK(previous_fixup != jump_destination_fixups_.end());
DCHECK(previous_fixup != jump_destination_fixups_.begin());
int previous_fixup_value = (--previous_fixup)->second;
jump_destination_fixups_[pos] = previous_fixup_value + fixup;
}
void BytecodePeephole::SetJumpDestinationFixup(int fixup, int pos) { auto previous_fixup = jump_destination_fixups_.lower_bound(pos);
DCHECK(previous_fixup != jump_destination_fixups_.end());
DCHECK(previous_fixup != jump_destination_fixups_.begin());
void BytecodePeephole::FixJumps() { for (auto jump_edge : dst_writer_->jump_edges()) { int jump_source = jump_edge.first; int jump_destination = jump_edge.second; int fixed_jump_destination =
jump_destination +
(--jump_destination_fixups_.upper_bound(jump_destination))->second;
DCHECK_LT(fixed_jump_destination, pc()); // TODO(pthier): This check could be better if we track the bytecodes // actually used and check if we jump to one of them.
DCHECK(Bytecodes::IsValidJumpTarget(
dst_writer_->buffer()[fixed_jump_destination]));
if (jump_destination != fixed_jump_destination) {
dst_writer_->PatchJump(fixed_jump_destination, jump_source);
}
}
}
// Preserve the original bytecode for tracing if needed.
std::optional<ZoneVector<uint8_t>> original_bytecode; if (v8_flags.trace_regexp_peephole_optimization) { const ZoneVector<uint8_t>& src_buffer = src_writer->buffer(); constauto begin = src_buffer.begin();
original_bytecode.emplace(begin, begin + src_writer->length(), zone);
}
constbool did_optimize =
BytecodePeephole::OptimizeBytecode(zone, src_writer, &dst_writer); // The result is in dst_writer iff a peephole rule fired; otherwise the // unchanged input bytecode is still in src_writer.
BytecodeWriter* result = did_optimize ? &dst_writer : src_writer; const uint8_t* optimized_bytecode = result->buffer().data();
uint32_t optimized_length = result->length();
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.