Skip to content

Commit 0ac2d54

Browse files
dougqhdevflow.devflow-routing-intake
andauthored
Adding lighter String processing methods to Strings (#10640)
Adding to StringUtiils - fast replaceAll for a fixed string & replacement, 3x throughput compared to regex based solutions, 1/2x allocation compared to regex solutions - added SubSequence which provides a view into a subsequence of a String without incurring extra allocation - Strings.spliit returns an Iterable<SubSequence> can be used to do light weight processing of a String Checking hashCode Reducing whitespace Fixing ugly spotless formatting Fixing ugly spotless formatting Renaming benchmark method Clarifying Javadoc grammar Allowing forbidden API usage These benchmarks exist to show why the APIs are forbidden Merge branch 'master' into dougqh/strings-improvements Merge branch 'master' into dougqh/strings-improvements Adding String.replace to the benchmark grammar Added comment explaining implementation approach Clean-up imports spotless Merge branch 'master' into dougqh/strings-improvements Fixing package after moving code from stand-alone reproduction Merge branch 'dougqh/strings-improvements' of github.com:DataDog/dd-trace-java into dougqh/strings-improvements Add motivation to SubSequence Javadoc (why avoid the substring allocation) Per bric3 review: the doc said what SubSequence avoids allocating but not why it matters. Explain the use case — allocation-free lightweight parsing: substring/ subSequence copy per call, so splitting a string into many pieces on a hot path allocates O(pieces) Strings; SubSequence is a zero-copy view. Also fixes a typo. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Merge remote-tracking branch 'origin/master' into dougqh/strings-improvements Merge branch 'master' into dougqh/strings-improvements Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
1 parent 29af0b4 commit 0ac2d54

7 files changed

Lines changed: 765 additions & 0 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package datadog.trace.util;
2+
3+
import de.thetaphi.forbiddenapis.SuppressForbidden;
4+
import java.util.regex.Pattern;
5+
import org.openjdk.jmh.annotations.Benchmark;
6+
import org.openjdk.jmh.annotations.Fork;
7+
import org.openjdk.jmh.annotations.Measurement;
8+
import org.openjdk.jmh.annotations.Threads;
9+
import org.openjdk.jmh.annotations.Warmup;
10+
11+
/**
12+
* <p>For simple replacements, Strings.replaceAll is recommened.
13+
*
14+
* <p>
15+
* For simple replacements, Strings.replaceAll or String.replace out performs the regex based
16+
* methods String.replaceAll and regex.Matcher.replaceAll by 3x in terms of throughput.
17+
*
18+
* <p>String.replace and Strings.replaceAll also require less allocation.
19+
*
20+
* <p>Strings.replaceAll out performs String.replace by 1.2x in terms of throughput,
21+
* but results may vary depending on the JVM version being used.
22+
*
23+
* <p>When pattern matching is needed, compiling the regex to Pattern slightly improves overhead,
24+
* but dramatically reduces memory allocation to 1/4x of String.replaceAll. <code>
25+
* MacBook M1 with 8 threads (Java 21)
26+
*
27+
* <code>
28+
* MacBook M1 - 8 Threads - Java 21
29+
*
30+
* StringReplaceAllBenchmark.regex_replaceAll thrpt 6 15500559.098 ± 8640183.754 ops/s
31+
* StringReplaceAllBenchmark.regex_replaceAll:gc.alloc.rate thrpt 6 4516.464 ± 2561.063 MB/sec
32+
*
33+
* StringReplaceAllBenchmark.string_replace thrpt 6 35429131.963 ± 3203548.932 ops/s
34+
* StringReplaceAllBenchmark.string_replace:gc.alloc.rate thrpt 6 3185.108 ± 152.601 MB/sec
35+
*
36+
* StringReplaceAllBenchmark.string_replaceAll thrpt 6 14253964.929 ± 4060225.866 ops/s
37+
* StringReplaceAllBenchmark.string_replaceAll:gc.alloc.rate thrpt 6 11114.939 ± 3129.891 MB/sec
38+
*
39+
* StringReplaceAllBenchmark.strings_replaceAll thrpt 6 43789250.524 ± 1910948.420 ops/s
40+
* StringReplaceAllBenchmark.strings_replaceAll:gc.alloc.rate thrpt 6 3079.973 ± 134.617 MB/sec
41+
* </code>
42+
*/
43+
@Fork(2)
44+
@Warmup(iterations = 2)
45+
@Measurement(iterations = 3)
46+
@Threads(8)
47+
@SuppressForbidden
48+
public class StringReplaceAllBenchmark {
49+
static final String[] INPUTS = {
50+
"foo",
51+
"baz",
52+
"foobar",
53+
"foobaz",
54+
"foo=baz",
55+
"bar=foo",
56+
"foo=foo&bar=foo",
57+
"lorem ipsum",
58+
"datadog"
59+
};
60+
61+
static int sharedInputIndex = 0;
62+
63+
static String nextInput() {
64+
int localIndex = ++sharedInputIndex;
65+
if (localIndex >= INPUTS.length) {
66+
sharedInputIndex = localIndex = 0;
67+
}
68+
return INPUTS[localIndex];
69+
}
70+
71+
@Benchmark
72+
public String string_replaceAll() {
73+
return _string_replaceAll(nextInput());
74+
}
75+
76+
static String _string_replaceAll(String input) {
77+
// Underneath, this does Pattern.compile("foo").matcher(str).replaceAll()
78+
return input.replaceAll("foo", "*redacted*");
79+
}
80+
81+
@Benchmark
82+
public String string_replace() {
83+
return _string_replace(nextInput());
84+
}
85+
86+
static String _string_replace(String input) {
87+
return input.replace("foo", "*redacted*");
88+
}
89+
90+
static final Pattern REGEX_COMPILED = Pattern.compile("foo");
91+
92+
@Benchmark
93+
public String regex_replaceAll() {
94+
return _regex_replaceAll(nextInput());
95+
}
96+
97+
static String _regex_replaceAll(String input) {
98+
return REGEX_COMPILED.matcher(input).replaceAll("*redcated*");
99+
}
100+
101+
@Benchmark
102+
public String strings_replaceAll() {
103+
return _strings_replaceAll(nextInput());
104+
}
105+
106+
static String _strings_replaceAll(String input) {
107+
return Strings.replaceAll(input, "foo", "*redacted*");
108+
}
109+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package datadog.trace.util;
2+
3+
import de.thetaphi.forbiddenapis.SuppressForbidden;
4+
import java.util.regex.Pattern;
5+
import org.openjdk.jmh.annotations.Benchmark;
6+
import org.openjdk.jmh.annotations.Fork;
7+
import org.openjdk.jmh.annotations.Measurement;
8+
import org.openjdk.jmh.annotations.Param;
9+
import org.openjdk.jmh.annotations.Scope;
10+
import org.openjdk.jmh.annotations.State;
11+
import org.openjdk.jmh.annotations.Threads;
12+
import org.openjdk.jmh.annotations.Warmup;
13+
import org.openjdk.jmh.infra.Blackhole;
14+
15+
/**
16+
* Strings.split is generally faster for String processing, since it creates SubSequences that are
17+
* views into the backing String rather than new String objects. <code>
18+
* Benchmark (testStr) Mode Cnt Score Error Units
19+
* StringSplitBenchmark.pattern_split EMPTY thrpt 6 291274421.621 ± 14834420.899 ops/s
20+
* StringSplitBenchmark.string_split EMPTY thrpt 6 1035461179.368 ± 60212686.921 ops/s
21+
* StringSplitBenchmark.strings_split EMPTY thrpt 6 8161781738.019 ± 178530888.497 ops/s
22+
*
23+
* StringSplitBenchmark.pattern_split TRIVIAL thrpt 6 83982270.075 ± 10250565.633 ops/s
24+
* StringSplitBenchmark.string_split TRIVIAL thrpt 6 848615850.339 ± 42453569.634 ops/s
25+
* StringSplitBenchmark.strings_split TRIVIAL thrpt 6 1765290890.948 ± 160053487.111 ops/s
26+
*
27+
* StringSplitBenchmark.pattern_split SMALL thrpt 6 27383819.756 ± 5454020.100 ops/s
28+
* StringSplitBenchmark.string_split SMALL thrpt 6 149047480.037 ± 6124271.615 ops/s
29+
* StringSplitBenchmark.strings_split SMALL thrpt 6 564058097.162 ± 49305418.971 ops/s
30+
*
31+
* StringSplitBenchmark.pattern_split MEDIUM thrpt 6 14879131.729 ± 1981850.920 ops/s
32+
* StringSplitBenchmark.string_split MEDIUM thrpt 6 51237769.598 ± 1808521.138 ops/s
33+
* StringSplitBenchmark.strings_split MEDIUM thrpt 6 176976970.705 ± 6813886.658 ops/s
34+
*
35+
* StringSplitBenchmark.pattern_split LARGE thrpt 6 482340.838 ± 24903.187 ops/s
36+
* StringSplitBenchmark.string_split LARGE thrpt 6 2460212.879 ± 86911.652 ops/s
37+
* StringSplitBenchmark.strings_split LARGE thrpt 6 4023658.103 ± 30305.699 ops/s
38+
* </code>
39+
*/
40+
@Fork(2)
41+
@Warmup(iterations = 2)
42+
@Measurement(iterations = 3)
43+
@Threads(8)
44+
@State(Scope.Benchmark)
45+
@SuppressForbidden
46+
public class StringSplitBenchmark {
47+
public enum TestString {
48+
EMPTY(""),
49+
TRIVIAL("app_key=1111"),
50+
SMALL("app_key=1111&foo=bar&baz=quux"),
51+
MEDIUM(repeat("app_key=1111", '&', 100)),
52+
LARGE(repeat("app_key=1111&application_key=2222&token=0894-4832", '&', 4096));
53+
54+
final String str;
55+
56+
TestString(String str) {
57+
this.str = str;
58+
}
59+
};
60+
61+
@Param TestString testStr;
62+
63+
static final String repeat(String repeat, char separator, int length) {
64+
StringBuilder builder = new StringBuilder(length);
65+
builder.append(repeat);
66+
while (builder.length() + repeat.length() + 1 < length) {
67+
builder.append(separator).append(repeat);
68+
}
69+
return builder.toString();
70+
}
71+
72+
@Benchmark
73+
public void string_split(Blackhole bh) {
74+
for (String substr : this.testStr.str.split("\\&")) {
75+
bh.consume(substr);
76+
}
77+
}
78+
79+
static final Pattern PATTERN = Pattern.compile("\\&");
80+
81+
@Benchmark
82+
public void pattern_split(Blackhole bh) {
83+
for (String str : PATTERN.split(this.testStr.str)) {
84+
bh.consume(str);
85+
}
86+
}
87+
88+
@Benchmark
89+
public void strings_split(Blackhole bh) {
90+
for (SubSequence subSeq : Strings.split(this.testStr.str, '&')) {
91+
bh.consume(subSeq);
92+
}
93+
}
94+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package datadog.trace.util;
2+
3+
import org.openjdk.jmh.annotations.Benchmark;
4+
import org.openjdk.jmh.annotations.Fork;
5+
import org.openjdk.jmh.annotations.Measurement;
6+
import org.openjdk.jmh.annotations.Threads;
7+
import org.openjdk.jmh.annotations.Warmup;
8+
import org.openjdk.jmh.infra.Blackhole;
9+
10+
/**
11+
* Strings.substring has 5x throughput. This is primarily achieved through less allocation.
12+
*
13+
* <p>NOTE: The higher allocation rate is misleading because 5x the work was performed. After
14+
* accounting for the 5x throughput difference, the actual allocation rate is 0.25x that of
15+
* String.substring or String.subSequence / SubSequence.of. <code>
16+
* Benchmark Mode Cnt Score Error Units
17+
* StringSubSequenceBenchmark.string_subSequence thrpt 6 140369998.493 ± 4387855.861 ops/s
18+
* StringSubSequenceBenchmark.string_subSequence:gc.alloc.rate thrpt 6 88880.463 ± 2778.032 MB/sec
19+
*
20+
* StringSubSequenceBenchmark.string_substring thrpt 6 136916708.207 ± 12299226.575 ops/s
21+
* StringSubSequenceBenchmark.string_substring:gc.alloc.rate thrpt 6 86689.852 ± 7777.642 MB/sec
22+
*
23+
* StringSubSequenceBenchmark.subSequence thrpt 6 679669385.260 ± 7194043.619 ops/s
24+
* StringSubSequenceBenchmark.subSequence:gc.alloc.rate thrpt 6 103702.745 ± 1095.741 MB/sec
25+
* </code>
26+
*/
27+
@Fork(2)
28+
@Warmup(iterations = 2)
29+
@Measurement(iterations = 3)
30+
@Threads(8)
31+
public class StringSubSequenceBenchmark {
32+
static final String LOREM_IPSUM =
33+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
34+
35+
@Benchmark
36+
public void string_substring(Blackhole bh) {
37+
String str = LOREM_IPSUM;
38+
int len = str.length();
39+
40+
for (int i = 0; i < str.length(); i += 100) {
41+
bh.consume(str.substring(i, Math.min(i + 100, len)));
42+
}
43+
}
44+
45+
@Benchmark
46+
public void string_subSequence(Blackhole bh) {
47+
String str = LOREM_IPSUM;
48+
int len = str.length();
49+
50+
for (int i = 0; i < str.length(); i += 100) {
51+
bh.consume(str.subSequence(i, Math.min(i + 100, len)));
52+
}
53+
}
54+
55+
@Benchmark
56+
public void subSequence(Blackhole bh) {
57+
String str = LOREM_IPSUM;
58+
int len = str.length();
59+
60+
for (int i = 0; i < str.length(); i += 100) {
61+
bh.consume(SubSequence.of(str, i, Math.min(i + 100, len)));
62+
}
63+
}
64+
}

0 commit comments

Comments
 (0)