起因

我的一个项目使用 Kotlin 编写,他是一个多维数据库应用程序,所以会非常频繁的操作 int 数组,其中有段程序就需要进行 几亿次的数组清除动作,类似这样的代码:

Arrays.fill(target, 0);

这个Arrays.fill其实就是jdk自带的一个实现,非常简陋,就是一个for循环填充数据。

所以我想改进他,将常见的数组长度编写成单个的实现,比如清除8个长度的方法如下:

fun clear8(target: IntArray) {
    if(target.size < 8){
        throw IndexOutOfBoundsException()
    }
    target[0] = 0
    target[1] = 0
    target[2] = 0
    target[3] = 0
    target[4] = 0
    target[5] = 0
    target[6] = 0
    target[7] = 0
}

不要怀疑你的眼睛,这样的写法通常是有效的。好的编译器会优化我写的代码,当然,更好的编译器会优化一个简单数组的for循环,这是后话。

那我们就测试一下吧。

import java.util.*
import kotlin.system.measureNanoTime

fun main() {
    test3()
}


private fun test3() {
    val size = 8
    val time2 = measureNanoTime {
        val target = IntArray(size)
        for (i in 0 until 10_0000_0000) {
            IntArrays.clear8(target)
        }
    }
    println("fill$size          $time2")

    val time1 = measureNanoTime {
        val target = IntArray(size)
        for (i in 0 until 10_0000_0000) {
            Arrays.fill(target, 0)
        }
    }
    println("Arrays.fill$size   $time1")
    println()
}

internal object IntArrays {
    fun clear8(target: IntArray) {
        if(target.size < 8){
            throw IndexOutOfBoundsException()
        }
        target[0] = 0
        target[1] = 0
        target[2] = 0
        target[3] = 0
        target[4] = 0
        target[5] = 0
        target[6] = 0
        target[7] = 0
    }
}

测试结果:

fill8                    55,408,200
Arrays.fill8    2,262,171,100

可以看出,使用展开的方式,比java自带的2.2秒,性能提高了40倍!!

与Java的性能对比

我感叹kotlin的编译器真的很强,但仔细一想,不对啊, Kotlin 就是基于 JVM 的,功劳应该是 java 的虚拟机运行时很厉害,所以如果这个程序如果转化为java直接编写是不是更快,至少性能一致吧。说干就干。

//IntArrays.java
import java.util.Arrays;

final class IntArrays {
    static void clear8(int[] target) {
/*        if (target.length < 8){
            throw new IndexOutOfBoundsException();
        }*/
        target[0] = 0;
        target[1] = 0;
        target[2] = 0;
        target[3] = 0;
        target[4] = 0;
        target[5] = 0;
        target[6] = 0;
        target[7] = 0;
    }
}

// IntArraysDemoJava.java
import java.util.Arrays;

public final class IntArraysDemoJava {
    public static void main(String[] var0) {
        test1();
    }

    private static void test1() {
        long count = 1000000000;
        long start = System.nanoTime();
        final int[] target = new int[8];

        for(int i = 0; i < count; i++) {
            IntArrays.clear8(target);
        }
        long time2 = System.nanoTime() - start;
        System.out.println("fill8          " + time2);

        start = System.nanoTime();
        for(int i = 0; i < count; i++) {
            Arrays.fill(target, 0);
        }

        long time1 = System.nanoTime() - start;
        System.out.println("Arrays.fill8   " + time1);
        System.out.println();
    }
}

Java的实现

测试结果如下:

fill8                   2,018,500,800
Arrays.fill8        2,234,306,500

天啊,在java下这种优化几乎没有效果,java我没有找到什么 release编译参数的概念,最多只有debug  = false,我是在gradle中包含

compileJava {
    options.debug = false
}

那么就是说,Kotlin生成的字节码要好于 Java生成的字节码?

Java               Kotlin
ALOAD 0         ALOAD 1
ICONST_0        ICONST_0
ICONST_0        ICONST_0
IASTORE         ASTORE
  
ALOAD 0         ALOAD 1
ICONST_1        ICONST_1
ICONST_0        ICONST_0
IASTORE         IASTORE

字节码稍微不同,你要是问我为什么?  我母鸡啊。。。。。。

与C# 的对比

作为一个 .net 的死忠粉,这个时候就会想着是不是 c# 更快一些,更何况 .net core 3做了大量的性能优化,

class Program {
   static void Main(string[] args) {
       Test3.test1();
   }
}

class Test3
{
    public static void test1()
    {
        long count = 1000000000;
        var watch = System.Diagnostics.Stopwatch.StartNew();
        int[] target = new int[8];

        for (int i = 0; i < count; i++)
        {
            Clear8(target);
        }
        watch.Stop();
        Console.WriteLine("fill8          " + watch.Elapsed);

        watch.Restart();
        for (int i = 0; i < count; i++)
        {
            Array.Clear(target, 0,8);
        }

        watch.Stop();
        Console.WriteLine("Array.Clear8   " + watch.Elapsed);
        Console.WriteLine();
    }

    static void Clear8(int[] target)
    {
        /* if (target.Length < 8)
        {
            throw new IndexOutOfRangeException();
        }*/
        target[0] = 0;
        target[1] = 0;
        target[2] = 0;
        target[3] = 0;
        target[4] = 0;
        target[5] = 0;
        target[6] = 0;
        target[7] = 0;
    }
}

测试成绩:

fill8                     00:00:02.7462676
Array.Clear8      00:00:08.4920514

和Java比起来还要慢,甚至系统自带的Array.clear更加慢,这怎么能让我忍,于是一通的 Span.Fill(0),结果更不理想。

和Nim对比的性能

兴趣提起来了,那就使用C语言实现一个....... 没写出来,我笨......,那就使用 Rust 实现一个,还是没有实现出来,按照教程一步步写,还是没有搞定..........

最后折腾出来一个 Nim 环境,嗯,还是这个简单。

import times, strutils

proc clear8*[int](target: var seq[int]) =
    target[0] = 0
    target[1] = 0
    target[2] = 0
    target[3] = 0
    target[4] = 0
    target[5] = 0
    target[6] = 0
    target[7] = 0

proc clear*[int](target: var seq[int]) =
    for i in 0..<target.len:
        target[i] = 0


proc test3() =
    const size = 8
    var start = epochTime()
    var target = newseq[int](size)
    for i in 0..<10_0000_0000:
        target.clear8()
    
    let elapsedStr = (epochTime() - start).formatFloat(format = ffDecimal, precision = 3)
    echo "fill8         ", elapsedStr

    start = epochTime()
    for i in 0..<10_0000_0000:
        target.clear()
    
    let elapsedStr2 = (epochTime() - start).formatFloat(format = ffDecimal, precision = 3)
    echo "Arrays.fill   ", elapsedStr2

test3()

Nim

测试成绩,注意要加 --release 参数。

fill8 3.499
Arrays.fill 5.825

失望,及其失望。

备注

所有测试是在我的台式机上进行的,配置如下:

AMD Ryzen 5 3600 6 Core 3.59 Ghz

8 GB RAM

Windows 10 64 专业版

所有测试都使用release编译。