[译]探索Kotlin中隐藏的性能开销-Part 3

[译]探索Kotlin中隐藏的性能开销-Part 3

翻译说明:html

原标题# Exploring Kotlin’s hidden costs — Part 3java

原文地址: medium.com/@BladeCoder…算法

原文做者: Christophe Beyls编程

代理属性和Range

在发布有关Kotlin编程语言的性能开销系列的前两篇文章以后,我收到了不少不错的反馈,甚至还包括 Jake Wharton 大神他本身。因此你还没看前两篇文章,千万不要错过哦。设计模式

在第3部分中,咱们将揭开更多有关Kotlin编译器的秘密,并提供如何编写更高效代码的新技巧。api

1、代理属性

代理属性是一种其getter和可选的setter的内部实现可由代理的外部对象提供的属性。它能够容许复用自定义属性的内部实现。数组

class Example {
    var p: String by Delegate()
}
复制代码

这个代理对象必须实现一个 operator getVlue()函数,以及一个 setValue()函数来用于属性的读/写. 这些函数将接收包含对象实例 以及属性的metadata元数据 做为额外参数(好比它的属性名)。缓存

当类中声明一个代理属性时,编译将生成如下代码(下面是反编译后的Java代码):安全

public final class Example {
   @NotNull
   private final Delegate p$delegate = new Delegate();
   // $FF: synthetic field
   static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Example.class), "p", "getP()Ljava/lang/String;"))};

   @NotNull
   public final String getP() {
      return this.p$delegate.getValue(this, $$delegatedProperties[0]);
   }

   public final void setP(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.p$delegate.setValue(this, $$delegatedProperties[0], var1);
   }
}
复制代码

一些静态属性metadata元数据被添加到类中。代理将在类的构造器中进行初始化,而后在每次读取或写入属性时都调用该代理。数据结构

代理实例

在上述例子中,将会建立一个新的代理对象的实例来实现该属性。当代理实例是有状态的时候, 这就是必需的,例如在计算本地缓存属性的值时.

class StringDelegate {
    private var cache: String? = null

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        var result = cache
        if (result == null) {
            result = someOperation()
            cache = result
        }
        return result
    }
}
复制代码

若是还须要经过其构造函数传递的额外参数,则还须要建立一个新的代理实例:

class Example {
    private val nameView by BindViewDelegate<TextView>(R.id.name)
}
复制代码

可是在某些状况下,只须要一个代理实例就能够实现任意属性: 当代理实例是无状态的时候,而且它执行所需的惟一变量就是对象实例和属性名称(然而这些编译器都直接提供了)。在这种状况下,能够经过将代理实例声明成object对象表达式而不是一个来使得成为单例

例如,下面的代理单例实例检索其标记名称与Android Activity 中的属性名称来匹配Fragment.

object FragmentDelegate {
    operator fun getValue(thisRef: Activity, property: KProperty<*>): Fragment? {
        return thisRef.fragmentManager.findFragmentByTag(property.name)
    }
}
复制代码

一样,任意的对象均可以扩展成代理。此外getValue()setValue()还能够声明成扩展函数。Kotlin中已经提供了内置的扩展函数,例如容许将MapMutableMap实例做为代理实例,并将属性的名称做为key.

若是你选择在同一个类中实现多个属性复用同一个局部代理实例的话,那么须要在类的构造器中初始化此实例。

注意: 从Kotlin1.1开始,也能够在函数中声明局部变量做为代理属性。那么在这种状况下,代理实例能够延迟初始化,直到在函数中声明变量为止。

在类中声明的每一个代理属性都涉及到其关联的代理对象建立的性能开销,并向该类中添加一些metadata元数据。必要的时候,能够尝试为不一样属性复用同一个代理实例。在你声明大量代理属性的时候,还须要考虑代理属性是否你的最佳选择。

泛型代理

还能够以泛型的方式声明代理函数,所以同一个代理类能够用任意的属性类型。

private var maxDelay: Long by SharedPreferencesDelegate<Long>()
复制代码

可是,若是像上面例子那样使用具备原生类型属性的泛型代理的话,即使声明的原生类型为非null,每次读取或写入该属性时都避免不了装箱和拆箱的发生

对于非null原生类型的代理属性,最好使用为该特定值类型建立特定的代理类,而不是泛型代理,以免在每次访问该属性时产生的装箱开销

标准库代理: lazy()

Kotlin内置了一些标准库代理函数来覆盖常见的状况,例如 Delegates.notNull(),Delegates.observable()lazy().

lazy(initializer: () -> T) 是一个为只读属性返回代理对象的函数,该属性是经过在其首次被读取的时,lazy函数参数lambda initializer执行来初始化的。

private val dateFormat: DateFormat by lazy {
    SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
}
复制代码

这是一种将昂贵的初始化操做延迟到实际须要使用以前的巧妙方法,能够在保持代码可读性的同时又提升了性能。

须要注意到的是,lazy()函数不是内联函数,而且做为参数传递的lambda将编译成独立的Function类,而且不会在返回的代理对象内进行内联。

一般会被人忽略的是lazy()另外一重载函数实际上还隐藏一个可选的模式参数来肯定应该返回3种不一样类型的代理中的一种:

public fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
public fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
        when (mode) {
            LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
            LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
            LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
        }
复制代码

默认的模式是 LazyThreadSafetyMode.SYNCHRONIZED 将执行相对开销昂贵的双重锁的检查,这是为了保证在多线程环境下读取属性时,初始化块能够安全运行。

若是你明确知道当前环境是单线程(例如主线程)访问属性,那么能够经过显式使用 LazyThreadSafetyMode.NONE 来彻底避免双重锁的检查所带来昂贵的开销。

val dateFormat: DateFormat by lazy(LazyThreadSafetyMode.NONE) {
    SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
}
复制代码

使用lazy()代理能够按需延迟昂贵的初始化,此外能够指定线程安全的模式以免没必要要的双重锁检查。

2、Ranges(区间)

区间是一种用于表示Kotlin中的一组有限值的特殊表达式。这些值能够是任意Comparable类型。这些表达式由建立用于实现ClosedRange对象的函数造成。用于建立区间的主要函数是 ..操做符。

区间包含的测试

区间表达式主要目的是使用in!in 运算符来判断是否包含某个值

if (i in 1..10) {
    println(i)
}
复制代码

该实现特意针对非null原生类型区间(有: Int, Long, Byte, Short, Float, Double或Char)进行了优化,所以上面例子能够高效编译成以下形式:

if(1 <= i && i <= 10) {
   System.out.println(i);
}
复制代码

性能开销几乎为0,没有额外的对象分配。区间也能够和任意其余非原生Comparable类型一块儿使用。

if (name in "Alfred".."Alicia") {
    println(name)
}
复制代码

在Kotlin 1.1.50以前,编译以上示例时始终会建立一个临时的ClosedRange对象。可是从1.1.50以后,已经对它的实现进行了优化,以免Comparable类型额外开销分配:

if(name.compareTo("Alfred") >= 0) {
   if(name.compareTo("Alicia") <= 0) {
      System.out.println(name);
   }
}
复制代码

此外,区间检查还包括应用再 when 表达式中

val message = when (statusCode) {
    in 200..299 -> "OK"
    in 300..399 -> "Find it somewhere else"
    else -> "Oops"
}
复制代码

这使代码比一系列if {...} else if {...}语句更具可读性,而且效率更高。

可是,在区间包含检查中,当区间的声明之间至少存在一个间接过程时,会有一个小的性能开销。 好比下面这段Kotlin代码:

private val myRange get() = 1..10

fun rangeTest(i: Int) {
    if (i in myRange) {
        println(i)
    }
}
复制代码

上述代码会形成在编译后额外建立一个IntRange对象:

private final IntRange getMyRange() {
   return new IntRange(1, 10);
}

public final void rangeTest(int i) {
   if(this.getMyRange().contains(i)) {
      System.out.println(i);
   }
}
复制代码

即便将属性getter声明成内联函数也不能避免建立IntRange对象。在这种状况下,Kotlin 1.1编译器已经改进了。 因为这些特定的区间类存在,至少在比较原生类型时不会出现装箱过程。

尝试在没有间接声明过程区间检查中使用直接声明区间的方式,来避免额外区间对象的建立分配,另外,能够将它们声明成常量以此来复用他们。

迭代: for循环

整数类型区间(除Float或Double以外的任何原生类型的区间)也是级数: 能够对其进行迭代。这容许用较短的语法替换经典的Java for循环。

for (i in 1..10) {
    println(i)
}
复制代码

这能够以零开销方式编译为可比较的优化代码:

int i = 1;
for(byte var2 = 11; i < var2; ++i) {
   System.out.println(i);
}
复制代码

若是向后迭代,请使用 downTo() 中缀函数来替代

for (i in 10 downTo 1) {
    println(i)
}
复制代码

一样,使用此构造进行编译后的开销为零:

int i = 10;
byte var1 = 1;
while(true) {
   System.out.println(i);
   if(i == var1) {
      return;
   }
   --i;
}
复制代码

还有一个有用的until()中缀函数能够迭代直到但不包括区间上限值。

for (i in 0 until size) {
    println(i)
}
复制代码

当本文的原始版本发布时,调用此函数用于生成次优代码。自Kotlin 1.1.4起,状况已大大改善,而且编译器如今生成等效的Java for循环:

int i = 0;
for(int var2 = size; i < var2; ++i) {
   System.out.println(i);
}
复制代码

可是,其余迭代变体的优化效果也不佳

这是另外一种使用reversed() 函数与区间组合的方法,能够向后迭代并产生与downTo()彻底相同的结果。

for (i in (1..10).reversed()) {
    println(i)
}
复制代码

不幸的是,生成的编译代码就不那么漂亮:

IntProgression var10000 = RangesKt.reversed((IntProgression)(new IntRange(1, 10)));
int i = var10000.getFirst();
int var3 = var10000.getLast();
int var4 = var10000.getStep();
if(var4 > 0) {
   if(i > var3) {
      return;
   }
} else if(i < var3) {
   return;
}

while(true) {
   System.out.println(i);
   if(i == var3) {
      return;
   }

   i += var4;
}
复制代码

将会建立一个临时的IntRange对象来表示区间,而后再建立另外一个IntProgression对象来反转第一个对象的值。

事实上,建立一个progression的以上功能任何组合都会生成相似的代码,涉及到建立至少两个轻量级progression对象的小开销。

此规则也适用于使用step()中缀函数来修改progression, 即便步长是1:

for (i in 1..10 step 2) {
    println(i)
}
复制代码

附带说明下,当生成的代码读取IntProgression的最后一个属性时,这将执行少许计算,以经过考虑边界和步长来肯定区间的确切最后一个值。在上面的示例中,最后一个值应该为9。

若要在for循环中进行迭代,最好使用区间表达式,该区间表达式只涉及到对 ..downTo()untill()的单个函数调用,以免建立临时progression对象的开销。

迭代: for-each()

与其使用for循环,不如尝试在区间上使用forEach()内联扩展函数来达到相同的结果。

(1..10).forEach {
    println(it)
}
复制代码

可是,若是您仔细查看此处使用的forEach()函数的签名,你会注意到,它并无针对区间进行优化,而只是针对Iterable进行了优化,所以须要建立一个迭代器。这是反编译后的Java代码表示形式:

Iterable $receiver$iv = (Iterable)(new IntRange(1, 10));
Iterator var1 = $receiver$iv.iterator();

while(var1.hasNext()) {
   int element$iv = ((IntIterator)var1).nextInt();
   System.out.println(element$iv);
}
复制代码

该代码甚至比之前的示例效率更低,由于除了建立IntRange对象外, 你还必须还有建立一个IntIterator的开销。至少,这个会生成原生类型的值。

要对范围进行迭代,最好使用简单的for循环,而不是在其上调用forEach()函数,以免迭代器对象的开销。

迭代: collection indices

Kotlin标准库提供了内置索引扩展属性,以生成数组索引和Collection索引的区间。

val list = listOf("A", "B", "C")
for (i in list.indices) {
    println(list[i])
}
复制代码

使人惊讶的是,遍历 indices 的代码也被编译为优化的代码

List list = CollectionsKt.listOf(new String[]{"A", "B", "C"});
int i = 0;
for(int var2 = ((Collection)list).size(); i < var2; ++i) {
   Object var3 = list.get(i);
   System.out.println(var3);
}
复制代码

在这里,咱们能够看到根本没有建立IntRange对象,而且列表迭代尽量高效。

这对于实现Collection的数组和类很是有效, 所以你可能会在本身定义类中定义本身的indices扩展,同时指望能达到相同的迭代性能.

inline val SparseArray<*>.indices: IntRange
    get() = 0 until size()

fun printValues(map: SparseArray<String>) {
    for (i in map.indices) {
        println(map.valueAt(i))
    }
}
复制代码

可是,在编译以后,咱们能够看到效率不高,由于编译器没法智能地避免建立区间对象:

public static final void printValues(@NotNull SparseArray map) {
   Intrinsics.checkParameterIsNotNull(map, "map");
   IntRange var10000 = RangesKt.until(0, map.size());
   int i = var10000.getFirst();
   int var2 = var10000.getLast();
   if(i <= var2) {
      while(true) {
         Object $receiver$iv = map.valueAt(i);
         System.out.println($receiver$iv);
         if(i == var2) {
            break;
         }
         ++i;
      }
   }
}
复制代码

相反,我建议直接在for循环中使用until()函数

fun printValues(map: SparseArray<String>) {
    for (i in 0 until map.size()) {
        println(map.valueAt(i))
    }
}
复制代码

当遍历未实现Collection接口的自定义集合时,最好直接在for循环中编写本身的索引范围,而不是依靠函数或属性来生成区间,以免分配区间对象。

我但愿这些对你的阅读和对个人写做同样有趣。你可能会在之后看到更多相关内容,可是前三部分涵盖了我计划最初编写的全部内容。若是你喜欢,请分享给他人,谢谢!

总结

到这里,有关探索Kotlin性能开销的系列文章终于暂时告于完结,说下本身切身感觉,翻译这个系列对我平时在用Kotlin开发时有了很大的帮助,能够写出更加高效优秀的代码。因此我以为有必要把它翻译出来和你们共享。下一站,咱们将进入Kotlin协程~~~

欢迎关注Kotlin开发者联盟,这里有最新Kotlin技术文章,每周会不按期翻译一篇Kotlin国外技术文章。若是你也喜欢Kotlin,欢迎加入咱们~~~

Kotlin系列文章,欢迎查看:

Kotlin邂逅设计模式系列:

数据结构与算法系列:

翻译系列:

原创系列:

Effective Kotlin翻译系列

实战系列:

相关文章
相关标签/搜索