Browse files

Improve pre-frozen string logic with a deduplication cache.

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...
1 parent 3b8b790 commit 926ca89075a4a4c84592add729531189263c143f @headius headius committed Jan 3, 2014
View
32 core/src/main/java/org/jruby/Ruby.java
@@ -127,6 +127,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
+import java.lang.ref.WeakReference;
import java.net.BindException;
import java.nio.channels.ClosedChannelException;
import java.security.AccessControlException;
@@ -4514,6 +4515,30 @@ public RubyString getDefinedMessage(DefinedMessage definedMessage) {
public RubyString getThreadStatus(RubyThread.Status 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() {
try {
@@ -4855,4 +4880,11 @@ public void addToObjectSpace(boolean useObjectSpace, IRubyObject object) {
}
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>>();
}
View
11 core/src/main/java/org/jruby/ast/CallNoArgNode.java
@@ -45,18 +45,29 @@
* A method or operator call.
*/
public final class CallNoArgNode extends CallNode {
+ private final boolean literalStringFreeze;
+ private transient RubyString literalFrozenString;
+
// For 'b.foo'
public CallNoArgNode(ISourcePosition position, Node receiverNode, String name) {
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
public CallNoArgNode(ISourcePosition position, Node receiverNode, Node args, String name) {
super(position, receiverNode, name, args, null);
+ literalStringFreeze = receiverNode instanceof StrNode && name.equals("freeze");
}
@Override
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));
}
View
2 core/src/main/java/org/jruby/ast/executable/RuntimeCache.java
@@ -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) {
RubyString str = frozenStrings[stringIndex];
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;
}
View
2 core/src/main/java/org/jruby/runtime/invokedynamic/InvokeDynamicSupport.java
@@ -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) {
- 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));
return string;
}

0 comments on commit 926ca89

Please sign in to comment.