Skip to content

Commit

Permalink
Various literal to_proc'ed Symbol optimizations.
Browse files Browse the repository at this point in the history
* Use a dummy binding rather than creating new every time.
* Cache the resulting proc at create site.

The second optimization results in a given &:foo in code only
creating a single Proc, ever, and caching it at that point in the
code. This is based on the observation that symbol procs typically
are used to iterate over homogeneous collections of objects, so
caching the proc allows its cache to stay populated and local to
the related code. This also eliminates the allocation of a Block,
BlockBody, and RubyProc for each encounter, which improves perf
also for heterogeneous collections with poor cacheability.

Benchmark:

```ruby
loop {
  puts Benchmark.measure {
    ary = [1,2,3,4]
    1_000_000.times {
      ary.each(&:object_id)
    }
  }
}
```

Before:

```
  1.270000   0.070000   1.340000 (  0.710043)
  0.640000   0.020000   0.660000 (  0.511692)
  0.470000   0.000000   0.470000 (  0.460667)
  0.490000   0.010000   0.500000 (  0.480732)
  0.470000   0.000000   0.470000 (  0.462888)
```

Just the dummy binding optimization:

```
  1.210000   0.070000   1.280000 (  0.660924)
  0.540000   0.020000   0.560000 (  0.432614)
  0.430000   0.000000   0.430000 (  0.422502)
  0.430000   0.000000   0.430000 (  0.416549)
  0.410000   0.010000   0.420000 (  0.412461)
```

And with proc caching:

```
  0.890000   0.060000   0.950000 (  0.456065)
  0.410000   0.020000   0.430000 (  0.279023)
  0.290000   0.000000   0.290000 (  0.282117)
  0.300000   0.010000   0.310000 (  0.288516)
  0.270000   0.000000   0.270000 (  0.270100)
```
headius committed Dec 28, 2015
1 parent d26c846 commit 69662ab
Showing 10 changed files with 221 additions and 68 deletions.
138 changes: 73 additions & 65 deletions core/src/main/java/org/jruby/RubySymbol.java
Original file line number Diff line number Diff line change
@@ -44,6 +44,7 @@
import org.jruby.compiler.Constantizable;
import org.jruby.parser.StaticScope;
import org.jruby.runtime.ArgumentDescriptor;
import org.jruby.runtime.Binding;
import org.jruby.runtime.Block;
import org.jruby.runtime.BlockBody;
import org.jruby.runtime.CallSite;
@@ -462,73 +463,10 @@ public IRubyObject encoding(ThreadContext context) {

@JRubyMethod
public IRubyObject to_proc(ThreadContext context) {
StaticScope scope = context.runtime.getStaticScopeFactory().getDummyScope();
final CallSite site = new FunctionalCachingCallSite(symbol);
BlockBody body = new ContextAwareBlockBody(scope, Signature.OPTIONAL) {
private IRubyObject yieldInner(ThreadContext context, RubyArray array, Block blockArg) {
if (array.isEmpty()) {
throw context.runtime.newArgumentError("no receiver given");
}

IRubyObject self = array.shift(context);

return site.call(context, self, self, array.toJavaArray(), blockArg);
}

@Override
public IRubyObject yield(ThreadContext context, Block block, IRubyObject[] args, IRubyObject self, Block blockArg) {
RubyProc.prepareArgs(context, block.type, blockArg.getBody(), args);
return yieldInner(context, context.runtime.newArrayNoCopyLight(args), blockArg);
}

@Override
public IRubyObject yield(ThreadContext context, Block block, IRubyObject value, Block blockArg) {
return yieldInner(context, ArgsUtil.convertToRubyArray(context.runtime, value, false), blockArg);
}

@Override
protected IRubyObject doYield(ThreadContext context, Block block, IRubyObject value) {
return yieldInner(context, ArgsUtil.convertToRubyArray(context.runtime, value, false), Block.NULL_BLOCK);
}

@Override
protected IRubyObject doYield(ThreadContext context, Block block, IRubyObject[] args, IRubyObject self) {
return yieldInner(context, context.runtime.newArrayNoCopyLight(args), Block.NULL_BLOCK);
}

@Override
public IRubyObject yieldSpecific(ThreadContext context, Block block, IRubyObject arg0) {
return site.call(context, arg0, arg0);
}

@Override
public IRubyObject yieldSpecific(ThreadContext context, Block block, IRubyObject arg0, IRubyObject arg1) {
return site.call(context, arg0, arg0, arg1);
}

@Override
public IRubyObject yieldSpecific(ThreadContext context, Block block, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2) {
return site.call(context, arg0, arg0, arg1, arg2);
}

@Override
public String getFile() {
return symbol;
}

@Override
public int getLine() {
return -1;
}

@Override
public ArgumentDescriptor[] getArgumentDescriptors() {
return ArgumentDescriptor.ANON_REST;
}
};
BlockBody body = new SymbolProcBody(context.runtime, symbol);

return RubyProc.newProc(context.runtime,
new Block(body, context.currentBinding()),
new Block(body, Binding.DUMMY),
Block.Type.PROC);
}

@@ -1071,4 +1009,74 @@ public static String objectToSymbolString(IRubyObject object) {
return object.convertToString().getByteList().toString();
}
}

private static class SymbolProcBody extends ContextAwareBlockBody {
private final CallSite site;

public SymbolProcBody(Ruby runtime, String symbol) {
super(runtime.getStaticScopeFactory().getDummyScope(), Signature.OPTIONAL);
this.site = new FunctionalCachingCallSite(symbol);
}

private IRubyObject yieldInner(ThreadContext context, RubyArray array, Block blockArg) {
if (array.isEmpty()) {
throw context.runtime.newArgumentError("no receiver given");
}

IRubyObject self = array.shift(context);

return site.call(context, self, self, array.toJavaArray(), blockArg);
}

@Override
public IRubyObject yield(ThreadContext context, Block block, IRubyObject[] args, IRubyObject self, Block blockArg) {
RubyProc.prepareArgs(context, block.type, blockArg.getBody(), args);
return yieldInner(context, context.runtime.newArrayNoCopyLight(args), blockArg);
}

@Override
public IRubyObject yield(ThreadContext context, Block block, IRubyObject value, Block blockArg) {
return yieldInner(context, ArgsUtil.convertToRubyArray(context.runtime, value, false), blockArg);
}

@Override
protected IRubyObject doYield(ThreadContext context, Block block, IRubyObject value) {
return yieldInner(context, ArgsUtil.convertToRubyArray(context.runtime, value, false), Block.NULL_BLOCK);
}

@Override
protected IRubyObject doYield(ThreadContext context, Block block, IRubyObject[] args, IRubyObject self) {
return yieldInner(context, context.runtime.newArrayNoCopyLight(args), Block.NULL_BLOCK);
}

@Override
public IRubyObject yieldSpecific(ThreadContext context, Block block, IRubyObject arg0) {
return site.call(context, arg0, arg0);
}

@Override
public IRubyObject yieldSpecific(ThreadContext context, Block block, IRubyObject arg0, IRubyObject arg1) {
return site.call(context, arg0, arg0, arg1);
}

@Override
public IRubyObject yieldSpecific(ThreadContext context, Block block, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2) {
return site.call(context, arg0, arg0, arg1, arg2);
}

@Override
public String getFile() {
return site.methodName;
}

@Override
public int getLine() {
return -1;
}

@Override
public ArgumentDescriptor[] getArgumentDescriptors() {
return ArgumentDescriptor.ANON_REST;
}
}
}
6 changes: 5 additions & 1 deletion core/src/main/java/org/jruby/ir/IRBuilder.java
Original file line number Diff line number Diff line change
@@ -2338,7 +2338,11 @@ private Operand setupCallClosure(Node node) {
case ITERNODE:
return build(node);
case BLOCKPASSNODE:
return build(((BlockPassNode)node).getBodyNode());
Node bodyNode = ((BlockPassNode)node).getBodyNode();
if (bodyNode instanceof SymbolNode) {
return new SymbolProc(((SymbolNode)bodyNode).getName(), ((SymbolNode)bodyNode).getEncoding());
}
return build(bodyNode);
default:
throw new NotCompilableException("ERROR: Encountered a method with a non-block, non-blockpass iter node at: " + node);
}
1 change: 1 addition & 0 deletions core/src/main/java/org/jruby/ir/IRVisitor.java
Original file line number Diff line number Diff line change
@@ -188,6 +188,7 @@ private void error(Object object) {
public void StringLiteral(StringLiteral stringliteral) { error(stringliteral); }
public void SValue(SValue svalue) { error(svalue); }
public void Symbol(Symbol symbol) { error(symbol); }
public void SymbolProc(SymbolProc symbolproc) { error(symbolproc); }
public void TemporaryVariable(TemporaryVariable temporaryvariable) { error(temporaryvariable); }
public void TemporaryLocalVariable(TemporaryLocalVariable temporarylocalvariable) { error(temporarylocalvariable); }
public void TemporaryFloatVariable(TemporaryFloatVariable temporaryfloatvariable) { error(temporaryfloatvariable); }
3 changes: 2 additions & 1 deletion core/src/main/java/org/jruby/ir/operands/OperandType.java
Original file line number Diff line number Diff line change
@@ -43,7 +43,8 @@ public enum OperandType {
WRAPPED_IR_CLOSURE((byte) 'w'),
FROZEN_STRING((byte) 'z'),
NULL_BLOCK((byte) 'o'),
FILENAME((byte) 'm')
FILENAME((byte) 'm'),
SYMBOL_PROC((byte) 'P')
;

private final byte coded;
73 changes: 73 additions & 0 deletions core/src/main/java/org/jruby/ir/operands/SymbolProc.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.jruby.ir.operands;

import org.jcodings.Encoding;
import org.jruby.ir.IRVisitor;
import org.jruby.ir.persistence.IRReaderDecoder;
import org.jruby.ir.persistence.IRWriterEncoder;
import org.jruby.ir.runtime.IRRuntimeHelpers;
import org.jruby.runtime.ThreadContext;

/**
* A literal representing proc'ified symbols, as in &:foo.
*
* Used to cache a unique and constant proc at the use site to reduce allocation and improve caching.
*/
public class SymbolProc extends ImmutableLiteral {
private final String name;
private final Encoding encoding;

public SymbolProc(String name, Encoding encoding) {
super();
this.name = name;
this.encoding = encoding;
}

@Override
public OperandType getOperandType() {
return OperandType.SYMBOL_PROC;
}

@Override
public Object createCacheObject(ThreadContext context) {
return IRRuntimeHelpers.newSymbolProc(context, name, encoding);
}

@Override
public int hashCode() {
return 47 * 7 + (int) (this.name.hashCode() ^ (this.encoding.hashCode() >>> 32));
}

@Override
public boolean equals(Object other) {
return other instanceof SymbolProc && name.equals(((SymbolProc) other).name) && encoding.equals(((SymbolProc) other).encoding);
}

@Override
public void visit(IRVisitor visitor) {
visitor.SymbolProc(this);
}

public String getName() {
return name;
}

public Encoding getEncoding() {
return encoding;
}

@Override
public void encode(IRWriterEncoder e) {
super.encode(e);
e.encode(name);
e.encode(encoding);
}

public static SymbolProc decode(IRReaderDecoder d) {
return new SymbolProc(d.decodeString(), d.decodeEncoding());
}

@Override
public String toString() {
return "SymbolProc:" + name;
}
}
24 changes: 24 additions & 0 deletions core/src/main/java/org/jruby/ir/runtime/IRRuntimeHelpers.java
Original file line number Diff line number Diff line change
@@ -1698,4 +1698,28 @@ public static IRubyObject useBindingSelf(Binding binding) {

return self;
}

/**
* Create a new Symbol.to_proc for the given symbol name and encoding.
*
* @param context
* @param symbol
* @return
*/
@Interp
public static RubyProc newSymbolProc(ThreadContext context, String symbol, Encoding encoding) {
return (RubyProc)context.runtime.newSymbol(symbol, encoding).to_proc(context);
}

/**
* Create a new Symbol.to_proc for the given symbol name and encoding.
*
* @param context
* @param symbol
* @return
*/
@JIT
public static RubyProc newSymbolProc(ThreadContext context, String symbol, String encoding) {
return newSymbolProc(context, symbol, retrieveJCodingsEncoding(context, encoding));
}
}
13 changes: 12 additions & 1 deletion core/src/main/java/org/jruby/ir/targets/IRBytecodeAdapter.java
Original file line number Diff line number Diff line change
@@ -351,14 +351,25 @@ public void pushBlockBody(Handle handle, org.jruby.runtime.Signature signature,
* Stack required: none
*
* @param sym the symbol's string identifier
* @param encoding the symbol's encoding
*/
public abstract void pushSymbol(String sym, Encoding encoding);

/**
* Push the JRuby runtime on the stack.
* Push a Symbol.to_proc on the stack.
*
* Stack required: none
*
* @param name the symbol's string identifier
* @param encoding the symbol's encoding
*/
public abstract void pushSymbolProc(String name, Encoding encoding);

/**
* Push the JRuby runtime on the stack.
*
* Stack required: none
*/
public abstract void loadRuntime();

/**
13 changes: 13 additions & 0 deletions core/src/main/java/org/jruby/ir/targets/IRBytecodeAdapter6.java
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@
import org.jruby.RubyFloat;
import org.jruby.RubyHash;
import org.jruby.RubyModule;
import org.jruby.RubyProc;
import org.jruby.RubyRegexp;
import org.jruby.RubyString;
import org.jruby.RubySymbol;
@@ -261,6 +262,18 @@ public void run() {
});
}

public void pushSymbolProc(final String name, final Encoding encoding) {
cacheValuePermanently("symbolProc", RubyProc.class, null, new Runnable() {
@Override
public void run() {
loadContext();
adapter.ldc(name);
adapter.ldc(encoding.toString());
invokeIRHelper("newSymbolProc", sig(RubyProc.class, ThreadContext.class, String.class, String.class));
}
});
}

public void loadRuntime() {
loadContext();
adapter.getfield(p(ThreadContext.class), "runtime", ci(Ruby.class));
5 changes: 5 additions & 0 deletions core/src/main/java/org/jruby/ir/targets/JVMVisitor.java
Original file line number Diff line number Diff line change
@@ -2306,6 +2306,11 @@ public void Symbol(Symbol symbol) {
jvmMethod().pushSymbol(symbol.getName(), symbol.getEncoding());
}

@Override
public void SymbolProc(SymbolProc symbolproc) {
jvmMethod().pushSymbolProc(symbolproc.getName(), symbolproc.getEncoding());
}

@Override
public void TemporaryVariable(TemporaryVariable temporaryvariable) {
jvmLoadLocal(temporaryvariable);
13 changes: 13 additions & 0 deletions core/src/main/java/org/jruby/runtime/Binding.java
Original file line number Diff line number Diff line change
@@ -33,15 +33,28 @@
package org.jruby.runtime;

import org.jruby.Ruby;
import org.jruby.RubyBasicObject;
import org.jruby.parser.StaticScopeFactory;
import org.jruby.runtime.backtrace.BacktraceElement;
import org.jruby.parser.StaticScope;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.runtime.scope.ManyVarsDynamicScope;
import org.jruby.runtime.scope.NoVarsDynamicScope;

/**
* Internal live representation of a block ({...} or do ... end).
*/
public class Binding {

public static final Binding DUMMY =
new Binding(
RubyBasicObject.NEVER,
new Frame(),
Visibility.PUBLIC,
new NoVarsDynamicScope(StaticScopeFactory.newStaticScope(null, StaticScope.Type.BLOCK, null)),
"<dummy>",
"dummy",
-1);

/**
* frame of method which defined this block

1 comment on commit 69662ab

@chrisseaton
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need a binding anyway? MRI:

def used_as_block
end

def accepts_block(&block)
  p block.binding
end

accepts_block(&:used_as_block)
test.rb:5:in `binding': Can't create Binding from C level Proc (ArgumentError)
    from test.rb:5:in `accepts_block'
    from test.rb:8:in `<main>'

Is the 'C level Proc' some kind of indirection written in C that does send(:symbol)?

Please sign in to comment.