Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
First pass attempting to optimize startup time
Browse files Browse the repository at this point in the history
This patch is a bunch of hacky fixes as a proof of
concept attempt to improve startup time.
* This fixes the interpreter to interpret IR instructions
  that the IR builder generates without having to build
  a CFG or run any passes on them. There is no cloning
  of in instructions either.
* If the scope runs at least N times (N = 20 right now),
  the CFG is then built, some basic passes are run and
  future runs will use the new optimized scope.

In interpret mode (-X-C), this patch:
* reduces startup time for gem list by 20+%.
* reduces total user time by ~13% across a bunch of benchmarks.
* reduces wall clock time by ~5% across the same benchmarks.
* improves peak perf on some of those benchmarks and
  degrades peak perf on some.

In default mode (-X-C), this patch:
* reduces total user time by ~20% across a bunch of benchmarks.
* reduces wall clock time by ~11% across the same benchmarks.

This was passing all tests in an earlier version of this
patch before I broke something messing and tweaking
something. It is late at night, so pushing this as is.

This branch is not ready for merge. It requires:
* code cleanup
* get rid of hacks
* more testing
* fixing broken tests
subbuss committed Feb 18, 2015
1 parent f3c4b71 commit df56604
Showing 12 changed files with 244 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -36,7 +36,16 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz
if (IRRuntimeHelpers.isDebug()) doDebug();

InterpreterContext ic = ensureInstrsReady();
return ic.engine.interpret(context, self, ic, getImplementationClass().getMethodLocation(), name, block, null);
if (ic.hasExplicitCallProtocol()) {
return ic.engine.interpret(context, self, ic, getImplementationClass().getMethodLocation(), name, block, null);
} else {
try {
this.pre(ic, context, self, name, block, getImplementationClass());
return ic.engine.interpret(context, self, ic, getImplementationClass().getMethodLocation(), name, block, null);
} finally {
this.post(ic, context);
}
}
}

@Override
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@
import org.jruby.parser.StaticScope;
import org.jruby.runtime.Arity;
import org.jruby.runtime.Block;
import org.jruby.runtime.DynamicScope;
import org.jruby.runtime.PositionAware;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.Visibility;
@@ -83,11 +84,35 @@ public Arity getArity() {
return this.arity;
}

protected void post(InterpreterContext ic, ThreadContext context) {
// update call stacks (pop: ..)
context.popFrame();
if (ic.popDynScope()) {
context.popScope();
}
}

protected void pre(InterpreterContext ic, ThreadContext context, IRubyObject self, String name, Block block, RubyModule implClass) {
// update call stacks (push: frame, class, scope, etc.)
context.preMethodFrameOnly(implClass, name, self, block);
if (ic.pushNewDynScope()) {
context.pushScope(DynamicScope.newDynamicScope(ic.getStaticScope()));
}
context.setCurrentVisibility(getVisibility());
}

public InterpreterContext ensureInstrsReady() {
// Try unsync access first before calling more expensive method for getting IC
InterpreterContext ic = method.getInterpreterContext();

return ic == null ? method.prepareForInterpretation() : ic;
// Build/rebuild if necessary
if (ic == null) {
ic = method.prepareForInterpretation();
} else if (ic.needsRebuilding()) {
ic = method.prepareForInterpretation(true);
}

return ic;
}

@Override
@@ -105,12 +130,21 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz
}
}

private static IRubyObject INTERPRET_METHOD(ThreadContext context, InterpreterContext ic, RubyModule implClass,
private IRubyObject INTERPRET_METHOD(ThreadContext context, InterpreterContext ic, RubyModule implClass,
IRubyObject self, String name, IRubyObject[] args, Block block) {
try {
ThreadContext.pushBacktrace(context, name, ic.getFileName(), context.getLine());

return ic.engine.interpret(context, self, ic, implClass, name, args, block, null);
if (ic.hasExplicitCallProtocol()) {
return ic.engine.interpret(context, self, ic, implClass, name, args, block, null);
} else {
try {
this.pre(ic, context, self, name, block, implClass);
return ic.engine.interpret(context, self, ic, implClass, name, args, block, null);
} finally {
this.post(ic, context);
}
}
} finally {
ThreadContext.popBacktrace(context);
}
@@ -131,12 +165,21 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz
}
}

private static IRubyObject INTERPRET_METHOD(ThreadContext context, InterpreterContext ic, RubyModule implClass,
private IRubyObject INTERPRET_METHOD(ThreadContext context, InterpreterContext ic, RubyModule implClass,
IRubyObject self, String name, Block block) {
try {
ThreadContext.pushBacktrace(context, name, ic.getFileName(), context.getLine());

return ic.engine.interpret(context, self, ic, implClass, name, block, null);
if (ic.hasExplicitCallProtocol()) {
return ic.engine.interpret(context, self, ic, implClass, name, block, null);
} else {
try {
this.pre(ic, context, self, name, block, implClass);
return ic.engine.interpret(context, self, ic, implClass, name, block, null);
} finally {
this.post(ic, context);
}
}
} finally {
ThreadContext.popBacktrace(context);
}
@@ -157,12 +200,21 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz
}
}

private static IRubyObject INTERPRET_METHOD(ThreadContext context, InterpreterContext ic, RubyModule implClass,
private IRubyObject INTERPRET_METHOD(ThreadContext context, InterpreterContext ic, RubyModule implClass,
IRubyObject self, String name, IRubyObject arg1, Block block) {
try {
ThreadContext.pushBacktrace(context, name, ic.getFileName(), context.getLine());

return ic.engine.interpret(context, self, ic, implClass, name, arg1, block, null);
if (ic.hasExplicitCallProtocol()) {
return ic.engine.interpret(context, self, ic, implClass, name, arg1, block, null);
} else {
try {
this.pre(ic, context, self, name, block, implClass);
return ic.engine.interpret(context, self, ic, implClass, name, arg1, block, null);
} finally {
this.post(ic, context);
}
}
} finally {
ThreadContext.popBacktrace(context);
}
@@ -183,12 +235,21 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz
}
}

private static IRubyObject INTERPRET_METHOD(ThreadContext context, InterpreterContext ic, RubyModule implClass,
private IRubyObject INTERPRET_METHOD(ThreadContext context, InterpreterContext ic, RubyModule implClass,
IRubyObject self, String name, IRubyObject arg1, IRubyObject arg2, Block block) {
try {
ThreadContext.pushBacktrace(context, name, ic.getFileName(), context.getLine());

return ic.engine.interpret(context, self, ic, implClass, name, arg1, arg2, block, null);
if (ic.hasExplicitCallProtocol()) {
return ic.engine.interpret(context, self, ic, implClass, name, arg1, arg2, block, null);
} else {
try {
this.pre(ic, context, self, name, block, implClass);
return ic.engine.interpret(context, self, ic, implClass, name, arg1, arg2, block, null);
} finally {
this.post(ic, context);
}
}
} finally {
ThreadContext.popBacktrace(context);
}
@@ -209,12 +270,21 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz
}
}

private static IRubyObject INTERPRET_METHOD(ThreadContext context, InterpreterContext ic, RubyModule implClass,
private IRubyObject INTERPRET_METHOD(ThreadContext context, InterpreterContext ic, RubyModule implClass,
IRubyObject self, String name, IRubyObject arg1, IRubyObject arg2, IRubyObject arg3, Block block) {
try {
ThreadContext.pushBacktrace(context, name, ic.getFileName(), context.getLine());

return ic.engine.interpret(context, self, ic, implClass, name, arg1, arg2, arg3, block, null);
if (ic.hasExplicitCallProtocol()) {
return ic.engine.interpret(context, self, ic, implClass, name, arg1, arg2, arg3, block, null);
} else {
try {
this.pre(ic, context, self, name, block, implClass);
return ic.engine.interpret(context, self, ic, implClass, name, arg1, arg2, arg3, block, null);
} finally {
this.post(ic, context);
}
}
} finally {
ThreadContext.popBacktrace(context);
}
4 changes: 2 additions & 2 deletions core/src/main/java/org/jruby/ir/IRClosure.java
Original file line number Diff line number Diff line change
@@ -106,8 +106,8 @@ public IRClosure(IRManager manager, IRScope lexicalParent, int lineNumber, Stati
}

@Override
public InterpreterContext allocateInterpreterContext(Instr[] instructionList) {
return new ClosureInterpreterContext(this, instructionList);
public InterpreterContext allocateInterpreterContext(Instr[] instructionList, boolean rebuild) {
return new ClosureInterpreterContext(this, instructionList, rebuild);
}

public void setBeginEndBlock() {
4 changes: 2 additions & 2 deletions core/src/main/java/org/jruby/ir/IREvalScript.java
Original file line number Diff line number Diff line change
@@ -35,8 +35,8 @@ public IREvalScript(IRManager manager, IRScope lexicalParent, String fileName,
}

@Override
public InterpreterContext allocateInterpreterContext(Instr[] instructionList) {
return new BeginEndInterpreterContext(this, instructionList);
public InterpreterContext allocateInterpreterContext(Instr[] instructionList, boolean rebuild) {
return new BeginEndInterpreterContext(this, instructionList, rebuild);
}

@Override
75 changes: 63 additions & 12 deletions core/src/main/java/org/jruby/ir/IRScope.java
Original file line number Diff line number Diff line change
@@ -58,6 +58,7 @@
* and so on ...
*/
public abstract class IRScope implements ParseResult {

private static final Logger LOG = LoggerFactory.getLogger("IRScope");

private static AtomicInteger globalScopeCount = new AtomicInteger();
@@ -139,6 +140,13 @@ public abstract class IRScope implements ParseResult {

private TemporaryVariable yieldClosureVariable;

// What state is this scope in?
enum ScopeState {
INIT, INSTRS_CLONED, CFG_BUILT
};

private ScopeState state = ScopeState.INIT;

// Used by cloning code
protected IRScope(IRScope s, IRScope lexicalParent) {
this.lexicalParent = lexicalParent;
@@ -163,6 +171,7 @@ protected IRScope(IRScope s, IRScope lexicalParent) {

this.localVars = new HashMap<String, LocalVariable>(s.localVars);
this.scopeId = globalScopeCount.getAndIncrement();
this.state = ScopeState.INIT; // SSS FIXME: Is this correct?

this.executedPasses = new ArrayList<CompilerPass>();

@@ -208,6 +217,7 @@ public IRScope(IRManager manager, IRScope lexicalParent, String name,

this.localVars = new HashMap<String, LocalVariable>();
this.scopeId = globalScopeCount.getAndIncrement();
this.state = ScopeState.INIT;

this.executedPasses = new ArrayList<CompilerPass>();

@@ -451,6 +461,7 @@ public CFG buildCFG() {
this.instrList = null;

setCFG(newCFG);
this.state = ScopeState.CFG_BUILT;

return newCFG;
}
@@ -465,6 +476,20 @@ public CFG getCFG() {

@Interp
protected Instr[] prepareInstructions() {
if (getCFG() == null) {
int n = instrList.size();
Instr[] linearizedInstrArray = instrList.toArray(new Instr[n]);
for (int ipc = 0; ipc < n; ipc++) {
Instr i = linearizedInstrArray[ipc];
i.setIPC(ipc);
if (i instanceof LabelInstr) {
((LabelInstr)i).getLabel().setTargetPC(ipc+1);
}
}

return linearizedInstrArray;
}

setupLinearization();

boolean simple_method = this instanceof IRMethod;
@@ -603,26 +628,52 @@ protected void initScope(boolean jitMode) {
}

/** Make version specific to scope which needs it (e.g. Closure vs non-closure). */
public InterpreterContext allocateInterpreterContext(Instr[] instructionList) {
return new InterpreterContext(this, instructionList);
public InterpreterContext allocateInterpreterContext(Instr[] instructionList, boolean rebuild) {
return new InterpreterContext(this, instructionList, rebuild);
}

public InterpreterContext prepareForInterpretation() {
return prepareForInterpretation(false);
}

protected void cloneInstrs() {
if (this.state == ScopeState.INIT) {
// Clone instrs before modifying them
SimpleCloneInfo cloneInfo = new SimpleCloneInfo(this, false);
List<Instr> newInstrList = new ArrayList<Instr>(this.instrList.size());
for (Instr instr: this.instrList) {
newInstrList.add(instr.clone(cloneInfo));
}
this.instrList = newInstrList;
this.state = ScopeState.INSTRS_CLONED;
}
}

/** Run any necessary passes to get the IR ready for interpretation */
public synchronized InterpreterContext prepareForInterpretation() {
if (interpreterContext != null) return interpreterContext; // Already prepared
public synchronized InterpreterContext prepareForInterpretation(boolean rebuild) {
if (interpreterContext != null) {
if (!rebuild || getCFG() != null) {
return interpreterContext; // Already prepared/rebuilt
}

initScope(false);
// If rebuilding, clone instrs before building cfg, running passes, etc.
this.cloneInstrs();
for (IRClosure cl: getClosures()) {
cl.cloneInstrs();
}

// System.out.println("-- passes run for: " + this + " = " + java.util.Arrays.toString(executedPasses.toArray()));
// Build CFG, run passes, etc.
initScope(false);

// Always add call protocol instructions now for both interpreter and JIT
// since we are removing support for implicit stuff in the interpreter.
// When JIT later runs this same pass, it will be a NOP there.
if (!isUnsafeScope()) {
(new AddCallProtocolInstructions()).run(this);
// Always add call protocol instructions now for both interpreter and JIT
// since we are removing support for implicit stuff in the interpreter.
// When JIT later runs this same pass, it will be a NOP there.
if (!isUnsafeScope()) {
(new AddCallProtocolInstructions()).run(this);
}
}

interpreterContext = allocateInterpreterContext(prepareInstructions());
interpreterContext = allocateInterpreterContext(prepareInstructions(), rebuild);

return interpreterContext;
}
4 changes: 2 additions & 2 deletions core/src/main/java/org/jruby/ir/IRScriptBody.java
Original file line number Diff line number Diff line change
@@ -31,8 +31,8 @@ public void setTopLevelBindingScope(DynamicScope tlbScope) {
}

@Override
public InterpreterContext allocateInterpreterContext(Instr[] instructionList) {
return new BeginEndInterpreterContext(this, instructionList);
public InterpreterContext allocateInterpreterContext(Instr[] instructionList, boolean rebuild) {
return new BeginEndInterpreterContext(this, instructionList, rebuild);
}

@Override
Original file line number Diff line number Diff line change
@@ -14,8 +14,8 @@
public class BeginEndInterpreterContext extends InterpreterContext {
private List<IRClosure> beginBlocks;

public BeginEndInterpreterContext(IRScope scope, Instr[] instructions) {
super(scope, instructions);
public BeginEndInterpreterContext(IRScope scope, Instr[] instructions, boolean rebuild) {
super(scope, instructions, rebuild);

beginBlocks = scope.getBeginBlocks();
}
Original file line number Diff line number Diff line change
@@ -9,8 +9,8 @@
* Interpreter knowledge needed to interpret a closure.
*/
public class ClosureInterpreterContext extends InterpreterContext {
public ClosureInterpreterContext(IRClosure scope, Instr[] instructions) {
super(scope, instructions);
public ClosureInterpreterContext(IRClosure scope, Instr[] instructions, boolean rebuild) {
super(scope, instructions, rebuild);
}

/**
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@
import org.jruby.ir.IRTranslator;
import org.jruby.ir.operands.IRException;
import org.jruby.ir.operands.WrappedIRClosure;
import org.jruby.ir.representations.CFG;
import org.jruby.ir.runtime.IRBreakJump;
import org.jruby.ir.runtime.IRRuntimeHelpers;
import org.jruby.parser.StaticScope;
Original file line number Diff line number Diff line change
@@ -19,7 +19,6 @@ public class InterpreterContext {
private final int temporaryFixnumVariablecount;
private final int temporaryFloatVariablecount;


private final String name;
private final String fileName;
private final int lineNumber;
@@ -44,10 +43,18 @@ public class InterpreterContext {
// View of CFG at time of creating this context.
private CFG cfg = null;

public InterpreterContext(IRScope scope, Instr[] instructions) {
private int runCount = 0;
private boolean rebuilt = false;

public InterpreterContext(IRScope scope, Instr[] instructions, boolean rebuild) {
//FIXME: Remove once we conditionally plug in CFG on debug-only
this.cfg = scope.getCFG();
this.rebuilt = rebuild;
if (this.rebuilt) {
this.runCount = 5;
}

/*
if (scope instanceof IRModuleBody || scope instanceof IRClassBody) {
engine = BODY_INTERPRETER;
// ENEBO: Playing with unboxable and subset instruction sets
@@ -56,6 +63,8 @@ public InterpreterContext(IRScope scope, Instr[] instructions) {
} else {
engine = DEFAULT_INTERPRETER;
}
*/
engine = DEFAULT_INTERPRETER;

this.name = scope.getName();
this.fileName = scope.getFileName();
@@ -75,6 +84,22 @@ public InterpreterContext(IRScope scope, Instr[] instructions) {
this.receivesKeywordArguments = scope.getFlags().contains(IRFlags.RECEIVES_KEYWORD_ARGS);
}

public CFG getCFG() {
return this.cfg;
}

public boolean isRebuilt() {
return this.rebuilt;
}

public void incrementRunCount() {
this.runCount++;
}

public boolean needsRebuilding() {
return this.runCount == 20;
}

public Object[] allocateTemporaryVariables() {
return temporaryVariablecount > 0 ? new Object[temporaryVariablecount] : null;
}
43 changes: 40 additions & 3 deletions core/src/main/java/org/jruby/ir/interpreter/InterpreterEngine.java
Original file line number Diff line number Diff line change
@@ -41,6 +41,7 @@
import org.jruby.ir.operands.Bignum;
import org.jruby.ir.operands.Fixnum;
import org.jruby.ir.operands.Float;
import org.jruby.ir.operands.Label;
import org.jruby.ir.operands.LocalVariable;
import org.jruby.ir.operands.Operand;
import org.jruby.ir.operands.Self;
@@ -52,6 +53,8 @@
import org.jruby.ir.operands.UnboxedFixnum;
import org.jruby.ir.operands.UnboxedFloat;
import org.jruby.ir.operands.Variable;
import org.jruby.ir.runtime.IRBreakJump;
import org.jruby.ir.runtime.IRReturnJump;
import org.jruby.ir.runtime.IRRuntimeHelpers;
import org.jruby.parser.StaticScope;
import org.jruby.runtime.Block;
@@ -63,6 +66,8 @@
import org.jruby.runtime.ivars.VariableAccessor;
import org.jruby.runtime.opto.ConstantCache;

import java.util.Stack;

/**
* Base full interpreter. Subclasses can use utility methods here and override what they want.
*/
@@ -115,11 +120,19 @@ public IRubyObject interpret(ThreadContext context, IRubyObject self,
IRScope scope = currScope.getIRScope();
boolean acceptsKeywordArgument = interpreterContext.receivesKeywordArguments();

Stack<Integer> rescuePCs = null;
if (interpreterContext.getCFG() == null) {
rescuePCs = new Stack<Integer>();
}

// Init profiling this scope
boolean debug = IRRuntimeHelpers.isDebug();
boolean profile = IRRuntimeHelpers.inProfileMode();
Integer scopeVersion = profile ? Profiler.initProfiling(scope) : 0;

// Update profile
interpreterContext.incrementRunCount();

// Enter the looooop!
while (ipc < n) {
Instr instr = instrs[ipc];
@@ -164,7 +177,7 @@ public IRubyObject interpret(ThreadContext context, IRubyObject self,
currDynScope = interpreterContext.newDynamicScope(context);
context.pushScope(currDynScope);
} else {
processBookKeepingOp(context, instr, operation, name, args, self, block, implClass);
processBookKeepingOp(context, instr, operation, name, args, self, block, implClass, rescuePCs);
}
break;
case OTHER_OP:
@@ -176,7 +189,21 @@ public IRubyObject interpret(ThreadContext context, IRubyObject self,
extractToMethodToAvoidC2Crash(context, instr, t);
}

ipc = instr.getRPC();
if (rescuePCs == null) {
// When CFG is present
ipc = instr.getRPC();
} else {
// When CFG is absent
if (rescuePCs.empty()
|| (t instanceof IRBreakJump && (instr instanceof BreakInstr))
|| (t instanceof IRReturnJump && (instr instanceof NonlocalReturnInstr)))
{
ipc = -1;
} else {
ipc = rescuePCs.pop();
}
}

if (debug) {
Interpreter.LOG.info("in : " + interpreterContext.getStaticScope().getIRScope() + ", caught Java throwable: " + t + "; excepting instr: " + instr);
Interpreter.LOG.info("ipc for rescuer: " + ipc);
@@ -323,8 +350,16 @@ private static void processCall(ThreadContext context, Instr instr, Operation op

private static void processBookKeepingOp(ThreadContext context, Instr instr, Operation operation,
String name, IRubyObject[] args, IRubyObject self, Block block,
RubyModule implClass) {
RubyModule implClass, Stack<Integer> rescuePCs) {
switch(operation) {
case LABEL:
break;
case EXC_REGION_START:
rescuePCs.push(((Label)instr.getOperands()[0]).getTargetPC());
break;
case EXC_REGION_END:
rescuePCs.pop();
break;
case PUSH_FRAME:
context.preMethodFrameOnly(implClass, name, self, block);
// Only the top-level script scope has PRIVATE visibility.
@@ -392,6 +427,8 @@ private static void processOtherOp(ThreadContext context, Instr instr, Operation
{
Object result;
switch(operation) {
case RECV_SELF:
break;
case COPY: {
CopyInstr c = (CopyInstr)instr;
Operand src = c.getSource();
14 changes: 12 additions & 2 deletions core/src/main/java/org/jruby/runtime/InterpretedIRBlockBody.java
Original file line number Diff line number Diff line change
@@ -32,8 +32,18 @@ public InterpreterContext ensureInstrsReady() {
LOG.info(closure.debugOutput());
displayedCFG = true;
}
// Always prepared in the context of parent scope -- so a null value here is a bug.
return closure.getInterpreterContext();

// Try unsync access first before calling more expensive method for getting IC
InterpreterContext ic = closure.getInterpreterContext();

// Build/rebuild if necessary
if (ic == null) {
ic = closure.prepareForInterpretation();
} else if (ic.needsRebuilding()) {
ic = closure.prepareForInterpretation(true);
}

return ic;
}

protected IRubyObject commonYieldPath(ThreadContext context, IRubyObject[] args, IRubyObject self, Binding binding, Type type, Block block) {

0 comments on commit df56604

Please sign in to comment.