Source code drop for the Actionscript virtual machine in the Adobe Flash Player for December 2013
417 lines
16 KiB
C++
417 lines
16 KiB
C++
/* -*- Mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 4 -*- */
|
|
/* vi: set ts=4 sw=4 expandtab: (add to ~/.vimrc: set modeline modelines=5) */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "avmplus.h"
|
|
|
|
#ifdef VMCFG_NANOJIT
|
|
#include "CodegenLIR.h"
|
|
#include "FrameState.h"
|
|
#include "exec-osr.h"
|
|
|
|
/*
|
|
|
|
OSR Design notes
|
|
|
|
OSR adds a new compilation threshold parameter to the existing RM_mixed
|
|
runmode. I chose not to add a new Runmode, just to keep the configuration
|
|
options simpler. There is a ripple effect: Runmode, AvmCore for default
|
|
values, ShellCoreSettings for commandline values, plus Flash settings
|
|
management. A new osr_threshold setting is added; the default value is 0
|
|
which is the existing static heuristic.
|
|
|
|
Static heuristic means never interpret static initializers, and eagerly
|
|
compile all other functions on first invocation. If the JIT fails, the
|
|
function reverts to the interpreter forever.
|
|
|
|
Generally, "invocation" means calls plus loop-backedges; we count both
|
|
with a single countdown counter.
|
|
|
|
New commandline arg -osr=count sets Config.osr_threshold to count. Default
|
|
is count=0, which means use the static heuristic.
|
|
|
|
If osr_threshold is 1 or higher, we compile after that many interpreter
|
|
invocations. The value 1 means we JIT compile on the second invocation,
|
|
because the first call triggers causes verification and the initial
|
|
interpreter invocation.
|
|
|
|
Limitations
|
|
|
|
We don't handle methods with exceptions because cloning and setting up
|
|
a new ExceptionFrame and jmp_buf seems too complicated. Instead we JIT
|
|
these functions eagerly or not-at-all, according to the static heuristic.
|
|
|
|
The JIT can fail if a method's stack frame is too large. We use a conservative
|
|
check for this case in OSR::isSupported(), and don't use OSR compilation if
|
|
the JIT could fail. Instead we revert to the static heuristic for such
|
|
methods.
|
|
|
|
Future
|
|
|
|
The interaction between -Ojit, -Dinterp, and -osr is redundant. If
|
|
-osr=1 meant jit-on-first-call, then -Ojit could be expressed in terms of
|
|
osr_threshold. Similarly, -Dinterp could be emulated by
|
|
forcing OSR::isSupported() to return false for every function.
|
|
|
|
TODO
|
|
- (done) factor out patch to look up FrameState and CodegenLabel by byte* pc, not int pc
|
|
- (done) factor out patch to capture activation scopes separately
|
|
- (done) combine invocation count and loop count
|
|
- (done) commandline configurable thresholds
|
|
- (done) count down to zero instead of up to threshold
|
|
- (active) move the osr branch earlier in the prolog
|
|
- Performance testing
|
|
- handle methods with 2+ loop targets. Currently we leak code if we OSR via
|
|
one loop, then the other, which is possible if the method has >1 stack
|
|
frame active in the interpreter. A weird-but-simple test could repeatedly
|
|
do this and cause OOM (TODO: write the test).
|
|
- ensure debugEnter/Exit pairing is correct
|
|
https://bugzilla.mozilla.org/show_bug.cgi?id=539094#c10
|
|
- disable ScopeWriter for subsequent verify passes. (need a new bug)
|
|
- add default threshold for -osr with no given threshold.
|
|
|
|
From testing
|
|
- Why is osr repo slower than tr-tip with osr not in use? See sunspider/math-cordic.as,math-partial-sums.as,math-spectral-norm.as,s3d-raytrace.as
|
|
- Not sure why osr repo seems faster with -osr not in use on regexp-dna.as?
|
|
- With -osr=50 jsbench/typed/FFT.as,LUFact.as,SOR.as are 5-15% slower than without -osr parameter set
|
|
- With -osr=50 jsbench/SparseMatmult.as is 20% faster than without -osr parameter set
|
|
|
|
*/
|
|
|
|
namespace avmplus
|
|
{
|
|
int32_t OSR::parseConfig(const char *str)
|
|
{
|
|
// parse the commandline threshold
|
|
int32_t threshold;
|
|
if (VMPI_sscanf(str, "%d", &threshold) != 1 ||
|
|
threshold < 0)
|
|
return -1;
|
|
return threshold;
|
|
}
|
|
|
|
OSR::OSR(const uint8_t *osr_pc, Atom* interp_frame)
|
|
: osr_pc(osr_pc),
|
|
interp_frame(interp_frame),
|
|
jit_frame_state(NULL)
|
|
{}
|
|
|
|
void OSR::setFrameState(FrameState *fs)
|
|
{
|
|
jit_frame_state = fs;
|
|
}
|
|
|
|
const uint8_t* OSR::osrPc() const
|
|
{
|
|
return osr_pc;
|
|
}
|
|
|
|
// OSR is supported generally only in runmode RM_mixed. We don't support
|
|
// methods with try/catch blocks because of the complexity of establishing
|
|
// a new ExceptionFrame and jmp_buf. We also don't support methods for which
|
|
// a previous compilation attempt failed, or for which failure can be predicted.
|
|
//
|
|
// We must only OSR methods that will execute with a BugCompatibility object
|
|
// such that interpreter/compiler divergences are corrected. Builtin methods
|
|
// are invoked with bug compatibility inherited from the innermost non-builtin
|
|
// function on the call chain, and thus may vary from call to call. Non-builtins
|
|
// should always execute with bug compatibility taken from the AbcEnv to which
|
|
// the method belongs, which will thus remain invariant. We can therefore only OSR
|
|
// non-builtin methods.
|
|
bool OSR::isSupported(const AbcEnv* abc_env, const MethodInfo* m, MethodSignaturep ms)
|
|
{
|
|
AvmAssert(abc_env->core() == m->pool()->core);
|
|
AvmAssert(abc_env->pool() == m->pool());
|
|
AvmAssert(abc_env->codeContext() != NULL);
|
|
AvmAssert(abc_env->codeContext()->bugCompatibility() != NULL);
|
|
|
|
return (m->osrEnabled() && // OSR allowed by policy (global or ExecPolicy attribute)
|
|
!m->hasExceptions() && // method does not have a try block
|
|
!m->hasFailedJit() && // no previous attempt to compile the method has failed
|
|
!CodegenLIR::jitWillFail(ms) && // fast-fail predictor says JIT success is possible
|
|
!m->pool()->isBuiltin && // the method is not a builtin (ABC baked into application)
|
|
abc_env->codeContext()->bugCompatibility()->bugzilla539094); // bug compatibility permits OSR
|
|
}
|
|
|
|
/**
|
|
* Compile and finish executing a function.
|
|
* We don't actually replace the interpreter frame. Instead, this
|
|
* routine JIT-compiles the current method and then calls the result
|
|
* of that. The JIT-ted version returns to here and the interpreter
|
|
* then immediately exits, returning the result of the JIT-ted version,
|
|
* which is returned here.
|
|
*/
|
|
bool OSR::execute(MethodEnv *env, Atom* interp_frame,
|
|
MethodSignaturep ms, const uint8_t* osr_pc, Atom* result)
|
|
{
|
|
BaseExecMgr* exec = BaseExecMgr::exec(env);
|
|
OSR osr(osr_pc, interp_frame);
|
|
|
|
#ifdef AVMPLUS_VERBOSE
|
|
if (env->method->pool()->isVerbose(VB_execpolicy)) {
|
|
env->core()->console << "execpolicy jit hot-loop " <<
|
|
env->method << " osr_pc=" <<
|
|
int(osr_pc - ms->abc_code_start()) <<
|
|
"\n";
|
|
}
|
|
#endif
|
|
// compile the method with an OSR entry point
|
|
AvmAssert(!env->method->hasFailedJit());
|
|
exec->verifyJit(env->method, ms, env->toplevel(), env->abcEnv(), &osr);
|
|
env->_implGPR = env->method->_implGPR;
|
|
|
|
if (env->method->hasFailedJit()) {
|
|
// Clean up OSR object explicitly, as there is no destructor.
|
|
mmfx_delete(osr.jit_frame_state);
|
|
return false;
|
|
}
|
|
|
|
// Save current_osr. It will be set to NULL in adjust_frame(),
|
|
// once we have initialized the JIT frame from it.
|
|
exec->current_osr = &osr;
|
|
int fakeArgc = ms->requiredParamCount();
|
|
*result = exec->endCoerce(env, fakeArgc, (uint32_t*)interp_frame, ms);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* This function is called by JIT code if OSR has been requested
|
|
* if (exec->current_osr != 0). This function fills the JIT frame locals
|
|
* to match the interpreter frame. The JIT code looks like this:
|
|
*
|
|
* if (*(&exec->current_osr)) {
|
|
* adjustFrame(...);
|
|
* goto loop_entry
|
|
* }
|
|
*/
|
|
void OSR::adjustFrame(MethodFrame* jitMethodFrame, CallStackNode *callStack,
|
|
FramePtr jitFramePointer, uint8_t *jitFrameTags)
|
|
{
|
|
MethodEnv* env = jitMethodFrame->env();
|
|
BaseExecMgr* exec = BaseExecMgr::exec(env);
|
|
OSR *osr = exec->current_osr;
|
|
AvmAssert(osr && "should not have gotten here");
|
|
Atom* interpFramePointer = osr->interp_frame;
|
|
|
|
MethodSignaturep ms = env->method->getMethodSignature();
|
|
int nLocals = ms->local_count();
|
|
int stackBase = nLocals + ms->max_scope();
|
|
FrameState* frameState = osr->jit_frame_state;
|
|
int scopeTop = nLocals + frameState->scopeDepth;
|
|
int stackTop = stackBase + frameState->stackDepth;
|
|
|
|
// OSR has been requested.
|
|
|
|
#ifdef AVMPLUS_VERBOSE
|
|
if (env->method->pool()->isVerbose(VB_interp)) {
|
|
env->core()->console <<
|
|
"osr-adjust_frame " << env->method->method_id() <<
|
|
" " << env->method <<
|
|
" scopeTop=" << scopeTop <<
|
|
" stackTop=" << stackTop <<
|
|
"\n";
|
|
}
|
|
#endif
|
|
|
|
// Patch the JIT frame local variable slots and the scope slots in use to match the interpreter state.
|
|
for (int i = 0; i < scopeTop; i++)
|
|
unboxSlot(frameState, env, interpFramePointer, jitFramePointer, jitFrameTags, i);
|
|
|
|
// zero out stack area for unused scopes:
|
|
if (scopeTop < stackBase) {
|
|
void* p = ((char*) jitFramePointer + (scopeTop << VARSHIFT(env->method)));
|
|
size_t nbytes = (stackBase - scopeTop) << VARSHIFT(env->method);
|
|
VMPI_memset(p, 0, nbytes);
|
|
}
|
|
|
|
// Patch operand stack slots:
|
|
for (int i = stackBase; i < stackTop; i++)
|
|
unboxSlot(frameState, env, interpFramePointer, jitFramePointer, jitFrameTags, i);
|
|
|
|
MethodFrame *interpreterMethodFrame = jitMethodFrame->next;
|
|
jitMethodFrame->dxns = interpreterMethodFrame->dxns;
|
|
jitMethodFrame->next = interpreterMethodFrame->next;
|
|
|
|
// Clean up non-local OSR parameterization data:
|
|
mmfx_delete(frameState);
|
|
exec->current_osr = NULL;
|
|
|
|
#ifdef DEBUGGER
|
|
// Call debugEnter if necessary, since the jit code won't.
|
|
// We can safely pass NULL for &eip here, because OSR is disabled
|
|
// for methods with catch blocks.
|
|
if (callStack)
|
|
env->debugEnter(jitFrameTags, callStack, jitFramePointer, NULL);
|
|
#else
|
|
(void) callStack;
|
|
#endif
|
|
}
|
|
|
|
#ifdef DEBUG
|
|
/**
|
|
* Make sure the atom we have meets all the invariants we derived
|
|
* at verify time.
|
|
*/
|
|
static bool isValidAtom(const FrameValue& value, Atom atom)
|
|
{
|
|
Traits* type = value.traits;
|
|
switch (Traits::getBuiltinType(type)) {
|
|
case BUILTIN_void:
|
|
return atom == undefinedAtom;
|
|
case BUILTIN_null:
|
|
return AvmCore::isNull(atom);
|
|
case BUILTIN_int:
|
|
case BUILTIN_uint:
|
|
case BUILTIN_number:
|
|
case BUILTIN_boolean:
|
|
return AvmCore::istype(atom, type);
|
|
case BUILTIN_any:
|
|
return !value.notNull || !AvmCore::isNullOrUndefined(atom);
|
|
default:
|
|
return (!value.notNull && AvmCore::isNull(atom)) ||
|
|
AvmCore::istype(atom, type);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
void OSR::unboxSlot(FrameState* frameState,
|
|
MethodEnv* env, Atom *interpFramePointer, FramePtr jitFramePointer,
|
|
uint8_t* jitFrameTags, int index)
|
|
{
|
|
const FrameValue& value = frameState->value(index);
|
|
Atom atom = interpFramePointer[index];
|
|
AvmAssert(isValidAtom(value, atom));
|
|
|
|
// Unbox the value and store in jit vars[].
|
|
AvmAssert(isValidAtom(value, atom));
|
|
Atom* addr = (Atom *) ((char *) jitFramePointer + (index << VARSHIFT(env->method)));
|
|
Traits* type = value.traits;
|
|
BaseExecMgr::unbox1(atom, type, addr);
|
|
|
|
// Also update the jit's SlotStorageType byte in tags[].
|
|
BuiltinType tag = Traits::getBuiltinType(type);
|
|
SlotStorageType sst = valueStorageType(tag);
|
|
jitFrameTags[index] = (uint8_t) sst;
|
|
|
|
#ifdef AVMPLUS_VERBOSE
|
|
if (env->method->pool()->isVerbose(VB_interp)) {
|
|
env->core()->console << "osr-unbox " << index << " type=" << type <<
|
|
" notnull=" << frameState->value(index).notNull <<
|
|
" value=" << asAtom(atom) <<
|
|
"\n";
|
|
}
|
|
#endif
|
|
(void)env;
|
|
}
|
|
|
|
// increment the invocation counter, return true once method is compiled
|
|
REALLY_INLINE bool OSR::countInvoke(MethodEnv* env)
|
|
{
|
|
MethodInfo* m = env->method;
|
|
if (--m->_abc.countdown)
|
|
return false;
|
|
if (m->isInterpreted()) {
|
|
#ifdef AVMPLUS_VERBOSE
|
|
if (m->pool()->isVerbose(VB_execpolicy))
|
|
env->core()->console <<
|
|
"execpolicy jit hot-call " << env->method << "\n";
|
|
#endif
|
|
AvmAssert(!m->hasFailedJit());
|
|
BaseExecMgr* exec = BaseExecMgr::exec(env);
|
|
exec->verifyJit(m, m->getMethodSignature(),
|
|
env->toplevel(), env->abcEnv(), NULL);
|
|
if (m->hasFailedJit())
|
|
return false;
|
|
}
|
|
// Method was already compiled; we got here because env->_implGPR
|
|
// was not updated. Update it now.
|
|
env->_implGPR = env->method->_implGPR;
|
|
return true;
|
|
}
|
|
|
|
// intercept call to interpreter increment invocation counter
|
|
uintptr_t OSR::osrInterpGPR(MethodEnv* env, int argc, uint32_t *ap)
|
|
{
|
|
if (countInvoke(env))
|
|
return (*env->_implGPR)(env, argc, ap);
|
|
return BaseExecMgr::interpGPR(env, argc, ap);
|
|
}
|
|
|
|
// intercept call to interpreter increment invocation counter
|
|
double OSR::osrInterpFPR(MethodEnv* env, int argc, uint32_t *ap)
|
|
{
|
|
if (countInvoke(env))
|
|
return (*env->_implFPR)(env, argc, ap);
|
|
return BaseExecMgr::interpFPR(env, argc, ap);
|
|
}
|
|
|
|
#ifdef VMCFG_FLOAT
|
|
// intercept call to interpreter increment invocation counter
|
|
float4_t OSR::osrInterpVECR(MethodEnv* env, int argc, uint32_t *ap)
|
|
{
|
|
if (countInvoke(env))
|
|
return (*env->_implVECR)(env, argc, ap);
|
|
return BaseExecMgr::interpVECR(env, argc, ap);
|
|
}
|
|
#endif
|
|
|
|
// intercept call to interpreter increment invocation counter
|
|
Atom OSR::osrInvokeInterp(MethodEnv* env, int argc, Atom* argv)
|
|
{
|
|
if (countInvoke(env))
|
|
return env->coerceEnter(argc, argv);
|
|
return BaseExecMgr::invokeInterp(env, argc, argv);
|
|
}
|
|
|
|
// OSR tramp for invoking a constructor that returns a GPR
|
|
uintptr_t OSR::osrInitInterpGPR(MethodEnv* env, int argc, uint32_t *ap)
|
|
{
|
|
if (countInvoke(env))
|
|
return (*env->_implGPR)(env, argc, ap);
|
|
return BaseExecMgr::initInterpGPR(env, argc, ap);
|
|
}
|
|
|
|
// OSR tramp for invoking a constructor that returns a FPR
|
|
double OSR::osrInitInterpFPR(MethodEnv* env, int argc, uint32_t *ap)
|
|
{
|
|
if (countInvoke(env))
|
|
return (*env->_implFPR)(env, argc, ap);
|
|
return BaseExecMgr::initInterpFPR(env, argc, ap);
|
|
}
|
|
|
|
#ifdef VMCFG_FLOAT
|
|
// OSR tramp for invoking a constructor that returns a FPR
|
|
float4_t OSR::osrInitInterpVECR(MethodEnv* env, int argc, uint32_t *ap)
|
|
{
|
|
if (countInvoke(env))
|
|
return (*env->_implVECR)(env, argc, ap);
|
|
return BaseExecMgr::initInterpVECR(env, argc, ap);
|
|
}
|
|
#endif
|
|
|
|
// intercept call to interpreter increment invocation counter
|
|
Atom OSR::osrInitInvokeInterp(MethodEnv* env, int argc, Atom* argv)
|
|
{
|
|
if (countInvoke(env))
|
|
return env->coerceEnter(argc, argv);
|
|
return BaseExecMgr::initInvokeInterp(env, argc, argv);
|
|
}
|
|
|
|
#ifdef DEBUG
|
|
// Verify that currentBugCompatibility() is as OSR expected when method is called.
|
|
void OSR::checkBugCompatibility(MethodEnv* env)
|
|
{
|
|
// A method can be OSR'd if it is not a builtin. We ignore builtins here.
|
|
if (!env->method->pool()->isBuiltin) {
|
|
const BugCompatibility* abcBugCompatibility = env->abcEnv()->codeContext()->bugCompatibility();
|
|
const BugCompatibility* dynamicBugCompatibility = env->core()->currentBugCompatibility();
|
|
// Verify that currentBugCompatibility() agrees with what the JIT would have assumed.
|
|
AvmAssert(dynamicBugCompatibility == abcBugCompatibility);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
#endif // VMCFG_NANOJIT
|