[Kotlin] Kotlin 의 숨겨진 비용 #1
https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-1-fbb9935d9b62
-
“With great power comes great responsibility” 를 기억해야 한다.
( 간단한 코드를 짜는 대신 대가가 있다는 얘기다 )
-
Kotlin bytecode inspector 를 사용하면 Kotlin 코드가 어떻게 bytecode 로 변환되는지 볼 수 있다.
Android studio plugin 으로 접할 수 있다.
이를 보면 primitive type 의 boxing, code 에서 보이지 않는 기타 object 들의 instantiation, 그리고 각종 추가 method 들의 추가 등을 눈으로 볼 수 있다.
Higher-order functions and Lambda expressions
-
Function object
lambda 와 anonymous function 은 “Function” object 로 치환된다. ( java 와 compatible 하기 위해 )
val deletedRows = transaction(db) {
it.delete(“Customers”, null, null)
}
/* Generated Code */
int deletedrows = this.transaction(db, (Function1)MyClass$myMethod$1.INSTANCE);
class MyClass$myMethod$1 implements Function1{
public Object invoke(Object var1) {
return Integer.valueOf(this.invoke((Database)var1));
}
public final int invoke(@NotNull Database it){
Intrinsics.checkParameterIsNotNull(it, “it”);
return db.delete(“Customers”, null, null)
}
}
즉 lambda 나 anonymous function 을 사용하면 기본적으로 3, 4 개의 method 추가가 생긴다.( inline 이 아닌 경우 )
variable capturing 의 경우는 Function instance 를 만들고 이는 gc 대상이다.
다행히도(?) variable capturing 이 없는 경우에는 singleton 으로 만들어 놓고 재활용한다.
결론적으로 variable capture 를 하는 lambda 를 사용하는 경우 주의를 해야 한다.
+ inline 이 아닌 Lambda 는 method 증가 cost 가 있다는 점도 알아두어야 한다.
-
Boxing overhead
Java8 은 boxing, unboxing 을 막기 위해 43 개의 특별 interface 를 정의하여 제공한다.
하지만 Kotlin 은 Function object 를 사용하며 generic type 으로만 제공한다.
// Function 뒤의 1 은 argument 를 1개만 받는다는 의미이다.
public interface Function1<in P1, out R> : Function<R> {
public operator fun invoke(p1: P1): R
}
lambda 에서 primitive type 을 input 이나 return 으로 사용할 때는 boxing, unboxing 에 대한 cost 는 항상 생각해야 한다.
-
Inline functions to the rescue
inline 을 사용하면 많은 부분이 해결된다.
Function object 가 생성되지 않으며, boxing과 unboxing 도 발생하지 않는다.
method adding 도 없고, function call 이 되는 형태가 아니라 performance issue 도 줄어든다.
물론 inline 도 단점은 있다.
inline function 은 자신을 recursive 하게 부를 수 없고, 다른 inline function 을 통해 불릴 수도 없다.
public inline function 은 public visibility accessor 를 가진 녀석들만 접근 할 수 있다.
compile 된 code 크기도 늘어난다.
inline 이 많은 것을 해결해주긴 하지만, inline function 으로 사용되는 녀석들은 안의 내용이 간결한 것이 좋다.
Companion objects
-
Accessing private class fields from its companion object
companion object 는 compile 되면 singleton class 가 된다.
그리고 companion object 에서 접근하는 outer class 의 녀석들은 추가적인 getter, setter 가 생성된다.
Java 에서는 package visibility 를 통해 이를 피하는데, Kotlin 은 package 도 없어 public 이나 internal 로 만들어야 하는데, 그렇게 하면 getter 와 setter 가 공식적으로 생성된다.
static method 보다 instance method 호출이 더 costly 하다는 것도 알아두면 좋다.
즉, companion object 에서 outer class 의 변수 접근하는 것이 잦다면, local variable 에 cache 해서 쓰는 것이 추천된다. ( 물론 그래도 괜찮다면 )
-
Accessing constants declared in a companion object
companion object 에 정의된 private constant ( const 로 정의하지 않은 단순 const val ) 를 접근하는 것은 synthetic getter 를 생성한다.
게다가 접근할 때, 생성된 getter 를 바로 호출하는 것이 아니라 일반 getter 를 한 번 더 통한다.
더 심각한 것은 companion object 안의 const 가 compile 이 되면 outer class 의 static final const 가 된다는 것이다.
그래서 companion object 안에서 자신의 property 를 접근하는데 private 정의 되어 있다고 해도 outer class 의 synthetic getter 를 통해서 접근을 한다.
( 위의 내용들은 말로 풀어쓰기가 너무 어렵다. 한번 읽어보고, 아래 코드들 보고 다시 읽어보면 이해가 될 것이다. )
정리자면.. companion object 안에 정의된 private constant 는..
companion object 의 static method 를 호출하게 되고, 그것은 companion object 의 instance method 를 호출하게 되고, 그것은 다시 class 의 static method 를 호출하고, 그것이 static field 를 읽어서 return 하게 된다.
class MyClass{
companion object {
private val TAG = “TAG" // const 로 정의하면 괜찮다
}
fun helloWorld() {
println(TAG)
}
}
/* Generated Code */
public final class MyClass{
private static final String TAG = “TAG”;
public static final Companion companion = new Companion();
// synthetic
public static final String access$getTAG$cp(){
return TAG;
}
public static final class Companion{
private final String getTAG(){
return MyClass.access.getTG$cp();
}
// synthetic
public static final String access$getTAG$p(Companion c){
return c.getTAG();
}
public final void helloWorld(){
System.out.println(Companion.access$getTAG$p(companion));
}
}
}
-
이는 companion object 안의 const 를 const keyword 를 넣어주며 정의함으로써 예방할 수 있다.
const 가 붙은 것들은 inlining 을 잘 해준다.
그러나 단점 중 하나! const 는 primitive type 과 string 만 정의할 수 있다.
-
또 다른 해결법은 public field 에 @JvmField annotation 을 달아주는 것이다.
@JvmField 는 Java 에서 Java 문법으로 접근하게 하는 것이기 때문에 getter 와 setter 를 만들지 않고 field 를 바로 노출하게 된다.
사실 이 녀석은 Java 와의 compatibility 만을 위해 만들어진 녀석이기 때문에 이런 용도로 쓰는게 좋은지는 생각해봐야 한다. (Java 에서 이렇게 접근하는 것을 허용하면서 getter 를 없애는 2가지 효과가 다 있다는 것을 명심해야 한다.) 게다가 public field 에만 사용할 수 있다는 것도 주의해야 한다.
-
글 필자는 Android 기준 Parcelable object 구현 이외에는 실제로는 @JvmField 를 쓰는 일은 거의 없을거라고( 혹은 없어야 한다고 ) 한다.
class MyClass() : Parcelable{
companion object {
@JvmField
val CREATOR = creator { MyClass(it) }
}
private constructor(parcel: Parcel) : this()
override fun writeToParcel(dest:Parcel, flags: Int) { }
override fun describeContents() = 0
}
]
-
물론 proguard optimization 이나 bytecode 치환 과정에서 약간의 optimization 이 될 수는 있지만..
우선 surfacial 한 문제는 저렇다.
-
정리하면 companion object 에 정의한 static constant 를 읽을 때는 2~3개의 간접적인 getter 들이 생겨난다.
그래서 primitive type 이나 String 의 경우 무조건 const 를 넣어주는 것이 좋다.
그리고 다른 constant 의 경우는 getter chain call 을 피하기 위해서 local variable 로 cache 해놓고 쓰는 것이 추천된다.
그리고 public global constants 는 companion object 대신 global const 에 모아 놓는 것이 좋다.
다음 글 : [Kotlin] Kotlin 의 숨겨진 비용 #2
'프로그래밍 놀이터 > Kotlin, Coroutine' 카테고리의 다른 글
[Kotlin] Kotlin 의 숨겨진 비용 #3 (0) | 2018.01.18 |
---|---|
[Kotlin] Kotlin 의 숨겨진 비용 #2 (0) | 2018.01.17 |
[Kotlin] 장점, 단점, 그리고 아쉬운 점 이야기 (0) | 2018.01.15 |
[Kotlin] Kotlin 은 Compile time 이 느리다는데.. 사실일까? (0) | 2017.09.26 |
[Kotlin Tutorial] The Kotlin ecosystem (0) | 2017.09.22 |
댓글