内容简介:原标题: Effective Kotlin: Consider Arrays with primitives for performance critical processing原文地址:原文作者:Marcin Moskala
翻译说明:
原标题: Effective Kotlin: Consider Arrays with primitives for performance critical processing
原文地址: blog.kotlin-academy.com/effective-k…
原文作者:Marcin Moskala
Kotlin底层实现是非常智能的。在Kotlin中我们不能直接声明原始类型(也称原语类型)的,但是当我们不像使用对象实例那样操作一个变量时,那么这个变量在底层将转换成原始类型处理。例如,请看以下示例:
var i = 10 i = i * 2 println(i) 复制代码
上述的变量声明在Kotlin底层是使用了原始类型 int .下面这是上述例子在 Java 中的内部表达:
// Java int i = 10; i = i * 2; System.out.println(i); 复制代码
上述使用 int 的实现到底比使用 Integer 的实现要快多少呢? 让我们来看看。我们需要在Java中定义两种方式函数声明:
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底层实现会尽可能使用原始类型,而且这种底层的优化我们甚至毫无察觉。但是你也应该知道有些情况底层编译器是不会转化成原始类型来做优化处理的:
- 可空类型不能是原始类型。编译器是很智能的,尽管是可空类型,可是当它检测到你没有对可空类型变量设置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也是可以使用原始类型的数组的。为此,我们需要使用一种特殊的数组类型来表示具有不同原始类型的数组: IntArray 、 LongArray 、 ShortArray 、 DoubleArray 、 FloatArray 或者 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 等,平时不管是什么场景都使用集合一梭哈。这也很正常,因为集合基本上可以替代数组出现所有场景,而且集合使用起来更加直观和方便。但是之前的你可能不知道原来原始类型的数组可以在某些场景替代集合反而可以优化性能。所以原始类型的数组是有一定应用场景的,那么从读了这篇文章起,请一定要记住这个优化点。关于这篇文章我还想再补充几点哈:
- 1、解释下文章中的原始类型
请注意:文章中的原始类型(原语类型或基本数据类型)实际上不是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 。如果不信,请看下面这个解释的源码论证。
- 2、解释下文章中的这句话 "尽管是可空类型,可是当它检测到你没有对可空类型变量设置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 等这些类型,并且长度还是固定的,这种情况更多考虑是原始类型数组来替代集合的使用,因为它效率更高。其他非这种场景还是建议使用集合。
以上所述就是小编给大家介绍的《[译]Effective Kotlin系列之考虑使用原始类型的数组优化性能(五)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Open Data Structures
Pat Morin / AU Press / 2013-6 / USD 29.66
Offered as an introduction to the field of data structures and algorithms, Open Data Structures covers the implementation and analysis of data structures for sequences (lists), queues, priority queues......一起来看看 《Open Data Structures》 这本书的介绍吧!