Java 很多基础组件在不同环境或者数据条件下会有比较大的性能差异,但理论还需实际测试,因此会将一些常见的组件性能比较在这篇文章中测试并记录。
- 文章是记录型文章,内容会不断拓展与补充。
单线程排序与并行排序
-
测试代码:
@BenchmarkMode(Mode.AverageTime) @Warmup(iterations = 10, time = 10) @Measurement(iterations = 5, time = 15) @OutputTimeUnit(TimeUnit.MILLISECONDS) @State(Scope.Benchmark) @Fork(2) public class TestParallelSort { int[] nums; @Setup(Level.Invocation) public void setUp() { nums = new int[1000_0000]; Random random = new Random(); for (int i = 0; i < nums.length; i++) nums[i] = random.nextInt(); } @Benchmark public void testSingleThreadSort() { Arrays.sort(nums); } @Benchmark public void testParallelSort() { Arrays.parallelSort(nums); } }
-
结果:
Benchmark Mode Cnt Score Error Units TestParallelSort.testParallelSort avgt 10 93.315 ± 1.227 ms/op TestParallelSort.testSingleThreadSort avgt 10 713.643 ± 3.333 ms/op
可以看到,数据量大的时候,并行排序效率要比单线程排序好太多。
看一下 Arrays::parallelSort
的代码,可以看到当数组长度小于 1 << 13 = 8192
时会使用单线程多路快排的方式进行排序,因此并行排序在数据量小于 8192 时,或许多线程操作带来的耗时会比收益要高。因此如果有排序需求,直接使用并行排序即可,不必担心数据量过小的问题。
public static void parallelSort(int[] a) {
int n = a.length, p, g;
if (n <= MIN_ARRAY_SORT_GRAN
|| (p = ForkJoinPool.getCommonPoolParallelism()) == 1)
DualPivotQuicksort.sort(a, 0, n - 1, null, 0, 0);
else
new ArraysParallelSortHelpers.FJInt.Sorter(null, a, new int[n], 0, n, 0,
((g = n / (p << 2)) <= MIN_ARRAY_SORT_GRAN) ? MIN_ARRAY_SORT_GRAN : g
).invoke();
}
字符串拼接
字符串的拼接操作总共有 +
操作符、StringBuilder
和 StringBuffer
,我们比较下吧。
-
测试代码:
@BenchmarkMode(Mode.AverageTime) @Warmup(iterations = 10, time = 2) @Measurement(iterations = 5, time = 5) @OutputTimeUnit(TimeUnit.MILLISECONDS) @Fork(2) @State(Scope.Benchmark) public class TestStringAppend { @Param({"1000", "10000", "100000"}) private int len; @Benchmark public void testAddOperator(Blackhole blackhole) { String str = ""; for (int i = 0; i < len; i++) str = str + i; blackhole.consume(str); } @Benchmark public void testStringBuilder(Blackhole blackhole) { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < len; i++) stringBuilder.append(i); blackhole.consume(stringBuilder.toString()); } @Benchmark public void testStringBuffer(Blackhole blackhole) { StringBuffer stringBuffer = new StringBuffer(); for (int i = 0; i < len; i++) stringBuffer.append(i); blackhole.consume(stringBuffer.toString()); } }
-
结果:
Benchmark (len) Mode Cnt Score Error Units TestStringAppend.testAddOperator 1000 avgt 10 0.407 ± 0.003 ms/op TestStringAppend.testAddOperator 10000 avgt 10 45.773 ± 0.285 ms/op TestStringAppend.testAddOperator 100000 avgt 10 7675.274 ± 198.300 ms/op TestStringAppend.testStringBuffer 1000 avgt 10 0.012 ± 0.001 ms/op TestStringAppend.testStringBuffer 10000 avgt 10 0.147 ± 0.002 ms/op TestStringAppend.testStringBuffer 100000 avgt 10 1.486 ± 0.022 ms/op TestStringAppend.testStringBuilder 1000 avgt 10 0.011 ± 0.001 ms/op TestStringAppend.testStringBuilder 10000 avgt 10 0.138 ± 0.001 ms/op TestStringAppend.testStringBuilder 100000 avgt 10 1.335 ± 0.022 ms/op
可以看到,使用
+
操作符的性能简直惨不忍睹,对于字符串常量使用+
是没有问题的,因为前端编译器会进行常量合并优化。但是对于变量的+
操作,某些编译器可能会自动转化为StringBuffer
或StringBuilder
来实现,但是大部分编译器还是不会这么操作的。因此不要使用+
操作符进行字符串合并操作。而
StringBuilder
与StringBuffer
在性能上的差异并不会太明显,因此,如果是开发多线程程序的话,如果为了减少错误隐患的角度编码,可以全部使用StringBuffer
;如果为了性能考虑,还是要考虑是否能够使用StringBuilder
的。