[譯]Effective Kotlin系列之考慮使用原始類型的數組優化性能(五)

翻譯說明:java

原標題: Effective Kotlin: Consider Arrays with primitives for performance critical processinggit

原文地址: blog.kotlin-academy.com/effective-k…github

原文做者: Marcin Moskala數組

Kotlin底層實現是很是智能的。在Kotlin中咱們不能直接聲明原始類型(也稱原語類型)的,可是當咱們不像使用對象實例那樣操做一個變量時,那麼這個變量在底層將轉換成原始類型處理。例如,請看如下示例:性能優化

var i = 10
i = i * 2
println(i)
複製代碼

上述的變量聲明在Kotlin底層是使用了原始類型int.下面這是上述例子在Java中的內部表達:app

// Java
int i = 10;
i = i * 2;
System.out.println(i);
複製代碼

上述使用int的實現到底比使用Integer的實現要快多少呢? 讓咱們來看看。咱們須要在Java中定義兩種方式函數聲明:ide

public class PrimitivesJavaBenchmark {

    public int primitiveCount() {
        int a = 1;
        for (int i = 0; i < 1_000_000; i++) {
            a = a + i * 2;
        }
        return a;
    }

    public Integer objectCount() {
        Integer a = 1;
        for (Integer i = 0; i < 1_000_000; i++) {
            a = a + i * 2;
        }
        return a;
    }
}
複製代碼

當你測試這兩種方法的性能時,您會發現一個巨大的差別。在個人機器中,使用Integer須要4905603ns, 而使用原始類型須要316954ns(這裏是源碼,本身檢查運行測試)這少了15倍!這是一個巨大的差別!函數

怎麼會產生如此之大的差別呢? 原始類型比對象類型更加輕量級。在內存中原始類型的變量僅僅存儲是一個數值而已,它們沒有面向對象那一整套的內存分配過程。當你看到這種差別時,你應該感到慶幸,由於在Kotlin底層實現會盡量使用原始類型,並且這種底層的優化咱們甚至毫無察覺。可是你也應該知道有些狀況底層編譯器是不會轉化成原始類型來作優化處理的:post

  • 可空類型不能是原始類型。編譯器是很智能的,儘管是可空類型,但是當它檢測到你沒有對可空類型變量設置null值時,而後它仍是會使用原始類型處理的。若是編譯不能肯定最終檢測結果,那麼它將默認使用非原始類型。請記住,這是代碼性能關鍵部分因可空性引入的額外成本。
  • 原始類型不能用於泛型類型參數。

第二個問題顯得尤其重要,由於咱們在大部分場景下不多會對代碼中數值作處理,可是咱們常常會對集合中的元素作操做。但是問題來了,泛型類型參數不能使用原始類型,可是每一個泛型集合都只能使用非原始類型了。例如:性能

  • Kotlin中的List<Int>等價於Java中的List<Integer>(注意下: 這個地方有點問題,糾正下原文做者的一個小錯誤,其實是Kotlin中的MutableList<Int>等價於Java中的List<Integer>,可是做者這裏主要想代表在Kotlin中做爲泛型類型參數Int類型狀況下等同於Java中的包裝器類型Integer而不是原始類型int)
  • Kotlin中的Set<Double>等價於Java中的Set<Double>(注意下: 這個地方有點問題,糾正下原文做者的一個小錯誤,其實是Kotlin中的MutableSet<Double>等價於Java中的Set<Double>,可是做者這裏主要想代表在Kotlin中做爲泛型類型參數Double類型狀況下等同於Java中的包裝器類型Double而不是原始類型double)

當咱們須要操做數據集合,這將是一筆很大的性能開銷。可是也是有解決方案的, 由於Java集合容許使用原始類型。

// Java
int[] a = { 1,2,3,4 };
複製代碼

若是在Java中可使用原始類型的數組,那麼在Kotlin也是可使用原始類型的數組的。爲此,咱們須要使用一種特殊的數組類型來表示具備不一樣原始類型的數組: IntArrayLongArrayShortArrayDoubleArrayFloatArray或者CharArray. 讓咱們使用IntArray,看看與List <Int>相比對代碼的性能影響:

open class InlineFilterBenchmark {

    lateinit var list: List<Int>
    lateinit var array: IntArray

    @Setup
    fun init() {
        list = List(1_000_000) { it }
        array = IntArray(1_000_000) { it }
    }

    @Benchmark
    fun averageOnIntList(): Double {
        return list.average()
    }

    @Benchmark
    fun averageOnIntArray(): Double {
        return array.average()
    }
}
複製代碼

儘管差別不是特別大,可是也是差別也是很是明顯的。例如,由於在底層實現上IntArray是使用原始類型的,因此IntArray數組的average()函數會比List<Int>集合運行效率高了約25%左右。(這裏是源碼,本身檢查運行測試)

具備原始類型的數組也會比集合更加輕量級。進行測量時,您會發現IntArray上面分配了400000016個字節,而List<Int>分配了2000006944個字節。大概是5倍的差距。

正如你所看到那樣,使用具備原始類型的變量或者數組都是優化性能關鍵部分一種手段。它們須要分配的內存更少,而且處理的速度更快。儘管原始類型數組在大多數狀況下做了優化,可是默認狀況下可能更可能是使用集合而不是數組。由於集合相比數據更加直觀和更常用。可是你也必須記住原始類型的變量和原始類型數組帶來的性能優化,而且在合適的場景中使用它們。

譯者有話說

這篇Effective Kotlin系列的文章比較簡單,可是也很重要。它指出了咱們常常會忽略的原始類型數組。相信不少人都習慣於使用集合,甚至有的人估計都沒怎麼用過Kotlin中的IntArray、LongArray、FloatArray等,平時不論是什麼場景都使用集合一梭哈。這也很正常,由於集合基本上能夠替代數組出現全部場景,並且集合使用起來更加直觀和方便。可是以前的你可能不知道原來原始類型的數組能夠在某些場景替代集合反而能夠優化性能。因此原始類型的數組是有必定應用場景的,那麼從讀了這篇文章起,請必定要記住這個優化點。關於這篇文章我還想再補充幾點哈:

  • 一、解釋下文章中的原始類型

請注意: 文章中的原始類型(原語類型或基本數據類型)實際上不是Kotlin中的Int、Float、Double、Long等這些類型,原始類型實際上它不對應一個類,就像咱們常在Java中說的String不是原始類型,而是引用類型。實際這裏原始類型就是指Java中的int、double、float、long等非引用類型。爲何說Kotlin中的Int不是原始類型,實際上它更是一種引用類型,一塊兒來看Int的源碼:

public class Int private constructor() : Number(), Comparable<Int> {
    companion object {
        public const val MIN_VALUE: Int = -2147483648
        public const val MAX_VALUE: Int = 2147483647
        @SinceKotlin("1.3")
        public const val SIZE_BYTES: Int = 4
        @SinceKotlin("1.3")
        public const val SIZE_BITS: Int = 32
    }
複製代碼

能夠明顯看出實際上Int是在Kotlin中定義的一個類,它屬於引用類型,不是原始類型。因此咱們平時在Kotlin中是不能直接聲明原始類型的,而所謂原始類型是Kotlin編譯器在底層作的一層內部表達。在Kotlin中聲明Int類型,實際上底層編譯器會根據具體使用狀況,智能推測出是將Int表達爲包裝器Integer仍是原始類型int。若是不信,請看下面這個解釋的源碼論證。

  • 二、解釋下文章中的這句話 "儘管是可空類型,但是當它檢測到你沒有對可空類型變量設置null值時,而後它仍是會使用原始類型處理的,若是設置null就當作非原始類型處理"

把上面那句話說的通俗就是,聲明一個可空類型Int?變量,若是沒有對它作賦值null的操做,那麼編譯器在底層實現會把這個Int?類型使用原始類型int,若是有賦值null操做就會使用包裝器類型Integer.一塊兒來看個例子

//kotlin定義的源碼
fun main(args: Array<String>) {
    var number: Int?
    number = 2
    println(number)
}
//反編譯後的Java代碼
  public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      int number = 2;//能夠明顯看到number變量使用的是int原始類型
      System.out.println(number);
 }
複製代碼

若是把上述例子改成賦值爲null

//kotlin定義的源碼
fun main(args: Array<String>) {
    var number: Int? = null
    number = 2
    println(number)
}
//反編譯後的Java代碼
  public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      Integer number = (Integer)null;//這裏number變量是使用了Integer包裝器類型
      number = 2;
      int var2 = number;
      System.out.println(var2);
   }
複製代碼

經過上述代碼的對比,能夠發現Kotlin編譯器是很是智能的,這也就是解釋了雖然在Kotlin定義的是Int,可是會根據不一樣的使用狀況,最終轉換成結果也不同的,因此使用的時候必定要作到內心有數。

  • 關於使用原始類型數組的建議

其實咱們大多數狀況下仍是使用集合的,由於數組使用具備侷限性。那麼何時使用原始類型數組呢? 元素的類型應該是Int、Float、Double、Long等這些類型,而且長度仍是固定的,這種狀況更多考慮是原始類型數組來替代集合的使用,由於它效率更高。其餘非這種場景仍是建議使用集合。

Kotlin系列文章,歡迎查看:

Effective Kotlin翻譯系列

原創系列:

翻譯系列:

實戰系列:

歡迎關注Kotlin開發者聯盟,這裏有最新Kotlin技術文章,每週會不按期翻譯一篇Kotlin國外技術文章。若是你也喜歡Kotlin,歡迎加入咱們~~~

相關文章
相關標籤/搜索