| // Copyright 2014 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. |
| |
| #include "src/compiler/move-optimizer.h" |
| |
| namespace v8 { |
| namespace internal { |
| namespace compiler { |
| |
| namespace { |
| |
| struct MoveKey { |
| InstructionOperand source; |
| InstructionOperand destination; |
| }; |
| |
| struct MoveKeyCompare { |
| bool operator()(const MoveKey& a, const MoveKey& b) const { |
| if (a.source.EqualsCanonicalized(b.source)) { |
| return a.destination.CompareCanonicalized(b.destination); |
| } |
| return a.source.CompareCanonicalized(b.source); |
| } |
| }; |
| |
| typedef ZoneMap<MoveKey, unsigned, MoveKeyCompare> MoveMap; |
| |
| class OperandSet { |
| public: |
| explicit OperandSet(ZoneVector<InstructionOperand>* buffer) |
| : set_(buffer), fp_reps_(0) { |
| buffer->clear(); |
| } |
| |
| void InsertOp(const InstructionOperand& op) { |
| set_->push_back(op); |
| |
| if (!kSimpleFPAliasing && op.IsFPRegister()) |
| fp_reps_ |= RepBit(LocationOperand::cast(op).representation()); |
| } |
| |
| bool Contains(const InstructionOperand& op) const { |
| for (const InstructionOperand& elem : *set_) { |
| if (elem.EqualsCanonicalized(op)) return true; |
| } |
| return false; |
| } |
| |
| bool ContainsOpOrAlias(const InstructionOperand& op) const { |
| if (Contains(op)) return true; |
| |
| if (!kSimpleFPAliasing && op.IsFPRegister()) { |
| // Platforms where FP registers have complex aliasing need extra checks. |
| const LocationOperand& loc = LocationOperand::cast(op); |
| MachineRepresentation rep = loc.representation(); |
| // If haven't encountered mixed rep FP registers, skip the extra checks. |
| if (!HasMixedFPReps(fp_reps_ | RepBit(rep))) return false; |
| |
| // Check register against aliasing registers of other FP representations. |
| MachineRepresentation other_rep1, other_rep2; |
| switch (rep) { |
| case MachineRepresentation::kFloat32: |
| other_rep1 = MachineRepresentation::kFloat64; |
| other_rep2 = MachineRepresentation::kSimd128; |
| break; |
| case MachineRepresentation::kFloat64: |
| other_rep1 = MachineRepresentation::kFloat32; |
| other_rep2 = MachineRepresentation::kSimd128; |
| break; |
| case MachineRepresentation::kSimd128: |
| other_rep1 = MachineRepresentation::kFloat32; |
| other_rep2 = MachineRepresentation::kFloat64; |
| break; |
| default: |
| UNREACHABLE(); |
| break; |
| } |
| const RegisterConfiguration* config = RegisterConfiguration::Default(); |
| int base = -1; |
| int aliases = |
| config->GetAliases(rep, loc.register_code(), other_rep1, &base); |
| DCHECK(aliases > 0 || (aliases == 0 && base == -1)); |
| while (aliases--) { |
| if (Contains(AllocatedOperand(LocationOperand::REGISTER, other_rep1, |
| base + aliases))) { |
| return true; |
| } |
| } |
| aliases = config->GetAliases(rep, loc.register_code(), other_rep2, &base); |
| DCHECK(aliases > 0 || (aliases == 0 && base == -1)); |
| while (aliases--) { |
| if (Contains(AllocatedOperand(LocationOperand::REGISTER, other_rep2, |
| base + aliases))) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| private: |
| static int RepBit(MachineRepresentation rep) { |
| return 1 << static_cast<int>(rep); |
| } |
| |
| static bool HasMixedFPReps(int reps) { |
| return reps && !base::bits::IsPowerOfTwo(reps); |
| } |
| |
| ZoneVector<InstructionOperand>* set_; |
| int fp_reps_; |
| }; |
| |
| int FindFirstNonEmptySlot(const Instruction* instr) { |
| int i = Instruction::FIRST_GAP_POSITION; |
| for (; i <= Instruction::LAST_GAP_POSITION; i++) { |
| ParallelMove* moves = instr->parallel_moves()[i]; |
| if (moves == nullptr) continue; |
| for (MoveOperands* move : *moves) { |
| if (!move->IsRedundant()) return i; |
| move->Eliminate(); |
| } |
| moves->clear(); // Clear this redundant move. |
| } |
| return i; |
| } |
| |
| } // namespace |
| |
| MoveOptimizer::MoveOptimizer(Zone* local_zone, InstructionSequence* code) |
| : local_zone_(local_zone), |
| code_(code), |
| local_vector_(local_zone), |
| operand_buffer1(local_zone), |
| operand_buffer2(local_zone) {} |
| |
| void MoveOptimizer::Run() { |
| for (Instruction* instruction : code()->instructions()) { |
| CompressGaps(instruction); |
| } |
| for (InstructionBlock* block : code()->instruction_blocks()) { |
| CompressBlock(block); |
| } |
| for (InstructionBlock* block : code()->instruction_blocks()) { |
| if (block->PredecessorCount() <= 1) continue; |
| if (!block->IsDeferred()) { |
| bool has_only_deferred = true; |
| for (RpoNumber& pred_id : block->predecessors()) { |
| if (!code()->InstructionBlockAt(pred_id)->IsDeferred()) { |
| has_only_deferred = false; |
| break; |
| } |
| } |
| // This would pull down common moves. If the moves occur in deferred |
| // blocks, and the closest common successor is not deferred, we lose the |
| // optimization of just spilling/filling in deferred blocks, when the |
| // current block is not deferred. |
| if (has_only_deferred) continue; |
| } |
| OptimizeMerge(block); |
| } |
| for (Instruction* gap : code()->instructions()) { |
| FinalizeMoves(gap); |
| } |
| } |
| |
| void MoveOptimizer::RemoveClobberedDestinations(Instruction* instruction) { |
| if (instruction->IsCall()) return; |
| ParallelMove* moves = instruction->parallel_moves()[0]; |
| if (moves == nullptr) return; |
| |
| DCHECK(instruction->parallel_moves()[1] == nullptr || |
| instruction->parallel_moves()[1]->empty()); |
| |
| OperandSet outputs(&operand_buffer1); |
| OperandSet inputs(&operand_buffer2); |
| |
| // Outputs and temps are treated together as potentially clobbering a |
| // destination operand. |
| for (size_t i = 0; i < instruction->OutputCount(); ++i) { |
| outputs.InsertOp(*instruction->OutputAt(i)); |
| } |
| for (size_t i = 0; i < instruction->TempCount(); ++i) { |
| outputs.InsertOp(*instruction->TempAt(i)); |
| } |
| |
| // Input operands block elisions. |
| for (size_t i = 0; i < instruction->InputCount(); ++i) { |
| inputs.InsertOp(*instruction->InputAt(i)); |
| } |
| |
| // Elide moves made redundant by the instruction. |
| for (MoveOperands* move : *moves) { |
| if (outputs.ContainsOpOrAlias(move->destination()) && |
| !inputs.ContainsOpOrAlias(move->destination())) { |
| move->Eliminate(); |
| } |
| } |
| |
| // The ret instruction makes any assignment before it unnecessary, except for |
| // the one for its input. |
| if (instruction->IsRet() || instruction->IsTailCall()) { |
| for (MoveOperands* move : *moves) { |
| if (!inputs.ContainsOpOrAlias(move->destination())) { |
| move->Eliminate(); |
| } |
| } |
| } |
| } |
| |
| void MoveOptimizer::MigrateMoves(Instruction* to, Instruction* from) { |
| if (from->IsCall()) return; |
| |
| ParallelMove* from_moves = from->parallel_moves()[0]; |
| if (from_moves == nullptr || from_moves->empty()) return; |
| |
| OperandSet dst_cant_be(&operand_buffer1); |
| OperandSet src_cant_be(&operand_buffer2); |
| |
| // If an operand is an input to the instruction, we cannot move assignments |
| // where it appears on the LHS. |
| for (size_t i = 0; i < from->InputCount(); ++i) { |
| dst_cant_be.InsertOp(*from->InputAt(i)); |
| } |
| // If an operand is output to the instruction, we cannot move assignments |
| // where it appears on the RHS, because we would lose its value before the |
| // instruction. |
| // Same for temp operands. |
| // The output can't appear on the LHS because we performed |
| // RemoveClobberedDestinations for the "from" instruction. |
| for (size_t i = 0; i < from->OutputCount(); ++i) { |
| src_cant_be.InsertOp(*from->OutputAt(i)); |
| } |
| for (size_t i = 0; i < from->TempCount(); ++i) { |
| src_cant_be.InsertOp(*from->TempAt(i)); |
| } |
| for (MoveOperands* move : *from_moves) { |
| if (move->IsRedundant()) continue; |
| // Assume dest has a value "V". If we have a "dest = y" move, then we can't |
| // move "z = dest", because z would become y rather than "V". |
| // We assume CompressMoves has happened before this, which means we don't |
| // have more than one assignment to dest. |
| src_cant_be.InsertOp(move->destination()); |
| } |
| |
| ZoneSet<MoveKey, MoveKeyCompare> move_candidates(local_zone()); |
| // We start with all the moves that don't have conflicting source or |
| // destination operands are eligible for being moved down. |
| for (MoveOperands* move : *from_moves) { |
| if (move->IsRedundant()) continue; |
| if (!dst_cant_be.ContainsOpOrAlias(move->destination())) { |
| MoveKey key = {move->source(), move->destination()}; |
| move_candidates.insert(key); |
| } |
| } |
| if (move_candidates.empty()) return; |
| |
| // Stabilize the candidate set. |
| bool changed = false; |
| do { |
| changed = false; |
| for (auto iter = move_candidates.begin(); iter != move_candidates.end();) { |
| auto current = iter; |
| ++iter; |
| InstructionOperand src = current->source; |
| if (src_cant_be.ContainsOpOrAlias(src)) { |
| src_cant_be.InsertOp(current->destination); |
| move_candidates.erase(current); |
| changed = true; |
| } |
| } |
| } while (changed); |
| |
| ParallelMove to_move(local_zone()); |
| for (MoveOperands* move : *from_moves) { |
| if (move->IsRedundant()) continue; |
| MoveKey key = {move->source(), move->destination()}; |
| if (move_candidates.find(key) != move_candidates.end()) { |
| to_move.AddMove(move->source(), move->destination(), code_zone()); |
| move->Eliminate(); |
| } |
| } |
| if (to_move.empty()) return; |
| |
| ParallelMove* dest = |
| to->GetOrCreateParallelMove(Instruction::GapPosition::START, code_zone()); |
| |
| CompressMoves(&to_move, dest); |
| DCHECK(dest->empty()); |
| for (MoveOperands* m : to_move) { |
| dest->push_back(m); |
| } |
| } |
| |
| void MoveOptimizer::CompressMoves(ParallelMove* left, MoveOpVector* right) { |
| if (right == nullptr) return; |
| |
| MoveOpVector& eliminated = local_vector(); |
| DCHECK(eliminated.empty()); |
| |
| if (!left->empty()) { |
| // Modify the right moves in place and collect moves that will be killed by |
| // merging the two gaps. |
| for (MoveOperands* move : *right) { |
| if (move->IsRedundant()) continue; |
| left->PrepareInsertAfter(move, &eliminated); |
| } |
| // Eliminate dead moves. |
| for (MoveOperands* to_eliminate : eliminated) { |
| to_eliminate->Eliminate(); |
| } |
| eliminated.clear(); |
| } |
| // Add all possibly modified moves from right side. |
| for (MoveOperands* move : *right) { |
| if (move->IsRedundant()) continue; |
| left->push_back(move); |
| } |
| // Nuke right. |
| right->clear(); |
| DCHECK(eliminated.empty()); |
| } |
| |
| void MoveOptimizer::CompressGaps(Instruction* instruction) { |
| int i = FindFirstNonEmptySlot(instruction); |
| bool has_moves = i <= Instruction::LAST_GAP_POSITION; |
| USE(has_moves); |
| |
| if (i == Instruction::LAST_GAP_POSITION) { |
| std::swap(instruction->parallel_moves()[Instruction::FIRST_GAP_POSITION], |
| instruction->parallel_moves()[Instruction::LAST_GAP_POSITION]); |
| } else if (i == Instruction::FIRST_GAP_POSITION) { |
| CompressMoves( |
| instruction->parallel_moves()[Instruction::FIRST_GAP_POSITION], |
| instruction->parallel_moves()[Instruction::LAST_GAP_POSITION]); |
| } |
| // We either have no moves, or, after swapping or compressing, we have |
| // all the moves in the first gap position, and none in the second/end gap |
| // position. |
| ParallelMove* first = |
| instruction->parallel_moves()[Instruction::FIRST_GAP_POSITION]; |
| ParallelMove* last = |
| instruction->parallel_moves()[Instruction::LAST_GAP_POSITION]; |
| USE(first); |
| USE(last); |
| |
| DCHECK(!has_moves || |
| (first != nullptr && (last == nullptr || last->empty()))); |
| } |
| |
| void MoveOptimizer::CompressBlock(InstructionBlock* block) { |
| int first_instr_index = block->first_instruction_index(); |
| int last_instr_index = block->last_instruction_index(); |
| |
| // Start by removing gap assignments where the output of the subsequent |
| // instruction appears on LHS, as long as they are not needed by its input. |
| Instruction* prev_instr = code()->instructions()[first_instr_index]; |
| RemoveClobberedDestinations(prev_instr); |
| |
| for (int index = first_instr_index + 1; index <= last_instr_index; ++index) { |
| Instruction* instr = code()->instructions()[index]; |
| // Migrate to the gap of prev_instr eligible moves from instr. |
| MigrateMoves(instr, prev_instr); |
| // Remove gap assignments clobbered by instr's output. |
| RemoveClobberedDestinations(instr); |
| prev_instr = instr; |
| } |
| } |
| |
| |
| const Instruction* MoveOptimizer::LastInstruction( |
| const InstructionBlock* block) const { |
| return code()->instructions()[block->last_instruction_index()]; |
| } |
| |
| |
| void MoveOptimizer::OptimizeMerge(InstructionBlock* block) { |
| DCHECK_LT(1, block->PredecessorCount()); |
| // Ensure that the last instruction in all incoming blocks don't contain |
| // things that would prevent moving gap moves across them. |
| for (RpoNumber& pred_index : block->predecessors()) { |
| const InstructionBlock* pred = code()->InstructionBlockAt(pred_index); |
| |
| // If the predecessor has more than one successor, we shouldn't attempt to |
| // move down to this block (one of the successors) any of the gap moves, |
| // because their effect may be necessary to the other successors. |
| if (pred->SuccessorCount() > 1) return; |
| |
| const Instruction* last_instr = |
| code()->instructions()[pred->last_instruction_index()]; |
| if (last_instr->IsCall()) return; |
| if (last_instr->TempCount() != 0) return; |
| if (last_instr->OutputCount() != 0) return; |
| for (size_t i = 0; i < last_instr->InputCount(); ++i) { |
| const InstructionOperand* op = last_instr->InputAt(i); |
| if (!op->IsConstant() && !op->IsImmediate()) return; |
| } |
| } |
| // TODO(dcarney): pass a ZoneStats down for this? |
| MoveMap move_map(local_zone()); |
| size_t correct_counts = 0; |
| // Accumulate set of shared moves. |
| for (RpoNumber& pred_index : block->predecessors()) { |
| const InstructionBlock* pred = code()->InstructionBlockAt(pred_index); |
| const Instruction* instr = LastInstruction(pred); |
| if (instr->parallel_moves()[0] == nullptr || |
| instr->parallel_moves()[0]->empty()) { |
| return; |
| } |
| for (const MoveOperands* move : *instr->parallel_moves()[0]) { |
| if (move->IsRedundant()) continue; |
| InstructionOperand src = move->source(); |
| InstructionOperand dst = move->destination(); |
| MoveKey key = {src, dst}; |
| auto res = move_map.insert(std::make_pair(key, 1)); |
| if (!res.second) { |
| res.first->second++; |
| if (res.first->second == block->PredecessorCount()) { |
| correct_counts++; |
| } |
| } |
| } |
| } |
| if (move_map.empty() || correct_counts == 0) return; |
| |
| // Find insertion point. |
| Instruction* instr = code()->instructions()[block->first_instruction_index()]; |
| |
| if (correct_counts != move_map.size()) { |
| // Moves that are unique to each predecessor won't be pushed to the common |
| // successor. |
| OperandSet conflicting_srcs(&operand_buffer1); |
| for (auto iter = move_map.begin(), end = move_map.end(); iter != end;) { |
| auto current = iter; |
| ++iter; |
| if (current->second != block->PredecessorCount()) { |
| InstructionOperand dest = current->first.destination; |
| // Not all the moves in all the gaps are the same. Maybe some are. If |
| // there are such moves, we could move them, but the destination of the |
| // moves staying behind can't appear as a source of a common move, |
| // because the move staying behind will clobber this destination. |
| conflicting_srcs.InsertOp(dest); |
| move_map.erase(current); |
| } |
| } |
| |
| bool changed = false; |
| do { |
| // If a common move can't be pushed to the common successor, then its |
| // destination also can't appear as source to any move being pushed. |
| changed = false; |
| for (auto iter = move_map.begin(), end = move_map.end(); iter != end;) { |
| auto current = iter; |
| ++iter; |
| DCHECK_EQ(block->PredecessorCount(), current->second); |
| if (conflicting_srcs.ContainsOpOrAlias(current->first.source)) { |
| conflicting_srcs.InsertOp(current->first.destination); |
| move_map.erase(current); |
| changed = true; |
| } |
| } |
| } while (changed); |
| } |
| |
| if (move_map.empty()) return; |
| |
| DCHECK_NOT_NULL(instr); |
| bool gap_initialized = true; |
| if (instr->parallel_moves()[0] != nullptr && |
| !instr->parallel_moves()[0]->empty()) { |
| // Will compress after insertion. |
| gap_initialized = false; |
| std::swap(instr->parallel_moves()[0], instr->parallel_moves()[1]); |
| } |
| ParallelMove* moves = instr->GetOrCreateParallelMove( |
| static_cast<Instruction::GapPosition>(0), code_zone()); |
| // Delete relevant entries in predecessors and move everything to block. |
| bool first_iteration = true; |
| for (RpoNumber& pred_index : block->predecessors()) { |
| const InstructionBlock* pred = code()->InstructionBlockAt(pred_index); |
| for (MoveOperands* move : *LastInstruction(pred)->parallel_moves()[0]) { |
| if (move->IsRedundant()) continue; |
| MoveKey key = {move->source(), move->destination()}; |
| auto it = move_map.find(key); |
| if (it != move_map.end()) { |
| if (first_iteration) { |
| moves->AddMove(move->source(), move->destination()); |
| } |
| move->Eliminate(); |
| } |
| } |
| first_iteration = false; |
| } |
| // Compress. |
| if (!gap_initialized) { |
| CompressMoves(instr->parallel_moves()[0], instr->parallel_moves()[1]); |
| } |
| CompressBlock(block); |
| } |
| |
| |
| namespace { |
| |
| bool IsSlot(const InstructionOperand& op) { |
| return op.IsStackSlot() || op.IsFPStackSlot(); |
| } |
| |
| |
| bool LoadCompare(const MoveOperands* a, const MoveOperands* b) { |
| if (!a->source().EqualsCanonicalized(b->source())) { |
| return a->source().CompareCanonicalized(b->source()); |
| } |
| if (IsSlot(a->destination()) && !IsSlot(b->destination())) return false; |
| if (!IsSlot(a->destination()) && IsSlot(b->destination())) return true; |
| return a->destination().CompareCanonicalized(b->destination()); |
| } |
| |
| } // namespace |
| |
| |
| // Split multiple loads of the same constant or stack slot off into the second |
| // slot and keep remaining moves in the first slot. |
| void MoveOptimizer::FinalizeMoves(Instruction* instr) { |
| MoveOpVector& loads = local_vector(); |
| DCHECK(loads.empty()); |
| |
| ParallelMove* parallel_moves = instr->parallel_moves()[0]; |
| if (parallel_moves == nullptr) return; |
| // Find all the loads. |
| for (MoveOperands* move : *parallel_moves) { |
| if (move->IsRedundant()) continue; |
| if (move->source().IsConstant() || IsSlot(move->source())) { |
| loads.push_back(move); |
| } |
| } |
| if (loads.empty()) return; |
| // Group the loads by source, moving the preferred destination to the |
| // beginning of the group. |
| std::sort(loads.begin(), loads.end(), LoadCompare); |
| MoveOperands* group_begin = nullptr; |
| for (MoveOperands* load : loads) { |
| // New group. |
| if (group_begin == nullptr || |
| !load->source().EqualsCanonicalized(group_begin->source())) { |
| group_begin = load; |
| continue; |
| } |
| // Nothing to be gained from splitting here. |
| if (IsSlot(group_begin->destination())) continue; |
| // Insert new move into slot 1. |
| ParallelMove* slot_1 = instr->GetOrCreateParallelMove( |
| static_cast<Instruction::GapPosition>(1), code_zone()); |
| slot_1->AddMove(group_begin->destination(), load->destination()); |
| load->Eliminate(); |
| } |
| loads.clear(); |
| } |
| |
| } // namespace compiler |
| } // namespace internal |
| } // namespace v8 |