Skip to content

Commit

Permalink
Improve pre-frozen string logic with a deduplication cache.
Browse files Browse the repository at this point in the history
This commit brings our "str".freeze compiler optimization in line
with MRI by using a global deduplication cache to provide for even
more sharing across literals. The cache is both weak-keyed and
weak-valued and just maps from an arbitrary RubyString to the
pre-frozen cached version of that RubyString.

Note that the logic for deduplication is synchronized against the
JRuby runtime. This will impact concurrent loads that repeatedly
ping the cache, but uses in this commit all cache the value
locally and only hit the dedup cache once. This potential
concurrency bottleneck may need to be addressed in the future.
  • Loading branch information
headius committed Jan 3, 2014
1 parent 3b8b790 commit 926ca89
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 2 deletions.
32 changes: 32 additions & 0 deletions core/src/main/java/org/jruby/Ruby.java
Expand Up @@ -127,6 +127,7 @@
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.PrintStream; import java.io.PrintStream;
import java.lang.ref.WeakReference;
import java.net.BindException; import java.net.BindException;
import java.nio.channels.ClosedChannelException; import java.nio.channels.ClosedChannelException;
import java.security.AccessControlException; import java.security.AccessControlException;
Expand Down Expand Up @@ -4514,6 +4515,30 @@ public RubyString getDefinedMessage(DefinedMessage definedMessage) {
public RubyString getThreadStatus(RubyThread.Status status) { public RubyString getThreadStatus(RubyThread.Status status) {
return threadStatuses.get(status); return threadStatuses.get(status);
} }

/**
* Given a Ruby string, cache a frozen, duplicated copy of it, or find an
* existing copy already prepared. This is used to reduce in-memory
* duplication of pre-frozen or known-frozen strings.
*
* Note that this cache is synchronized against the Ruby instance. This
* could cause contention under heavy concurrent load, so a reexamination
* of this design might be warranted.
*
* @param string the string to freeze-dup if an equivalent does not already exist
* @return the freeze-duped version of the string
*/
public synchronized RubyString freezeAndDedupString(RubyString string) {
WeakReference<RubyString> dedupedRef = dedupMap.get(string);
RubyString deduped;

if (dedupedRef == null || (deduped = dedupedRef.get()) == null) {
deduped = string.strDup(this);
deduped.setFrozen(true);
dedupMap.put(string, new WeakReference<RubyString>(deduped));
}
return deduped;
}


private void setNetworkStack() { private void setNetworkStack() {
try { try {
Expand Down Expand Up @@ -4855,4 +4880,11 @@ public void addToObjectSpace(boolean useObjectSpace, IRubyObject object) {
} }


private RubyArray emptyFrozenArray; private RubyArray emptyFrozenArray;

/**
* A map from Ruby string data to a pre-frozen global version of that string.
*
* Access must be synchronized.
*/
private WeakHashMap<RubyString, WeakReference<RubyString>> dedupMap = new WeakHashMap<RubyString, WeakReference<RubyString>>();
} }
11 changes: 11 additions & 0 deletions core/src/main/java/org/jruby/ast/CallNoArgNode.java
Expand Up @@ -45,18 +45,29 @@
* A method or operator call. * A method or operator call.
*/ */
public final class CallNoArgNode extends CallNode { public final class CallNoArgNode extends CallNode {
private final boolean literalStringFreeze;
private transient RubyString literalFrozenString;

// For 'b.foo' // For 'b.foo'
public CallNoArgNode(ISourcePosition position, Node receiverNode, String name) { public CallNoArgNode(ISourcePosition position, Node receiverNode, String name) {
super(position, receiverNode, name, null, null); super(position, receiverNode, name, null, null);
literalStringFreeze = receiverNode instanceof StrNode && name.equals("freeze");
} }


// For 'b.foo()'. Args are only significant in maintaining backwards compatible AST structure // For 'b.foo()'. Args are only significant in maintaining backwards compatible AST structure
public CallNoArgNode(ISourcePosition position, Node receiverNode, Node args, String name) { public CallNoArgNode(ISourcePosition position, Node receiverNode, Node args, String name) {
super(position, receiverNode, name, args, null); super(position, receiverNode, name, args, null);
literalStringFreeze = receiverNode instanceof StrNode && name.equals("freeze");
} }


@Override @Override
public IRubyObject interpret(Ruby runtime, ThreadContext context, IRubyObject self, Block aBlock) { public IRubyObject interpret(Ruby runtime, ThreadContext context, IRubyObject self, Block aBlock) {
if (literalStringFreeze) {
if (literalFrozenString != null) return literalFrozenString;

return literalFrozenString = runtime.freezeAndDedupString((RubyString) getReceiverNode().interpret(runtime, context, self, aBlock));
}

return callAdapter.call(context, self, getReceiverNode().interpret(runtime, context, self, aBlock)); return callAdapter.call(context, self, getReceiverNode().interpret(runtime, context, self, aBlock));
} }


Expand Down
Expand Up @@ -111,7 +111,7 @@ public final RubyString getString(ThreadContext context, int index, int codeRang
public final RubyString getFrozenString(ThreadContext context, int bytelistIndex, int stringIndex, int codeRange) { public final RubyString getFrozenString(ThreadContext context, int bytelistIndex, int stringIndex, int codeRange) {
RubyString str = frozenStrings[stringIndex]; RubyString str = frozenStrings[stringIndex];
if (str == null) { if (str == null) {
str = frozenStrings[stringIndex] = (RubyString)RubyString.newStringShared(context.runtime, getByteList(bytelistIndex), codeRange).freeze(context); str = frozenStrings[stringIndex] = context.runtime.freezeAndDedupString((RubyString) RubyString.newStringShared(context.runtime, getByteList(bytelistIndex), codeRange).freeze(context));
} }
return str; return str;
} }
Expand Down
Expand Up @@ -895,7 +895,7 @@ public static RubyString newString(ThreadContext context, ByteList contents, int
} }


public static RubyString newFrozenString(ThreadContext context, MutableCallSite site, ByteList contents, int codeRange) { public static RubyString newFrozenString(ThreadContext context, MutableCallSite site, ByteList contents, int codeRange) {
RubyString string = RubyString.newStringShared(context.runtime, contents, codeRange); RubyString string = context.runtime.freezeAndDedupString(RubyString.newStringShared(context.runtime, contents, codeRange));
site.setTarget(dropArguments(constant(RubyString.class, string), 0, ThreadContext.class)); site.setTarget(dropArguments(constant(RubyString.class, string), 0, ThreadContext.class));
return string; return string;
} }
Expand Down

0 comments on commit 926ca89

Please sign in to comment.