[译]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,欢迎加入咱们~~~

相关文章
相关标签/搜索