Jit/deopt.cpp (326 lines of code) (raw):
// Copyright (c) Facebook, Inc. and its affiliates. (http://www.facebook.com)
#include "Jit/deopt.h"
#include "pycore_shadow_frame.h"
#include "Jit/codegen/gen_asm.h"
#include "Jit/hir/analysis.h"
#include "Jit/jit_rt.h"
#include "Jit/runtime.h"
#include "Jit/util.h"
using jit::codegen::PhyLocation;
namespace jit {
namespace {
// Set of interned strings for deopt descriptions.
std::unordered_set<std::string> s_descrs;
} // namespace
hir::ValueKind deoptValueKind(hir::Type type) {
if (type <= jit::hir::TCBool) {
return jit::hir::ValueKind::kBool;
}
if (type <= jit::hir::TCDouble) {
return jit::hir::ValueKind::kDouble;
}
if (type <= jit::hir::TCEnum) {
return jit::hir::ValueKind::kSigned;
}
// TODO(bsimmers): The type predicates here are gross and indicate a deeper
// problem with how we're using Types earlier in the pipeline: we use
// `LoadNull` to zero-initialize locals with primitive types (currently done
// in SSAify). It works fine at runtime and a proper fix likely involves
// reworking HIR's support for constant values, so we paper over the issue
// here for the moment.
if (type.couldBe(jit::hir::TCUnsigned | jit::hir::TCSigned)) {
if (type <= (jit::hir::TCUnsigned | jit::hir::TNullptr)) {
return jit::hir::ValueKind::kUnsigned;
}
if (type <= (jit::hir::TCSigned | jit::hir::TNullptr)) {
return jit::hir::ValueKind::kSigned;
}
} else if (type.couldBe(jit::hir::TCDouble)) {
return jit::hir::ValueKind::kDouble;
}
JIT_CHECK(
type <= jit::hir::TOptObject, "Unexpected type %s in deopt value", type);
return jit::hir::ValueKind::kObject;
}
const char* deoptReasonName(DeoptReason reason) {
switch (reason) {
#define REASON(name) \
case DeoptReason::k##name: \
return #name;
DEOPT_REASONS(REASON)
#undef REASON
}
JIT_CHECK(false, "Invalid DeoptReason %d", static_cast<int>(reason));
}
const char* deoptActionName(DeoptAction action) {
switch (action) {
case DeoptAction::kResumeInInterpreter:
return "ResumeInInterpreter";
case DeoptAction::kUnwind:
return "Unwind";
}
JIT_CHECK(false, "Invalid DeoptAction %d", static_cast<int>(action));
}
PyObject* MemoryView::read(const LiveValue& value, bool borrow) const {
uint64_t raw;
PhyLocation loc = value.location;
if (loc.is_register()) {
raw = regs[loc.loc];
} else {
uint64_t rbp = regs[PhyLocation::RBP];
// loc.loc is negative when loc is a memory location relative to RBP
raw = *(reinterpret_cast<uint64_t*>(rbp + loc.loc));
}
switch (value.value_kind) {
case jit::hir::ValueKind::kSigned:
JIT_CHECK(!borrow, "borrow can only get raw pyobjects");
return PyLong_FromSsize_t((Py_ssize_t)raw);
case jit::hir::ValueKind::kUnsigned:
JIT_CHECK(!borrow, "borrow can only get raw pyobjects");
return PyLong_FromSize_t(raw);
case hir::ValueKind::kDouble:
JIT_CHECK(!borrow, "borrow can only get raw pyobjects");
return PyFloat_FromDouble(raw);
case jit::hir::ValueKind::kBool: {
JIT_CHECK(!borrow, "borrow can only get raw pyobjects");
PyObject* res = raw ? Py_True : Py_False;
Py_INCREF(res);
return res;
}
case jit::hir::ValueKind::kObject: {
PyObject* res = reinterpret_cast<PyObject*>(raw);
if (!borrow) {
Py_XINCREF(res);
}
return res;
}
}
return nullptr;
}
static void reifyLocalsplus(
PyFrameObject* frame,
const DeoptMetadata& meta,
const DeoptFrameMetadata& frame_meta,
const MemoryView& mem) {
for (std::size_t i = 0; i < frame_meta.localsplus.size(); i++) {
auto value = meta.getLocalValue(i, frame_meta);
if (value == nullptr) {
// Value is dead
Py_CLEAR(frame->f_localsplus[i]);
continue;
}
PyObject* obj = mem.read(*value);
Py_XSETREF(frame->f_localsplus[i], obj);
}
}
// When we are deoptimizing a JIT-compiled function that contains an
// optimizable LoadMethod, we need to be able to know whether or not the
// LoadMethod returned a bound method object in order to properly reconstruct
// the stack for the interpreter. Unfortunately, this can only be determined at
// runtime, and can be inferred from the value pointed to by call_method_kind.
static bool isUnboundMethod(
const LiveValue& value,
const JITRT_CallMethodKind* call_method_kind) {
if (value.source != LiveValue::Source::kOptimizableLoadMethod) {
return false;
}
return *call_method_kind != JITRT_CALL_KIND_OTHER;
}
static void reifyStack(
PyFrameObject* frame,
const DeoptMetadata& meta,
const DeoptFrameMetadata& frame_meta,
const MemoryView& mem,
const JITRT_CallMethodKind* call_method_kind) {
frame->f_stacktop = frame->f_valuestack + frame_meta.stack.size();
for (int i = frame_meta.stack.size() - 1; i >= 0; i--) {
const auto& value = meta.getStackValue(i, frame_meta);
if (value.isLoadMethodResult()) {
PyObject* callable = mem.read(value);
if (isUnboundMethod(value, call_method_kind)) {
// We avoided creating a bound method object. The interpreter
// expects the stack to look like:
//
// callable
// self
// arg1
// ...
// argN <-- TOS
PyObject* receiver = mem.read(meta.getStackValue(i - 1, frame_meta));
frame->f_valuestack[i - 1] = callable;
frame->f_valuestack[i] = receiver;
} else {
// Otherwise, the interpreter expects the stack look like:
//
// nullptr
// bound_method
// arg1
// ...
// argN <-- TOS
frame->f_valuestack[i - 1] = nullptr;
frame->f_valuestack[i] = callable;
}
i--;
} else {
PyObject* obj = mem.read(value);
frame->f_valuestack[i] = obj;
}
}
}
Ref<> profileDeopt(
std::size_t deopt_idx,
const DeoptMetadata& meta,
const MemoryView& mem) {
const LiveValue* live_val = meta.getGuiltyValue();
auto guilty_obj =
Ref<>::steal(live_val == nullptr ? nullptr : mem.read(*live_val, false));
codegen::NativeGeneratorFactory::runtime()->recordDeopt(
deopt_idx, guilty_obj.get());
return guilty_obj;
}
static void reifyBlockStack(
PyFrameObject* frame,
const jit::hir::BlockStack& block_stack) {
std::size_t bs_size = block_stack.size();
frame->f_iblock = bs_size;
for (std::size_t i = 0; i < bs_size; i++) {
const auto& block = block_stack.at(i);
frame->f_blockstack[i].b_type = block.opcode;
frame->f_blockstack[i].b_handler = block.handler_off;
frame->f_blockstack[i].b_level = block.stack_level;
}
}
void reifyFrame(
PyFrameObject* frame,
const DeoptMetadata& meta,
const DeoptFrameMetadata& frame_meta,
const uint64_t* regs,
const JITRT_CallMethodKind* call_method_kind) {
frame->f_locals = NULL;
frame->f_trace = NULL;
frame->f_trace_opcodes = 0;
frame->f_trace_lines = 1;
frame->f_executing = 0;
// Interpreter loop will handle filling this in
frame->f_lineno = frame->f_code->co_firstlineno;
// Instruction pointer
if (frame_meta.next_instr_offset == 0) {
frame->f_lasti = -1;
} else {
frame->f_lasti = frame_meta.next_instr_offset - sizeof(_Py_CODEUNIT);
}
MemoryView mem{regs};
reifyLocalsplus(frame, meta, frame_meta, mem);
reifyStack(frame, meta, frame_meta, mem, call_method_kind);
reifyBlockStack(frame, frame_meta.block_stack);
// Generator/frame linkage happens in `materializePyFrame` in frame.cpp
}
static DeoptReason getDeoptReason(const jit::hir::DeoptBase& instr) {
switch (instr.opcode()) {
case jit::hir::Opcode::kCheckVar: {
return DeoptReason::kUnhandledUnboundLocal;
}
case jit::hir::Opcode::kCheckFreevar: {
return DeoptReason::kUnhandledUnboundFreevar;
}
case jit::hir::Opcode::kCheckField: {
return DeoptReason::kUnhandledNullField;
}
case jit::hir::Opcode::kDeopt:
case jit::hir::Opcode::kDeoptPatchpoint:
case jit::hir::Opcode::kGuard:
case jit::hir::Opcode::kGuardIs:
case jit::hir::Opcode::kGuardType: {
return DeoptReason::kGuardFailure;
}
case jit::hir::Opcode::kRaise: {
auto& raise = static_cast<const hir::Raise&>(instr);
switch (raise.kind()) {
case hir::Raise::Kind::kReraise:
return DeoptReason::kReraise;
case hir::Raise::Kind::kRaiseWithExc:
case hir::Raise::Kind::kRaiseWithExcAndCause:
return DeoptReason::kRaise;
}
JIT_CHECK(false, "invalid raise kind");
}
case jit::hir::Opcode::kRaiseStatic: {
return DeoptReason::kRaiseStatic;
}
default: {
return DeoptReason::kUnhandledException;
}
}
}
DeoptMetadata DeoptMetadata::fromInstr(
const jit::hir::DeoptBase& instr,
const std::unordered_set<const jit::hir::Instr*>& optimizable_lms,
CodeRuntime* code_rt) {
auto get_source = [&](jit::hir::Register* reg) {
reg = hir::modelReg(reg);
auto instr = reg->instr();
if (instr->IsLoadMethod()) {
if (optimizable_lms.count(instr)) {
return LiveValue::Source::kOptimizableLoadMethod;
} else {
return LiveValue::Source::kUnoptimizableLoadMethod;
}
}
return LiveValue::Source::kUnknown;
};
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
DeoptMetadata meta{.code_rt = code_rt};
#pragma GCC diagnostic pop
std::unordered_map<jit::hir::Register*, int> reg_idx;
int i = 0;
for (const auto& reg_state : instr.live_regs()) {
auto reg = reg_state.reg;
LiveValue lv = {
// location will be filled in once we've generated code
.location = 0,
.ref_kind = reg_state.ref_kind,
.value_kind = reg_state.value_kind,
.source = get_source(reg),
};
meta.live_values.emplace_back(std::move(lv));
reg_idx[reg] = i;
i++;
}
auto get_reg_idx = [®_idx](jit::hir::Register* reg) {
if (reg == nullptr) {
return -1;
}
auto it = reg_idx.find(reg);
JIT_CHECK(it != reg_idx.end(), "register %s not live", reg->name());
return it->second;
};
auto populate_localsplus =
[get_reg_idx](DeoptFrameMetadata& meta, hir::FrameState* fs) {
std::size_t nlocals = fs->locals.size();
std::size_t ncells = fs->cells.size();
meta.localsplus.resize(nlocals + ncells, -1);
for (std::size_t i = 0; i < nlocals; i++) {
meta.localsplus[i] = get_reg_idx(fs->locals[i]);
}
for (std::size_t i = 0; i < ncells; i++) {
meta.localsplus[nlocals + i] = get_reg_idx(fs->cells[i]);
}
};
auto populate_stack = [get_reg_idx](
DeoptFrameMetadata& meta, hir::FrameState* fs) {
std::unordered_set<jit::hir::Register*> lms_on_stack;
for (auto& reg : fs->stack) {
if (reg->instr()->IsLoadMethod()) {
// Our logic for reconstructing the Python stack assumes that if a
// value on the stack was produced by a LoadMethod instruction, it
// corresponds to the output of a LOAD_METHOD opcode and will
// eventually be consumed by a CALL_METHOD. That doesn't technically
// have to be true, but it's our contention that the CPython
// compiler will never produce bytecode that would contradict this.
auto result = lms_on_stack.emplace(reg);
JIT_CHECK(
result.second,
"load method results may only appear in one stack slot");
}
meta.stack.emplace_back(get_reg_idx(reg));
}
};
auto fs = instr.frameState();
meta.inline_depth = fs->inlineDepth();
int num_frames = meta.inline_depth;
meta.frame_meta.resize(num_frames + 1); // +1 for caller
for (hir::FrameState* frame = fs; frame != NULL; frame = frame->parent) {
int i = num_frames--;
// Translate locals and cells
populate_localsplus(meta.frame_meta.at(i), frame);
populate_stack(meta.frame_meta.at(i), frame);
meta.frame_meta.at(i).block_stack = frame->block_stack;
meta.frame_meta.at(i).next_instr_offset = frame->next_instr_offset;
meta.frame_meta.at(i).code = frame->code.get();
}
if (hir::Register* guilty_reg = instr.guiltyReg()) {
meta.guilty_value = get_reg_idx(guilty_reg);
}
meta.nonce = instr.nonce();
meta.reason = getDeoptReason(instr);
JIT_CHECK(
meta.reason != DeoptReason::kUnhandledNullField ||
meta.guilty_value != -1,
"Guilty value is required for UnhandledNullField deopts");
if (meta.reason == DeoptReason::kGuardFailure || fs->hasTryBlock()) {
meta.action = DeoptAction::kResumeInInterpreter;
}
if (auto check = dynamic_cast<const hir::CheckBaseWithName*>(&instr)) {
meta.eh_name = check->name();
}
std::string descr = instr.descr();
if (descr.empty()) {
descr = hir::kOpcodeNames[static_cast<size_t>(instr.opcode())];
}
{
ThreadedCompileSerialize guard;
meta.descr = s_descrs.emplace(descr).first->c_str();
}
return meta;
}
} // namespace jit