一個清除數組的方法在 Kotlin、Java、C#和Nim上的性能測試

起因

我的一個項目使用 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編譯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章