본문 바로가기
프로그래밍 놀이터/Kotlin, Coroutine

[Kotlin] Kotlin 의 숨겨진 비용 #1

by 돼지왕 왕돼지 2018. 1. 16.
반응형

 [Kotlin] Kotlin 의 숨겨진 비용 #1


https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-1-fbb9935d9b62


@jvmfield, accessing private class fields, android studio plugin, annotation, anonymous function, Boxing, boxing overhead, boxing unboxing, bytecode, chain call, CODE, code size, companion object, companion objects, Compatibility, compiled code, const, const keyword, Constants, constants in companion object, function call, function instance, function object, Function1, GC, generated code, generic type, getter, getter chain, global const, hidden cost, higher-order function, inline, inline function, input, instance method coast, Interface, Internal, Invoke, Java, java compatible, java8, jvmfield, Kotlin, kotlin bytecode inspector, LAMBDA, Lambda expression, lambda with primitive type, local variable, local variable cache, Method, object instantiation, outer class, package, package visibility, Parcelable, preformance issue, Primitive type, primitive type const, primitve type, private constant, proguard optimization, Public, public field, public inline function, public visibility accessor, recursive call inline, Return, Setter, Singleton, singleton class, static method cost, string, string const, synthetic getter setter, variable capture, variable capturing, With great power comes great responsibility, [Kotlin] Kotlin 의 숨겨진 비용 #1, 간접 getter, 숨겨진 비용, 재활용, 코드


-

“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 에 모아 놓는 것이 좋다.





반응형

댓글