Skip to content

Commit

Permalink
Introduce FString to cache common conversions
Browse files Browse the repository at this point in the history
An FString is a frozen string that has additionally been de-
duplicated and cached. Since such strings are often later
converted to symbols, integers, or floats, the FString class adds
fields to lazily cache those converted values. This helps, for
example, when using APIs that can take either strings or symbols
but need a symbol internally; if the incoming string is an FString
the symbol created from it will be cached and more readily
accessible.

Performance of FString conversions is many times faster with this
change at the cost of all FStrings having three additional
reference fields.

Warming up --------------------------------------
       intern normal     6.203M i/100ms
      intern fstring    25.811M i/100ms
         to_i normal     9.876M i/100ms
        to_i fstring    27.629M i/100ms
         to_f normal    14.780M i/100ms
        to_f fstring    27.612M i/100ms
Calculating -------------------------------------
       intern normal     62.499M (± 0.5%) i/s -    316.353M in   5.061824s
      intern fstring    274.520M (± 1.0%) i/s -      1.394B in   5.077736s
         to_i normal     97.868M (± 1.3%) i/s -    493.824M in   5.046716s
        to_i fstring    273.394M (± 1.3%) i/s -      1.381B in   5.053858s
         to_f normal    146.627M (± 1.2%) i/s -    739.006M in   5.040805s
        to_f fstring    271.105M (± 1.5%) i/s -      1.381B in   5.093658s
  • Loading branch information
headius committed Apr 1, 2024
1 parent bb5e46e commit 1f12fa0
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 4 deletions.
62 changes: 62 additions & 0 deletions bench/bench_string_conversions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen-string-literal: true

require 'benchmark/ips'

def intern(a)
a.intern
end

def to_i(a)
a.to_i
end

def to_f(a)
a.to_f
end

FSTRING = "string"
NORMAL = +FSTRING

Benchmark.ips do |bm|
bm.report("intern normal") do |i|
while i > 0
intern(NORMAL)
i-=1
end
end

bm.report("intern fstring") do |i|
while i > 0
intern(FSTRING)
i-=1
end
end

bm.report("to_i normal") do |i|
while i > 0
to_i(NORMAL)
i-=1
end
end

bm.report("to_i fstring") do |i|
while i > 0
to_i(FSTRING)
i-=1
end
end

bm.report("to_f normal") do |i|
while i > 0
to_f(NORMAL)
i-=1
end
end

bm.report("to_f fstring") do |i|
while i > 0
to_f(FSTRING)
i-=1
end
end
end
4 changes: 1 addition & 3 deletions core/src/main/java/org/jruby/Ruby.java
Original file line number Diff line number Diff line change
Expand Up @@ -4873,9 +4873,7 @@ public RubyString freezeAndDedupString(RubyString string) {
DEDUP_WRAPPER_CACHE.remove();

// Never use incoming value as key
deduped = string.strDup(this);
deduped.setFlag(ObjectFlags.FSTRING, true);
deduped.setFrozen(true);
deduped = string.dupAsFString(this, stringClass);

final WeakReference<RubyString> weakref = new WeakReference<>(deduped);

Expand Down
62 changes: 61 additions & 1 deletion core/src/main/java/org/jruby/RubyString.java
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ public class RubyString extends RubyObject implements CharSequence, EncodingCapa

public static RubyString[] NULL_ARRAY = {};

private volatile int shareLevel = SHARE_LEVEL_NONE;
protected volatile int shareLevel = SHARE_LEVEL_NONE;

private ByteList value;

Expand Down Expand Up @@ -861,6 +861,15 @@ final RubyString strDup(Ruby runtime, RubyClass clazz) {
return dup;
}

public FString dupAsFString(Ruby runtime, RubyClass clazz) {
shareLevel = SHARE_LEVEL_BYTELIST;
FString dup = new FString(runtime, clazz, value, getCodeRange());
dup.shareLevel = SHARE_LEVEL_BYTELIST;
dup.flags |= ObjectFlags.FSTRING | FROZEN_F | (flags & CR_MASK);

return dup;
}

/* rb_str_subseq */
public final RubyString makeSharedString(Ruby runtime, int index, int len) {
return makeShared(runtime, runtime.getString(), value, index, len);
Expand Down Expand Up @@ -1067,6 +1076,57 @@ protected void frozenCheck() {
}
}

/**
* An FString is a frozen string that is also deduplicated and cached. We add a few fields for common conversions
* since they'll only be cached in one place and if converted once they'll likely be used again.
*/
public static class FString extends RubyString {
private RubySymbol symbol;
private IRubyObject integer;
private IRubyObject flote;

protected FString(Ruby runtime, RubyClass rubyClass, ByteList value, int cr) {
super(runtime, rubyClass, value, cr);

// set flag for code that does not use isFrozen
setFrozen(true);
}

@Override
protected void frozenCheck() {
Ruby runtime = getRuntime();

throw runtime.newFrozenError("String", this);
}

@Override
public RubySymbol intern() {
RubySymbol symbol = this.symbol;
if (symbol == null) {
this.symbol = symbol = getRuntime().newSymbol(getByteList());
}
return symbol;
}

@Override
public IRubyObject to_i() {
IRubyObject integer = this.integer;
if (integer == null) {
this.integer = integer = super.to_i();
}
return integer;
}

@Override
public IRubyObject to_f() {
IRubyObject flote = this.flote;
if (flote == null) {
this.flote = flote = super.to_f();
}
return flote;
}
}

/** rb_str_resize
*/
public final void resize(final int size) {
Expand Down

0 comments on commit 1f12fa0

Please sign in to comment.