【Java】基础功能性能测试

240 阅读3分钟

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();
}

字符串拼接

字符串的拼接操作总共有 + 操作符、StringBuilderStringBuffer,我们比较下吧。

  • 测试代码

    @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
    

    可以看到,使用 + 操作符的性能简直惨不忍睹,对于字符串常量使用 + 是没有问题的,因为前端编译器会进行常量合并优化。但是对于变量的 + 操作,某些编译器可能会自动转化为 StringBufferStringBuilder 来实现,但是大部分编译器还是不会这么操作的。因此不要使用 + 操作符进行字符串合并操作。

    StringBuilderStringBuffer 在性能上的差异并不会太明显,因此,如果是开发多线程程序的话,如果为了减少错误隐患的角度编码,可以全部使用 StringBuffer;如果为了性能考虑,还是要考虑是否能够使用 StringBuilder 的。