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

2024-08-27 14:38

本文主要是介绍[译]探索Kotlin中隐藏的性能开销-Part 2,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

翻译说明:

原标题: Exploring Kotlin’s hidden costs — Part 2

原文地址: https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-2-324a4a50b70

原文作者: Christophe Beyls

这是关于探索Kotlin中隐藏的性能开销的第2部分,如果你还没有看到第1部分,不要忘记阅读第1部分。

让我们一起从底层重新探索和发现更多有关Kotlin语法实现细节。

局部函数

这是我们之前第一篇文章中没有介绍过的一种函数: 就是像正常定义普通函数的语法一样,在其他函数体内部声明该函数。这些被称为局部函数,它们能访问到外部函数的作用域。

fun someMath(a: Int): Int {fun sumSquare(b: Int) = (a + b) * (a + b)return sumSquare(1) + sumSquare(2)
}

我们首先来说下局部函数最大的局限性:
局部函数不能被声明成内联的(inline)并且函数体内含有局部函数的函数也不能被声明成内联的(inline). 在这种情况下没有任何有效的方法可以帮助你避免函数调用的开销。

经过编译后,这些局部函数会将被转化成Function对象, 就类似lambda表达式一样,并且同样具有上篇文章part1中讲到的关于非内联函数存在很多的限制。反编译后的java代码:

public static final int someMath(final int a) {Function1 sumSquare$ = new Function1(1) {// $FF: synthetic method// $FF: bridge method//注: 这是Function1接口生成的泛型合成方法invokepublic Object invoke(Object var1) {return Integer.valueOf(this.invoke(((Number)var1).intValue()));}//注: 实例的特定方法invokepublic final int invoke(int b) {return (a + b) * (a + b);}};return sumSquare$.invoke(1) + sumSquare$.invoke(2);
}

但是与lambda表达式相比,它对性能的影响要小得多: 由于该函数的实例对象是从调用方就知道的,所以它将直接调用该实例的特定方法invoke而不是从Function接口直接调用其泛型合成方法invoke。这就意味着从外部函数调用局部函数时,不会进行基本类型的转换或装箱操作. 我们可以通过看下字节码来验证一下:

   ALOAD 1ICONST_1INVOKEVIRTUAL be/myapplication/MyClassKt$someMath$1.invoke    (I)IALOAD 1ICONST_2INVOKEVIRTUAL be/myapplication/MyClassKt$someMath$1.invoke    (I)IIADD //加法操作IRETURN

我们可以看到被调用两次的函数是接收一个 Int 类型的参数并且返回一个 Int 类型的函数,并且加法操作是立即执行的,而无需任何中间的装箱、拆箱操作。

当然,在每次方法被调用期间仍会创建一个新的Function对象。但是这个可以通过将局部函数改写为非捕获的方式来避免这种情况:

fun someMath(a: Int): Int {fun sumSquare(a: Int, b: Int) = (a + b) * (a + b)return sumSquare(a, 1) + sumSquare(a, 2)
}

现在相同的Function实例将会被复用,仍然不会进行强制的转换或装箱操作。与普通的私有函数相比,此局部函数的唯一劣势就是使用一些方法生成额外的类。

局部函数是私有函数的替代品,其附加好处是能够访问外部函数的局部变量。然而这种好处会伴随着为外部函数每次调用创建Function对象的隐性成本,因此首选使用非捕获的局部函数。

空安全

Kotlin语言的最好特性之一就是,它在可空类型和非空类型之间做出了明显清晰的界限区分。这使得编译可以通过在运行时禁止将非null或可为null的值分配给非null变量的任何代码来有效防止意外的NullPointerException.

非空参数的运行时检查

下面我们来声明一个使用非null字符串作为采纳数的公有函数:

fun sayHello(who: String) {println("Hello $who")
}

现在来看下对应的反编译后Java代码:

public static final void sayHello(@NotNull String who) {Intrinsics.checkParameterIsNotNull(who, "who");//执行静态函数进行非空检查String var1 = "Hello " + who;System.out.println(var1);
}

请注意,Kotlin编译器对Java是非常友好的,可以看到在函数参数上自动添加了@NotNull注解,因此Java工具可以使用此注解在传递空值的时候显示警告。

但是,注解不足以强制外部调用者传入非null的值。因此,编译器还在函数的开头添加一个静态方法调用,该方法将检查参数,如果为null,则抛出IllegalArgumentException. 为了使不安全的调用者代码更易于修复,该函数将尽早且持续抛出异常,而不是将它置后抛出运行时的NullPointerException.

实际上,每个公有的函数都有一个对Intrinsics.checkParameterIsNotNull()的静态调用,该调用为每个非null引用参数添加。这些检查不会被添加到私有函数中,因为编译器保证了Kotlin类中的代码为null安全的。

这些静态调用对性能的影响几乎可以忽略不计,并且在调试和测试应用程序的时候非常有帮助。话虽如此,如果对于release版本来说你可能认为这是没必要的额外开销。在这种情况下,可以使用-Xno-param-assertions编译器选项或添加以下Proguard规则来禁止运行时的空检查:

-assumenosideeffects class kotlin.jvm.internal.Intrinsics {static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
}
可空的原生类型

有一点似乎众所周知,但还是在这里提醒下: 可空类型始终是引用类型。将原生类型的变量声明成可空类型可以防止Kotlin使用Java基本数据类型(例如intfloat), 而是使用装箱的引用类型(例如IntegerFloat),这会避免装箱和拆想操作带来的额外开销。

与Java相反的是它允许你草率地使用几乎像int变量的Integer变量,这都要归功于自动装箱和忽略了null的安全性,可是Kotlin则会强制你在使用可null的类型时编写空安全的代码,因次使用非null类型的好处就变得更显而易见了:

fun add(a: Int, b: Int): Int {return a + b
}
fun add(a: Int?, b: Int?): Int {return (a ?: 0) + (b ?: 0)
}

尽可能使用非null的原生类型,以此来提高代码可读性和性能。

关于数组

在Kotlin中存在3种类型的数组:

  • IntArray,FloatArray以及其他原生类型的数组。

    最终会编译成 int[],float[]以及其他对应基本数据类型的数组

  • Array<T>: 非空对象引用类型的数组

    这里会涉及到原生类型的装箱过程

  • Array<T?>: 可空对象引用类型的数组

    很明显,这里也会涉及到原生类型的装箱过程

如果你需要一个非null原生类型的数组,最好使用IntArray而不是Array<Int>以避免装箱过程带来性能开销

可变数量的参数(Varargs)

类似Java, Kotlin允许使用可变数量的参数声明函数。只是声明的语法有点不一样而已:

fun printDouble(vararg values: Int) {values.forEach { println(it * 2) }
}

就像在Java中一样,vararg参数实际上被编译为给定类型的数组参数。然后,可以通过三种不同的方式调用这些函数:

1.传递多个参数
printDouble(1, 2, 3)

Kotlin编译器将将此代码转换为新数组的创建和初始化,就像Java编译器一样:

printDouble(new int[]{1, 2, 3});

所以,创建新数组会产生开销,但是与Java相比,这并不是什么新鲜事。

2.传递单个数组

这里不同之处就是,在Java中,可以直接将现有的数组引用作为vararg参数传递。在Kotlin中,则需要使用伸展(spread)操作符:

val values = intArrayOf(1, 2, 3)
printDouble(*values)

在Java中,数组引用按原样传递给函数,而无需分配额外的数组空间。然而,如你在反编译后java代码中所见,Kotlin伸展(spread)操作符的编译方式有所不同:

int[] values = new int[]{1, 2, 3};
printDouble(Arrays.copyOf(values, values.length));

调用函数时,始终会复制现有数组。好处是代码更安全:它允许函数修改数组而不影响调用者代码。但是它会分配额外的内存

请注意,使用Kotlin代码中可变数量的参数调用Java方法具有相同的效果。

3.传递数组和参数的混合

Kotlin伸展(spread)运算符的主要好处是它还允许在同一调用中将数组与其他参数混合在一起。

val values = intArrayOf(1, 2, 3)
printDouble(0, *values, 42)

上述代码将会怎样编译呢?生成代码会十分有趣:

int[] values = new int[]{1, 2, 3};
IntSpreadBuilder var10000 = new IntSpreadBuilder(3);
var10000.add(0);
var10000.addSpread(values);
var10000.add(42);
printDouble(var10000.toArray());

除了创建新数组之外,还使用一个临时生成器对象来计算最终数组大小并填充它。这给方法调用又增加了另一笔小开销。

即使在使用现有数组中的值时,在Kotlin中调用具有可变数量参数的函数也会增加创建新临时数组的成本。对于重复调用该函数的性能至关重要的代码,请考虑添加具有实际数组参数而不是vararg的方法

感谢您的阅读,如果喜欢,请分享这篇文章。

继续阅读第3部分:委托的属性范围

读者有话说

大概隔了很久很久之前,我好像写了一篇探索Kotlin中隐藏的性能开销系列的Part1. 如果没有读过第1篇建议也去读下第1篇,因为这个系列确实对你写出高效的Kotlin代码十分有帮助,也能帮助你从源码,编译层面认清Kotlin语法背后的原理。我更喜欢把这些写Kotlin代码技巧称为Effective Kotlin, 这也是我最初翻译这个系列文章的初衷。关于这篇文章,有几点我需要补充下:

1、为什么非捕获局部函数可以减少开销

其实关于捕获和非捕获的概念,在之前文章中也有所提及,比如在讲变量的捕获,lambda的捕获和非捕获。

这里就以上述局部函数举例,下面对比下这两个函数:

//改写前的捕获局部函数
fun someMath(a: Int): Int {fun sumSquare(b: Int) = (a + b) * (a + b)//注意:局部函数这里的a是直接引用外部函数的参数a, //因为局部函数特性可以访问外部函数的作用域,这里实际上就存在了变量的捕获,所以这里sumSquare称为捕获局部函数return sumSquare(1) + sumSquare(2)
}
//改写前反编译后代码public static final int someMath(final int a) {//创建Function1对象$fun$sumSquare$1,所以每调用一次someMath都会创建一个Function1对象<undefinedtype> $fun$sumSquare$1 = new Function1() {// $FF: synthetic method// $FF: bridge methodpublic Object invoke(Object var1) {return this.invoke(((Number)var1).intValue());}public final int invoke(int b) {return (a + b) * (a + b);}};return $fun$sumSquare$1.invoke(1) + $fun$sumSquare$1.invoke(2);}

捕获局部函数会生成额外的Function对象,所以我们为了减少性能的开销尽量使用非捕获局部函数。

//改写后的非捕获局部函数
fun someMath(a: Int): Int {//注意: 可以明显发现改写后a参数,直接由函数参数传入,而不是在局部函数直接引用外部函数的参数变量,这就是非捕获局部函数fun sumSquare(a: Int, b: Int) = (a + b) * (a + b)return sumSquare(a,1) + sumSquare(a,2)
}//改写后反编译后代码
public static final int someMath(int a) {//注意:可以看到非捕获的局部函数实例是一个单例,多次调用都只会复用之前的实例不会重新创建。<undefinedtype> $fun$sumSquare$1 = null.INSTANCE;return $fun$sumSquare$1.invoke(a, 1) $fun$sumSquare$1.invoke(a, 2);
}

通过上述对比,应该很清楚知道了什么是捕获什么是非捕获以及为什么非捕获局部函数会减少性能的开销。

2、总结下提高Kotlin代码性能开销几个点
  • 局部函数是私有函数的替代品,其附加好处是能够访问外部函数的局部变量。然而这种好处会伴随着为外部函数每次调用创建Function对象的隐性成本,因此首选使用非捕获的局部函数。
  • 对于release版本应用来说,特别是Android应用,可以使用-Xno-param-assertions编译器选项或添加以下Proguard规则来禁止运行时的空检查:
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
}
  • 需要使用非null原生类型的数组时,最好使用IntArray而不是Array<Int>以避免装箱过程带来性能开销

最后

首先想和一直关注我公众号和技术博客的老铁们说声抱歉,因为中间已经很久没更新技术文章,因此有很多人也离开了,但也有人一直默默支持。所以从今天起我又准备开始更新了文章。时间每周一更新1篇Kotlin技术相关文章,研究dart和flutter也有一段时间了,沉淀了一些技术心得,所以周二至周五时间会不定期更新有关Dart和Flutter的文章,感谢关注,感谢理解。

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

Kotlin系列文章,欢迎查看:

Kotlin邂逅设计模式系列:

  • 当Kotlin完美邂逅设计模式之单例模式(一)

数据结构与算法系列:

  • 每周一算法之二分查找(Kotlin描述)
  • 每周一数据结构之链表(Kotlin描述)

翻译系列:

  • [译] [译]探索Kotlin中隐藏的性能开销-Part 1
  • [译] Kotlin中关于Companion Object的那些事
  • [译]记一次Kotlin官方文档翻译的PR(内联类)
  • [译]Kotlin中内联类的自动装箱和高性能探索(二)
  • [译]Kotlin中内联类(inline class)完全解析(一)
  • [译]Kotlin的独门秘籍Reified实化类型参数(上篇)
  • [译]Kotlin泛型中何时该用类型形参约束?
  • [译] 一个简单方式教你记住Kotlin的形参和实参
  • [译]Kotlin中是应该定义函数还是定义属性?
  • [译]如何在你的Kotlin代码中移除所有的!!(非空断言)
  • [译]掌握Kotlin中的标准库函数: run、with、let、also和apply
  • [译]有关Kotlin类型别名(typealias)你需要知道的一切
  • [译]Kotlin中是应该使用序列(Sequences)还是集合(Lists)?
  • [译]Kotlin中的龟(List)兔(Sequence)赛跑

原创系列:

  • 教你如何完全解析Kotlin中的注解
  • 教你如何完全解析Kotlin中的类型系统
  • 如何让你的回调更具Kotlin风味
  • Jetbrains开发者日见闻(三)之Kotlin1.3新特性(inline class篇)
  • JetBrains开发者日见闻(二)之Kotlin1.3的新特性(Contract契约与协程篇)
  • JetBrains开发者日见闻(一)之Kotlin/Native 尝鲜篇
  • 教你如何攻克Kotlin中泛型型变的难点(实践篇)
  • 教你如何攻克Kotlin中泛型型变的难点(下篇)
  • 教你如何攻克Kotlin中泛型型变的难点(上篇)
  • Kotlin的独门秘籍Reified实化类型参数(下篇)
  • 有关Kotlin属性代理你需要知道的一切
  • 浅谈Kotlin中的Sequences源码解析
  • 浅谈Kotlin中集合和函数式API完全解析-上篇
  • 浅谈Kotlin语法篇之lambda编译成字节码过程完全解析
  • 浅谈Kotlin语法篇之Lambda表达式完全解析
  • 浅谈Kotlin语法篇之扩展函数
  • 浅谈Kotlin语法篇之顶层函数、中缀调用、解构声明
  • 浅谈Kotlin语法篇之如何让函数更好地调用
  • 浅谈Kotlin语法篇之变量和常量
  • 浅谈Kotlin语法篇之基础语法

Effective Kotlin翻译系列

  • [译]Effective Kotlin系列之考虑使用原始类型的数组优化性能(五)
  • [译]Effective Kotlin系列之使用Sequence来优化集合的操作(四)
  • [译]Effective Kotlin系列之探索高阶函数中inline修饰符(三)
  • [译]Effective Kotlin系列之遇到多个构造器参数要考虑使用构建器(二)
  • [译]Effective Kotlin系列之考虑使用静态工厂方法替代构造器(一)

实战系列:

  • 用Kotlin撸一个图片压缩插件ImageSlimming-导学篇(一)
  • 用Kotlin撸一个图片压缩插件-插件基础篇(二)
  • 用Kotlin撸一个图片压缩插件-实战篇(三)
  • 浅谈Kotlin实战篇之自定义View图片圆角简单应用

这篇关于[译]探索Kotlin中隐藏的性能开销-Part 2的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1111959

相关文章

Apache Tomcat服务器版本号隐藏的几种方法

《ApacheTomcat服务器版本号隐藏的几种方法》本文主要介绍了ApacheTomcat服务器版本号隐藏的几种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需... 目录1. 隐藏HTTP响应头中的Server信息编辑 server.XML 文件2. 修China编程改错误

正则表达式高级应用与性能优化记录

《正则表达式高级应用与性能优化记录》本文介绍了正则表达式的高级应用和性能优化技巧,包括文本拆分、合并、XML/HTML解析、数据分析、以及性能优化方法,通过这些技巧,可以更高效地利用正则表达式进行复杂... 目录第6章:正则表达式的高级应用6.1 模式匹配与文本处理6.1.1 文本拆分6.1.2 文本合并6

Vue3 的 shallowRef 和 shallowReactive:优化性能

大家对 Vue3 的 ref 和 reactive 都很熟悉,那么对 shallowRef 和 shallowReactive 是否了解呢? 在编程和数据结构中,“shallow”(浅层)通常指对数据结构的最外层进行操作,而不递归地处理其内部或嵌套的数据。这种处理方式关注的是数据结构的第一层属性或元素,而忽略更深层次的嵌套内容。 1. 浅层与深层的对比 1.1 浅层(Shallow) 定义

性能测试介绍

性能测试是一种测试方法,旨在评估系统、应用程序或组件在现实场景中的性能表现和可靠性。它通常用于衡量系统在不同负载条件下的响应时间、吞吐量、资源利用率、稳定性和可扩展性等关键指标。 为什么要进行性能测试 通过性能测试,可以确定系统是否能够满足预期的性能要求,找出性能瓶颈和潜在的问题,并进行优化和调整。 发现性能瓶颈:性能测试可以帮助发现系统的性能瓶颈,即系统在高负载或高并发情况下可能出现的问题

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

黑神话,XSKY 星飞全闪单卷性能突破310万

当下,云计算仍然是企业主要的基础架构,随着关键业务的逐步虚拟化和云化,对于块存储的性能要求也日益提高。企业对于低延迟、高稳定性的存储解决方案的需求日益迫切。为了满足这些日益增长的 IO 密集型应用场景,众多云服务提供商正在不断推陈出新,推出具有更低时延和更高 IOPS 性能的云硬盘产品。 8 月 22 日 2024 DTCC 大会上(第十五届中国数据库技术大会),XSKY星辰天合正式公布了基于星

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动

AI(文生语音)-TTS 技术线路探索学习:从拼接式参数化方法到Tacotron端到端输出

AI(文生语音)-TTS 技术线路探索学习:从拼接式参数化方法到Tacotron端到端输出 在数字化时代,文本到语音(Text-to-Speech, TTS)技术已成为人机交互的关键桥梁,无论是为视障人士提供辅助阅读,还是为智能助手注入声音的灵魂,TTS 技术都扮演着至关重要的角色。从最初的拼接式方法到参数化技术,再到现今的深度学习解决方案,TTS 技术经历了一段长足的进步。这篇文章将带您穿越时

PR曲线——一个更敏感的性能评估工具

在不均衡数据集的情况下,精确率-召回率(Precision-Recall, PR)曲线是一种非常有用的工具,因为它提供了比传统的ROC曲线更准确的性能评估。以下是PR曲线在不均衡数据情况下的一些作用: 关注少数类:在不均衡数据集中,少数类的样本数量远少于多数类。PR曲线通过关注少数类(通常是正类)的性能来弥补这一点,因为它直接评估模型在识别正类方面的能力。 精确率与召回率的平衡:精确率(Pr