OutOfMemory 与 JMH 和 Mode.AverageTime
OutOfMemory with JMH and Mode.AverageTime
我正在编写一个微型基准测试来比较使用 + 运算符与 StringBuilder 的字符串连接。为此,我基于 OpenJDK example that uses the batchSize parameter:
创建了一个 JMH 基准 class
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@Measurement(batchSize = 10000, iterations = 10)
@Warmup(batchSize = 10000, iterations = 10)
@Fork(1)
public class StringConcatenationBenchmark {
private String string;
private StringBuilder stringBuilder;
@Setup(Level.Iteration)
public void setup() {
string = "";
stringBuilder = new StringBuilder();
}
@Benchmark
public void stringConcatenation() {
string += "some more data";
}
@Benchmark
public void stringBuilderConcatenation() {
stringBuilder.append("some more data");
}
}
当我 运行 基准测试时,stringBuilderConcatenation
方法出现以下错误:
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:137)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:121)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:421)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at link.pellegrino.string_concatenation.StringConcatenationBenchmark.stringBuilderConcatenation(StringConcatenationBenchmark.java:29)
at link.pellegrino.string_concatenation.generated.StringConcatenationBenchmark_stringBuilderConcatenation.stringBuilderConcatenation_avgt_jmhStub(StringConcatenationBenchmark_stringBuilderConcatenation.java:165)
at link.pellegrino.string_concatenation.generated.StringConcatenationBenchmark_stringBuilderConcatenation.stringBuilderConcatenation_AverageTime(StringConcatenationBenchmark_stringBuilderConcatenation.java:130)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.openjdk.jmh.runner.BenchmarkHandler$BenchmarkTask.call(BenchmarkHandler.java:430)
at org.openjdk.jmh.runner.BenchmarkHandler$BenchmarkTask.call(BenchmarkHandler.java:412)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
我在想必须增加默认的 JVM 堆大小,所以我尝试使用 JMH 提供的 -Xmx10G
值和 -jvmArgs
选项允许最多 10GB。不幸的是,我仍然收到错误。
因此,我尝试将 batchSize
参数的值减小为 1,但我仍然得到 OutOfMemoryError。
我发现的唯一解决方法是将基准模式设置为 Mode.SingleShotTime
。由于此模式似乎将批次视为单次拍摄(即使 s/op 显示在 Units 列中),似乎我得到了我想要的指标:平均值执行一组批处理操作的时间。但是,我仍然不明白为什么它不能与 Mode.AverageTime
.
一起使用
另请注意,无论使用何种基准测试模式,方法 stringConcatenation
的基准测试都按预期工作。该问题仅出现在使用 StringBuilder 的 stringBuilderConcatenation
方法中。
欢迎提供任何有助于理解为什么前面的示例无法在设置为 Mode.AverageTime
的基准模式下工作的帮助。
我用的JMH版本是1.10.4.
您说得对,Mode.SingleShotTime
正是您所需要的:它测量单个批次的时间。使用 Mode.AverageTime
时,您的迭代仍然有效,直到迭代时间结束(默认为 1 秒)。它测量的是每次执行单个批次的时间(只计算在执行时间内完全完成的批次),所以最终结果不同,但执行时间是一样的。
另一个问题是 @Setup(Level.Iteration)
强制设置在每次迭代之前执行,而不是在每个批次之前执行。因此,您的字符串实际上不受批量大小的限制。字符串版本不会导致 OutOfMemoryError
只是因为它比 StringBuilder
慢得多,所以在 1 秒内它能够构建更短的字符串。
修复基准测试的不太好的方法(同时仍然使用平均时间模式和 batchSize 参数)是手动重置 string/stringBuilder:
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Measurement(batchSize = 10000, iterations = 10)
@Warmup(batchSize = 10000, iterations = 10)
@Fork(1)
public class StringConcatenationBenchmark {
private static final String S = "some more data";
private static final int maxLen = S.length()*10000;
private String string;
private StringBuilder stringBuilder;
@Setup(Level.Iteration)
public void setup() {
string = "";
stringBuilder = new StringBuilder();
}
@Benchmark
public void stringConcatenation() {
if(string.length() >= maxLen) string = "";
string += S;
}
@Benchmark
public void stringBuilderConcatenation() {
if(stringBuilder.length() >= maxLen) stringBuilder = new StringBuilder();
stringBuilder.append(S);
}
}
这是我盒子上的结果(i5 3340、4Gb 内存、64 位 Win7、JDK 1.8.0_45):
Benchmark Mode Cnt Score Error Units
stringBuilderConcatenation avgt 10 145.997 ± 2.301 us/op
stringConcatenation avgt 10 324878.341 ± 39824.738 us/op
因此您可以看到只有大约 3 个批次适合 stringConcatenation
(1e6/324878
) 的第二个批次,而对于 stringBuilderConcatenation
可以执行数千个批次导致巨大的字符串导致 OutOfMemoryError
.
我不知道为什么添加更多内存对您不起作用,对我来说 -Xmx4G
足以 运行 原始基准测试的 stringBuilder 测试。可能您的盒子速度更快,因此生成的字符串甚至更长。请注意,对于非常大的字符串,即使您有足够的内存,您也可以达到数组大小限制(20 亿个元素)。添加内存后检查异常堆栈跟踪:是否相同?如果达到数组大小限制,它仍将是 OutOfMemoryError
,但堆栈跟踪会有所不同。无论如何,即使有足够的内存,基准测试的结果也会不正确(String
和 StringBuilder
)。
我正在编写一个微型基准测试来比较使用 + 运算符与 StringBuilder 的字符串连接。为此,我基于 OpenJDK example that uses the batchSize parameter:
创建了一个 JMH 基准 class@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@Measurement(batchSize = 10000, iterations = 10)
@Warmup(batchSize = 10000, iterations = 10)
@Fork(1)
public class StringConcatenationBenchmark {
private String string;
private StringBuilder stringBuilder;
@Setup(Level.Iteration)
public void setup() {
string = "";
stringBuilder = new StringBuilder();
}
@Benchmark
public void stringConcatenation() {
string += "some more data";
}
@Benchmark
public void stringBuilderConcatenation() {
stringBuilder.append("some more data");
}
}
当我 运行 基准测试时,stringBuilderConcatenation
方法出现以下错误:
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:137)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:121)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:421)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at link.pellegrino.string_concatenation.StringConcatenationBenchmark.stringBuilderConcatenation(StringConcatenationBenchmark.java:29)
at link.pellegrino.string_concatenation.generated.StringConcatenationBenchmark_stringBuilderConcatenation.stringBuilderConcatenation_avgt_jmhStub(StringConcatenationBenchmark_stringBuilderConcatenation.java:165)
at link.pellegrino.string_concatenation.generated.StringConcatenationBenchmark_stringBuilderConcatenation.stringBuilderConcatenation_AverageTime(StringConcatenationBenchmark_stringBuilderConcatenation.java:130)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.openjdk.jmh.runner.BenchmarkHandler$BenchmarkTask.call(BenchmarkHandler.java:430)
at org.openjdk.jmh.runner.BenchmarkHandler$BenchmarkTask.call(BenchmarkHandler.java:412)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
我在想必须增加默认的 JVM 堆大小,所以我尝试使用 JMH 提供的 -Xmx10G
值和 -jvmArgs
选项允许最多 10GB。不幸的是,我仍然收到错误。
因此,我尝试将 batchSize
参数的值减小为 1,但我仍然得到 OutOfMemoryError。
我发现的唯一解决方法是将基准模式设置为 Mode.SingleShotTime
。由于此模式似乎将批次视为单次拍摄(即使 s/op 显示在 Units 列中),似乎我得到了我想要的指标:平均值执行一组批处理操作的时间。但是,我仍然不明白为什么它不能与 Mode.AverageTime
.
另请注意,无论使用何种基准测试模式,方法 stringConcatenation
的基准测试都按预期工作。该问题仅出现在使用 StringBuilder 的 stringBuilderConcatenation
方法中。
欢迎提供任何有助于理解为什么前面的示例无法在设置为 Mode.AverageTime
的基准模式下工作的帮助。
我用的JMH版本是1.10.4.
您说得对,Mode.SingleShotTime
正是您所需要的:它测量单个批次的时间。使用 Mode.AverageTime
时,您的迭代仍然有效,直到迭代时间结束(默认为 1 秒)。它测量的是每次执行单个批次的时间(只计算在执行时间内完全完成的批次),所以最终结果不同,但执行时间是一样的。
另一个问题是 @Setup(Level.Iteration)
强制设置在每次迭代之前执行,而不是在每个批次之前执行。因此,您的字符串实际上不受批量大小的限制。字符串版本不会导致 OutOfMemoryError
只是因为它比 StringBuilder
慢得多,所以在 1 秒内它能够构建更短的字符串。
修复基准测试的不太好的方法(同时仍然使用平均时间模式和 batchSize 参数)是手动重置 string/stringBuilder:
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Measurement(batchSize = 10000, iterations = 10)
@Warmup(batchSize = 10000, iterations = 10)
@Fork(1)
public class StringConcatenationBenchmark {
private static final String S = "some more data";
private static final int maxLen = S.length()*10000;
private String string;
private StringBuilder stringBuilder;
@Setup(Level.Iteration)
public void setup() {
string = "";
stringBuilder = new StringBuilder();
}
@Benchmark
public void stringConcatenation() {
if(string.length() >= maxLen) string = "";
string += S;
}
@Benchmark
public void stringBuilderConcatenation() {
if(stringBuilder.length() >= maxLen) stringBuilder = new StringBuilder();
stringBuilder.append(S);
}
}
这是我盒子上的结果(i5 3340、4Gb 内存、64 位 Win7、JDK 1.8.0_45):
Benchmark Mode Cnt Score Error Units
stringBuilderConcatenation avgt 10 145.997 ± 2.301 us/op
stringConcatenation avgt 10 324878.341 ± 39824.738 us/op
因此您可以看到只有大约 3 个批次适合 stringConcatenation
(1e6/324878
) 的第二个批次,而对于 stringBuilderConcatenation
可以执行数千个批次导致巨大的字符串导致 OutOfMemoryError
.
我不知道为什么添加更多内存对您不起作用,对我来说 -Xmx4G
足以 运行 原始基准测试的 stringBuilder 测试。可能您的盒子速度更快,因此生成的字符串甚至更长。请注意,对于非常大的字符串,即使您有足够的内存,您也可以达到数组大小限制(20 亿个元素)。添加内存后检查异常堆栈跟踪:是否相同?如果达到数组大小限制,它仍将是 OutOfMemoryError
,但堆栈跟踪会有所不同。无论如何,即使有足够的内存,基准测试的结果也会不正确(String
和 StringBuilder
)。