Jit/hir/analysis.cpp (628 lines of code) (raw):
// Copyright (c) Facebook, Inc. and its affiliates. (http://www.facebook.com)
#include "Jit/hir/analysis.h"
#include "Jit/dataflow.h"
#include "Jit/hir/hir.h"
#include "Jit/hir/memory_effects.h"
#include "Jit/hir/printer.h"
#include <memory>
namespace jit {
namespace hir {
const RegisterSet kEmptyRegSet;
std::ostream& operator<<(std::ostream& os, const RegisterSet& regs) {
fmt::print(os, "RegisterSet[{}] = {{", regs.size());
std::vector<Register*> sorted_regs{regs.begin(), regs.end()};
std::sort(sorted_regs.begin(), sorted_regs.end(), [](auto r1, auto r2) {
return r1->id() < r2->id();
});
auto sep = "";
for (auto reg : sorted_regs) {
fmt::print(os, "{}{}", sep, reg->name());
sep = ", ";
}
return os << "}";
}
bool isPassthrough(const Instr& instr) {
switch (instr.opcode()) {
case Opcode::kAssign:
case Opcode::kCheckExc:
case Opcode::kCheckField:
case Opcode::kCheckFreevar:
case Opcode::kCheckNeg:
case Opcode::kCheckVar:
case Opcode::kGuardType:
case Opcode::kRefineType:
case Opcode::kUseType:
return true;
// GuardIs returns a copy of its input with a more specialized type, so it
// looks like a passthrough instruction at first glance. However, its
// purpose is to verify that a runtime value is a specific object, breaking
// the dependency on the instruction that produced the runtime value. It's
// possible to augment RefcountInsertion to support this pattern but let's
// wait to see if more instructions need it before adding that complexity.
case Opcode::kGuardIs:
return false;
// Cast is pass-through except when we are casting to float, in which case
// we may coerce an incoming int to a new float. It's also not a pass
// thru when we return True/False to indicate success/failure.
case Opcode::kCast: {
auto& cast = static_cast<const Cast&>(instr);
return cast.pytype() != &PyFloat_Type && cast.iserror();
}
case Opcode::kBinaryOp:
case Opcode::kBuildSlice:
case Opcode::kBuildString:
case Opcode::kCallCFunc:
case Opcode::kCallEx:
case Opcode::kCallExKw:
case Opcode::kCallMethod:
case Opcode::kCallStatic:
case Opcode::kCallStaticRetVoid:
case Opcode::kCheckSequenceBounds:
case Opcode::kClearError:
case Opcode::kCompare:
case Opcode::kCompareBool:
case Opcode::kDoubleBinaryOp:
case Opcode::kFillTypeAttrCache:
case Opcode::kFormatValue:
case Opcode::kGetIter:
case Opcode::kGetTuple:
case Opcode::kImportFrom:
case Opcode::kImportName:
case Opcode::kInPlaceOp:
case Opcode::kInitialYield:
case Opcode::kIntBinaryOp:
case Opcode::kPrimitiveCompare:
case Opcode::kIntConvert:
case Opcode::kPrimitiveUnbox:
case Opcode::kInvokeIterNext:
case Opcode::kInvokeMethod:
case Opcode::kInvokeStaticFunction:
case Opcode::kIsErrStopAsyncIteration:
case Opcode::kIsInstance:
case Opcode::kIsSubtype:
case Opcode::kIsNegativeAndErrOccurred:
case Opcode::kIsTruthy:
case Opcode::kListAppend:
case Opcode::kListExtend:
case Opcode::kLoadArg:
case Opcode::kLoadArrayItem:
case Opcode::kLoadAttr:
case Opcode::kLoadAttrSpecial:
case Opcode::kLoadAttrSuper:
case Opcode::kLoadCellItem:
case Opcode::kLoadConst:
case Opcode::kLoadCurrentFunc:
case Opcode::kLoadEvalBreaker:
case Opcode::kLoadField:
case Opcode::kLoadFieldAddress:
case Opcode::kLoadFunctionIndirect:
case Opcode::kLoadGlobal:
case Opcode::kLoadGlobalCached:
case Opcode::kLoadMethod:
case Opcode::kLoadMethodSuper:
case Opcode::kLoadTupleItem:
case Opcode::kLoadTypeAttrCacheItem:
case Opcode::kLoadVarObjectSize:
case Opcode::kLongCompare:
case Opcode::kLongBinaryOp:
case Opcode::kMakeCell:
case Opcode::kMakeCheckedDict:
case Opcode::kMakeDict:
case Opcode::kMakeCheckedList:
case Opcode::kMakeFunction:
case Opcode::kMakeListTuple:
case Opcode::kMakeSet:
case Opcode::kMakeTupleFromList:
case Opcode::kMergeDictUnpack:
case Opcode::kMergeSetUnpack:
case Opcode::kPhi:
case Opcode::kPrimitiveBox:
case Opcode::kPrimitiveUnaryOp:
case Opcode::kRepeatList:
case Opcode::kRepeatTuple:
case Opcode::kRunPeriodicTasks:
case Opcode::kSetCurrentAwaiter:
case Opcode::kSetDictItem:
case Opcode::kSetSetItem:
case Opcode::kStealCellItem:
case Opcode::kStoreArrayItem:
case Opcode::kStoreAttr:
case Opcode::kStoreSubscr:
case Opcode::kTpAlloc:
case Opcode::kUnaryOp:
case Opcode::kUnpackExToTuple:
case Opcode::kVectorCall:
case Opcode::kVectorCallKW:
case Opcode::kVectorCallStatic:
case Opcode::kWaitHandleLoadCoroOrResult:
case Opcode::kWaitHandleLoadWaiter:
case Opcode::kYieldAndYieldFrom:
case Opcode::kYieldFrom:
case Opcode::kYieldValue:
return false;
case Opcode::kBatchDecref:
case Opcode::kBeginInlinedFunction:
case Opcode::kBranch:
case Opcode::kCondBranch:
case Opcode::kCondBranchIterNotDone:
case Opcode::kCondBranchCheckType:
case Opcode::kDecref:
case Opcode::kDeleteAttr:
case Opcode::kDeleteSubscr:
case Opcode::kDeopt:
case Opcode::kDeoptPatchpoint:
case Opcode::kEndInlinedFunction:
case Opcode::kGuard:
case Opcode::kHintType:
case Opcode::kSnapshot:
case Opcode::kIncref:
case Opcode::kInitFunction:
case Opcode::kInitListTuple:
case Opcode::kReturn:
case Opcode::kSetCellItem:
case Opcode::kSetFunctionAttr:
case Opcode::kStoreField:
case Opcode::kXDecref:
case Opcode::kXIncref:
case Opcode::kRaiseAwaitableError:
case Opcode::kRaise:
case Opcode::kRaiseStatic:
case Opcode::kWaitHandleRelease:
JIT_CHECK(false, "Opcode %s has no output", instr.opname());
}
JIT_CHECK(false, "Bad opcode %d", static_cast<int>(instr.opcode()));
}
Register* modelReg(Register* reg) {
auto orig_reg = reg;
while (isPassthrough(*reg->instr())) {
reg = reg->instr()->GetOperand(0);
JIT_DCHECK(reg != orig_reg, "Hit cycle while looking for model reg");
}
return reg;
}
static bool isSingleCInt(Type t) {
return t <= TCInt8 || t <= TCUInt8 || t <= TCInt16 || t <= TCUInt16 ||
t <= TCInt32 || t <= TCUInt32 || t <= TCInt64 || t <= TCUInt64;
}
bool registerTypeMatches(Type op_type, OperandType expected_type) {
switch (expected_type.kind) {
case Constraint::kType:
return op_type <= expected_type.type;
case Constraint::kTupleExactOrCPtr:
return op_type <= TTupleExact || op_type <= TCPtr;
case Constraint::kListOrChkList:
return op_type <= TList ||
(op_type.hasTypeSpec() &&
_PyCheckedList_TypeCheck(op_type.typeSpec()));
case Constraint::kDictOrChkDict:
return op_type <= TDict ||
(op_type.hasTypeSpec() &&
_PyCheckedDict_TypeCheck(op_type.typeSpec()));
case Constraint::kOptObjectOrCIntOrCBool:
return op_type <= TOptObject || op_type <= TCInt || op_type <= TCBool;
case Constraint::kOptObjectOrCInt:
return op_type <= TOptObject || op_type <= TCInt;
case Constraint::kMatchAllAsCInt:
return isSingleCInt(op_type);
case Constraint::kMatchAllAsPrimitive:
return isSingleCInt(op_type) || op_type <= TCBool ||
op_type <= TCDouble || op_type <= TCEnum || op_type <= TCPtr;
}
JIT_CHECK(false, "unknown constraint");
return false;
}
bool operandsMustMatch(OperandType op_type) {
switch (op_type.kind) {
case Constraint::kMatchAllAsCInt:
case Constraint::kMatchAllAsPrimitive:
return true;
case Constraint::kType:
case Constraint::kTupleExactOrCPtr:
case Constraint::kListOrChkList:
case Constraint::kDictOrChkDict:
case Constraint::kOptObjectOrCInt:
case Constraint::kOptObjectOrCIntOrCBool:
return false;
}
JIT_CHECK(false, "unknown constraint");
return false;
}
bool funcTypeChecks(const Function& func, std::ostream& err) {
for (auto& block : func.cfg.blocks) {
for (const Instr& instr : block) {
if (instr.NumOperands() > 1 &&
operandsMustMatch(instr.GetOperandType(0))) {
Type join = TBottom;
for (std::size_t i = 0; i < instr.NumOperands(); i++) {
JIT_DCHECK(
operandsMustMatch(instr.GetOperandType(i)),
"Inconsistent operand type constraint");
join |= instr.GetOperand(i)->type();
}
OperandType expected_type = instr.GetOperandType(0);
if (!registerTypeMatches(join, expected_type)) {
fmt::print(
err,
"TYPE MISMATCH in bb {} of '{}'\nInstr '{}' expected "
"join of operands of type {} to subclass '{}'\n",
block.id,
func.fullname,
instr,
join,
expected_type);
return false;
}
} else {
for (std::size_t i = 0; i < instr.NumOperands(); i++) {
Register* op = instr.GetOperand(i);
OperandType expected_type = instr.GetOperandType(i);
if (!registerTypeMatches(op->type(), expected_type)) {
fmt::print(
err,
"TYPE MISMATCH in bb {} of '{}'\nInstr '{}' expected "
"operand {} to be of type {} "
"but got {} from '{}'\n",
block.id,
func.fullname,
instr,
i,
expected_type,
op->type(),
*op->instr());
return false;
}
}
}
}
}
return true;
}
void DataflowAnalysis::AddBasicBlock(const BasicBlock* cfg_block) {
auto res = df_blocks_.emplace(
std::piecewise_construct,
std::forward_as_tuple(cfg_block),
std::forward_as_tuple());
auto& df_block = res.first->second;
df_analyzer_.AddBlock(df_block);
setUninitialized(&df_block);
std::unordered_set<Register*> gen, kill;
ComputeGenKill(cfg_block, gen, kill);
for (auto reg : gen) {
df_analyzer_.SetBlockGenBit(df_block, reg);
}
for (auto reg : kill) {
df_analyzer_.SetBlockKillBit(df_block, reg);
}
}
void DataflowAnalysis::Initialize() {
// Add all registers -- this sets up the correct number of bits for the
// analysis
num_bits_ = irfunc_.env.GetRegisters().size();
for (const auto& it : irfunc_.env.GetRegisters()) {
df_analyzer_.AddObject(it.second.get());
}
// Compute the initial state for each block
for (const auto& cfg_block : irfunc_.cfg.blocks) {
AddBasicBlock(&cfg_block);
}
// Set up dataflow graph
df_analyzer_.AddBlock(df_entry_);
df_analyzer_.SetEntryBlock(df_entry_);
df_analyzer_.AddBlock(df_exit_);
df_analyzer_.SetExitBlock(df_exit_);
for (const auto& cfg_block : irfunc_.cfg.blocks) {
auto& df_block = df_blocks_[&cfg_block];
if (&cfg_block == irfunc_.cfg.entry_block) {
df_entry_.ConnectTo(df_block);
}
if (cfg_block.out_edges().empty()) {
df_block.ConnectTo(df_exit_);
} else {
for (auto cfg_edge : cfg_block.out_edges()) {
auto succ_cfg_block = cfg_edge->to();
JIT_CHECK(
df_blocks_.count(succ_cfg_block),
"succ_cfg_block has to be in the hash table df_blocks_.");
auto& succ_df_block = df_blocks_.at(succ_cfg_block);
df_block.ConnectTo(succ_df_block);
}
}
}
}
RegisterSet DataflowAnalysis::GetIn(const BasicBlock* cfg_block) {
RegisterSet in;
const auto& df_block = df_blocks_[cfg_block];
df_analyzer_.forEachBlockIn(df_block, [&](Register* r) { in.insert(r); });
return in;
}
RegisterSet DataflowAnalysis::GetOut(const BasicBlock* cfg_block) {
RegisterSet out;
const auto& df_block = df_blocks_[cfg_block];
df_analyzer_.forEachBlockOut(df_block, [&](Register* r) { out.insert(r); });
return out;
}
void DataflowAnalysis::dump() {
if (!g_debug) {
return;
}
std::string out = fmt::format("{} complete:\n", name());
for (auto& block : irfunc_.cfg.blocks) {
format_to(out, " bb {}\n", block.id);
auto format_set = [&](const RegisterSet& regs) {
for (auto reg : regs) {
format_to(out, " {}\n", reg->name());
}
};
format_to(out, " In:\n");
format_set(GetIn(&block));
format_to(out, " Out:\n");
format_set(GetOut(&block));
format_to(out, "\n");
}
JIT_DLOG("%s", out);
}
void BackwardDataflowAnalysis::Run() {
Initialize();
std::list<jit::optimizer::DataFlowBlock*> blocks;
for (auto& it : df_blocks_) {
if (&it.second != &df_entry_) {
blocks.emplace_back(&it.second);
}
}
while (!blocks.empty()) {
auto block = blocks.front();
blocks.pop_front();
auto new_out = ComputeNewOut(block);
bool changed = (new_out != block->out_);
block->out_ = std::move(new_out);
auto new_in = ComputeNewIn(block);
changed |= (new_in != block->in_);
block->in_ = std::move(new_in);
if (changed) {
std::copy(
block->pred_.begin(), block->pred_.end(), std::back_inserter(blocks));
}
}
}
void ForwardDataflowAnalysis::Run() {
Initialize();
std::list<jit::optimizer::DataFlowBlock*> blocks;
for (auto& it : df_blocks_) {
if (&it.second != &df_exit_) {
blocks.emplace_back(&it.second);
}
}
while (!blocks.empty()) {
auto block = blocks.front();
blocks.pop_front();
auto new_in = ComputeNewIn(block);
bool changed = (new_in != block->in_);
block->in_ = std::move(new_in);
auto new_out = ComputeNewOut(block);
changed |= (new_out != block->out_);
block->out_ = std::move(new_out);
if (changed) {
blocks.insert(blocks.end(), block->succ_.begin(), block->succ_.end());
}
}
}
template <typename OutputFunc, typename UseFunc>
static void analyzeInstrLiveness(
const Instr& instr,
OutputFunc define_output,
UseFunc use) {
if (auto output = instr.GetOutput()) {
define_output(output);
}
if (instr.IsPhi()) {
// Phi uses happen at the end of the predecessor block.
return;
}
instr.visitUses([&](Register* reg) {
use(reg);
return true;
});
if (instr.numEdges() > 0) {
// Mark any Phi inputs on successors to this block as live. When we switch
// to Branch passing arguments to blocks rather than using Phis, this will
// happen naturally as the Branch is processed.
for (size_t i = 0, n = instr.numEdges(); i < n; ++i) {
auto succ = instr.successor(i);
int phi_idx = -1;
for (auto& succ_instr : *succ) {
if (!succ_instr.IsPhi()) {
break;
}
auto& phi = static_cast<const Phi&>(succ_instr);
if (phi_idx == -1) {
phi_idx = phi.blockIndex(instr.block());
}
use(phi.GetOperand(phi_idx));
}
}
}
}
LivenessAnalysis::LastUses LivenessAnalysis::GetLastUses() {
LastUses last_uses;
for (auto& pair : df_blocks_) {
auto block = pair.first;
auto live = GetOut(block);
for (auto it = block->rbegin(); it != block->rend(); ++it) {
auto& instr = *it;
analyzeInstrLiveness(
instr,
[&](Register* output) {
if (live.erase(output) == 0) {
// output isn't live after instr. It's dead and dies right after
// definition.
last_uses[&instr].emplace(output);
}
},
[&](Register* value) {
if (live.emplace(value).second) {
// value isn't live after instr, so this is a last use.
last_uses[&instr].emplace(value);
}
});
}
}
return last_uses;
}
void LivenessAnalysis::ComputeGenKill(
const BasicBlock* cfg_block,
RegisterSet& gen,
RegisterSet& kill) {
for (auto it = cfg_block->rbegin(); it != cfg_block->rend(); ++it) {
analyzeInstrLiveness(
*it,
[&](Register* output) {
kill.insert(output);
gen.erase(output);
},
[&](Register* use) { gen.insert(use); });
}
}
jit::util::BitVector LivenessAnalysis::ComputeNewIn(
const jit::optimizer::DataFlowBlock* block) {
jit::util::BitVector new_in(num_bits_);
new_in = block->gen_ | (block->out_ - block->kill_);
return new_in;
}
jit::util::BitVector LivenessAnalysis::ComputeNewOut(
const jit::optimizer::DataFlowBlock* block) {
jit::util::BitVector new_out(num_bits_);
for (auto& succ : block->succ_) {
new_out |= succ->in_;
}
return new_out;
}
void LivenessAnalysis::setUninitialized(jit::optimizer::DataFlowBlock*) {
// Do nothing.
}
bool LivenessAnalysis::IsLiveIn(const BasicBlock* cfg_block, Register* reg) {
const auto& df_block = df_blocks_[cfg_block];
return df_analyzer_.GetBlockInBit(df_block, reg);
}
bool LivenessAnalysis::IsLiveOut(const BasicBlock* cfg_block, Register* reg) {
const auto& df_block = df_blocks_[cfg_block];
return df_analyzer_.GetBlockOutBit(df_block, reg);
}
AssignmentAnalysis::AssignmentAnalysis(const Function& irfunc, bool is_definite)
: ForwardDataflowAnalysis(irfunc), args_(), is_definite_(is_definite) {
for (const auto& instr : *irfunc_.cfg.entry_block) {
if (instr.IsLoadArg()) {
args_.insert(instr.GetOutput());
}
}
}
bool AssignmentAnalysis::IsAssignedIn(
const BasicBlock* cfg_block,
Register* reg) {
const auto& df_block = df_blocks_[cfg_block];
return df_analyzer_.GetBlockInBit(df_block, reg);
}
bool AssignmentAnalysis::IsAssignedOut(
const BasicBlock* cfg_block,
Register* reg) {
const auto& df_block = df_blocks_[cfg_block];
return df_analyzer_.GetBlockOutBit(df_block, reg);
}
void AssignmentAnalysis::ComputeGenKill(
const BasicBlock* block,
RegisterSet& gen,
RegisterSet& /* kill */) {
gen = args_;
for (const auto& instr : *block) {
auto output = instr.GetOutput();
if (output != nullptr) {
gen.insert(output);
}
}
}
jit::util::BitVector AssignmentAnalysis::ComputeNewIn(
const jit::optimizer::DataFlowBlock* block) {
if (block->pred_.empty()) {
jit::util::BitVector new_in(num_bits_);
return new_in;
}
auto it = block->pred_.begin();
auto pred = *it++;
jit::util::BitVector new_in = pred->out_;
while (it != block->pred_.end()) {
if (is_definite_) {
new_in &= (*it)->out_;
} else {
new_in |= (*it)->out_;
}
it++;
}
return new_in;
}
jit::util::BitVector AssignmentAnalysis::ComputeNewOut(
const jit::optimizer::DataFlowBlock* block) {
jit::util::BitVector new_out(num_bits_);
new_out = block->gen_ | (block->in_ - block->kill_);
return new_out;
}
void AssignmentAnalysis::setUninitialized(
jit::optimizer::DataFlowBlock* block) {
if (is_definite_) {
block->out_.fill(true);
}
}
DominatorAnalysis::DominatorAnalysis(const Function& irfunc) : idoms_{} {
// Calculate immediate dominators with the iterative two-finger algorithm.
// When it terminates, idoms_[block-id] will contain the block-id of the
// immediate dominator of each block. idoms_[start] will be nullptr. This is
// the general algorithm but it will only loop twice for loop-free graphs.
std::vector<BasicBlock*> rpo = irfunc.cfg.GetRPOTraversal();
// Map block ids to their index in the RPO traversal
std::unordered_map<int, int> rpo_index{};
for (size_t i = 0; i < rpo.size(); i++) {
rpo_index[rpo[i]->id] = i;
}
auto start = rpo.begin();
const BasicBlock* entry = *start;
idoms_[entry->id] = entry;
start++;
for (bool changed = true; changed;) {
changed = false;
for (auto it = start; it != rpo.end(); it++) {
const BasicBlock* block = *it;
auto predIter = block->in_edges().begin();
auto predEnd = block->in_edges().end();
// pred1 = any already-processed predecessor
auto pred1 = const_cast<const BasicBlock*>((*predIter)->from());
while (!idoms_[pred1->id]) {
JIT_DCHECK(
predIter != predEnd,
"There should be an already-processed predecessor since we're "
"iterating in RPO");
pred1 = (*(++predIter))->from();
}
// for all other already-processed predecessors pred2 of block
for (++predIter; predIter != predEnd; ++predIter) {
auto pred2 = const_cast<const BasicBlock*>((*predIter)->from());
if (pred2 == pred1 || !idoms_[pred2->id]) {
continue;
}
// find earliest common predecessor of pred1 and pred2
// (lower RPO ids are earlier in flow and in dom-tree).
do {
while (rpo_index[pred1->id] < rpo_index[pred2->id]) {
pred2 = idoms_[pred2->id];
}
while (rpo_index[pred2->id] < rpo_index[pred1->id]) {
pred1 = idoms_[pred1->id];
}
} while (pred1 != pred2);
}
if (idoms_[block->id] != pred1) {
idoms_[block->id] = pred1;
changed = true;
}
}
}
idoms_[entry->id] = nullptr;
}
RegisterTypeHints::RegisterTypeHints(const Function& irfunc)
: dom_hint_{}, doms_{irfunc} {
for (const auto& block : irfunc.cfg.blocks) {
for (const auto& instr : block) {
if (instr.IsHintType()) {
for (size_t i = 0; i < instr.NumOperands(); i++) {
dom_hint_[instr.GetOperand(i)][block.id] = &instr;
}
} else if (instr.IsPhi()) {
dom_hint_[instr.GetOutput()][block.id] = &instr;
}
}
}
}
const Instr* RegisterTypeHints::dominatingTypeHint(
Register* reg,
const BasicBlock* block) {
// Make sure we don't default construct the map for untracked registers
auto it = dom_hint_.find(reg);
if (it == dom_hint_.end()) {
return nullptr;
}
std::unordered_map<int, const Instr*> hint_types = it->second;
// Look for the first type hint that dominates the passed in block
while (!hint_types[block->id]) {
block = doms_.immediateDominator(block);
if (block == nullptr) {
return nullptr;
}
}
return hint_types[block->id];
}
} // namespace hir
} // namespace jit