[Kotlin] Kotlin 의 숨겨진 비용 #3
https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-3-3bf6e0dbf0a4
Delegated property
-
class Example{
var p: String by Delegate()
}
property 에 delegate 를 사용할 경우에 해당 delegate 는 operator function 인 getValue 와 setValue 를 구현해야 한다.
해당 function 들은 object instance 와 property metadata 를 받는다.
public final class Example{
@NonNull
private final Delegate p$delegate = new Delegate();
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.checkParametersIsNotNull(var1, “<set-?>”);
this.p$delegate.setValue(this, $$delegatedProperties[0], var1);
}
}
-
Delegate instances
위의 예제에서 새로운 delegate instance 가 생성되는 것을 보았다.
이는 delegate instance 가 state 가 있을 때, delegate 가 추가 param 이 있을 때에 해당한다.
( 바로 아래 다루는데, 위의 조건이 아닌 경우에는 singleton 으로 만들어 재활용하면 된다. )
-
만약 stateless 하면서 하나의 single instance 로 모든 instance 의 property 를 대응하고 싶다면
delegate class 를 object keyword 를 사용해서 singleton 으로 만들어주면 된다.
object FragmentDelegate {
operator fun getValue(thisRef: Activity, property: KProperty<*>): Fragment? {
return thisRef.fragmentManager.findFragmentByTag(property.name)
}
}
-
어떤 object 던지 delegate 로 확장시킬 수 있다.
getValue, setValue 가 extension function 으로 정의 가능하기 때문이다.
Map, MutableMap 등이 대표적인 extension function 으로 확장된 delegate 이다.
그들은 property name 을 key 로 사용한다.
-
만약 한 local delegate 를 여러개의 property 의 delegate 로 쓰고 싶다면 (예를 들면 Map 의 경우)
class constructor 에서 초기화 해주어야 한다.
( Kotlin 1.1 부터는 function 안의 local variable 을 delegated property 로 쓸 수 있게도 된다... )
-
모든 delegated property 는 관련된 delegate object 와 metadata 를 만드는 overhead 가 생긴다.
여러개의 property 에 delegates 를 재사용하는 것도 가능하며, 가능하다면 이 형태로 가는 것도 좋다.
사실 delegate 를 써야 하는지를 먼저 생각해보는 것이 좋다. 간단한 것이나 꼭 필요없는데 위임하는 케이스는 안 하는 것이 좋다.
-
Generic delegates
generic delegate 를 사용하는 경우 동일하게 작동한다.
하지만 primitive type 을 쓰는 경우에는 마찬가지로 boxing, unboxing 을 고려해야 한다.
그래서 generic delegate 대신 IntArray 와 같이 specialized 된 class 를 가져다 쓰거나 정의해서 쓰는 것이 좋을 수 있다.
-
Standard delegates: lazy()
Delegates.notNull(), Delegates.observable(), lazy() 와 같은 특별한 delegate 를 제공한다.
-
lazy() 는 read-only property 에 대해 lazy 하게 lambda 를 수행해서 값을 assign 해서 return 하도록 하는 역할을 한다.
private val dateFormat: DateFormat by lazy{
SimpleDateFormat(“dd-MM-yyyy”, Locale.getDefault())
}
이 때 또 주의해야 할 것은 lazy 가 inline 이 아니기 때문에 lazy 에 전달되는 lambda 는 Function 으로 instantiate 된다.
-
lazy 를 사용될 때 간과되기 쉬운 점 중 하나는 lazy 가 “mode” 라는 optional argument 를 받는다는 것이다.
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)
}
default mode 는 LazyThreadSafetyMode.SYNCHRONIZED 이다.
그리고 이 로직은 비싼 double-check lock 을 사용한다.
따라서 single thread 에서만 사용된다는 것을 guarantee 할 수 있다면 NONE 으로 해주는 것이 좋다.
Ranges
-
Ranges 는 유한한 범위를 나타내는 특별한 녀석이다.
Range 의 value 는 Comparable 을 구현한 녀석이면 가능하다.
Range 문법을 사용하면 ClosedRange interface 를 구현한 녀석을 만들어낸다.
-
Inclusion tests
Range 를 사용하면 in ,!in 을 통해 포함관계를 알기 쉽다.
여기서 primitive type 에 대해서는 잘 최적화되어 작동한다.
if (i in 1..10){
println(i)
}
if (1 <= i && i <= 10) {
System.out.println(i)
}
-
when 에서도 잘 작동한다. overhead 도 없다.
val message = when(statusCode){
in 200..299 -> “OK”
int 300..399 -> “Find it somewhere else”
else -> “Oops”
}
-
Range 는 direct call 이 아닐 경우 cost 가 발생할 수 있다.
private val myRange get() = 1..10
fun rangeTest(i: Int) {
if ( i in myRnage ) {
println( i )
}
}
private final IntRange getMyRange(){
return new IntRange(1, 10);
}
public final void rangeTest(int i){
if( this.getMyRange().contains(i) ){
System.out.println(i);
}
}
따라서 non-null primitive type range 의 경우는 가능하다면 direct 로 쓰는 것이 더 좋을 수 있다.
-
non-primitive type 의 경우는 ClosedRange 가 항상 생성되며 다음과 같이 비교된다.
if (name in “Alfread”..”Alicia”){
println(name)
}
if(RangesKt.rangeTo((Comparable)”Alfred”, (Comparable)”Alicia”).contains((Comparable)name)){
System.out.println(name);
}
따라서 non-primitive type 의 경우에는 constant 로 만들어 새로운 ClosedRange 가 매번 생성되는 것을 막는 것이 좋다.
-
Iterations: for loops
Integral type ranges( Float, Double 을 제외한 primitive type ) 은 progress 를 가질 수 있다.
이 말은 해당 type 의 range 는 for loop 에서 사용될 수 있다는 것이다.
일반적인 increase 형태는 overhead 가 전혀 없다.
downTo 를 사용한 decrease 형태 역시 overhead 가 전혀 없다.
-
대부분의 plain 한 케이스에는 optimize 가 잘 되어 overhead 가 없는데..
다음과 같은 케이스에는 문제가 된다.
for (i in (1..10).reversed()){
println(i)
}
compile 되면 아래와 같다고...
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;
}
progression 을 만드는데 2개 "이상"의 함수를 사용하면 overhead 가 생긴다고 보면 된다.
따라서 for loop iterate 할 때 range expression 을 쓴다면
single function 만 쓰는 것이 좋다.
( .., downTo, until )
-
Iterations: forEach()
for loop 대신 forEach extension function 을 써서 iterate 하는 방법도 있다.
forEach 는 Iterable 에 대해서만 최적화가 되어 있다.
그래서 Range 에 대해 forEach 를 수행하면, Range 를 Iterable 로 치환해서 forEach 를 수행한다.
따라서 range 에 대해 iterate 할 때는 forEach 보다 for loop 가 더 좋다.
-
Iterations: collection indices
Kotlin 에서는 Array 와 Collection 에 대해 “indices" extension property 를 제공한다.
val list = listOf(“A”, “B”, “C”)
for ( i in last.indices ) {
println( list[i] )
}
compile 하면 overhead 도 없다.
그렇다면.. 내가 만든 indices 도 잘 작동하지 않을까 하고 다른 class 에 IntRange 를 return 하는 extension function 을 추가하면 똑같이 overhead 없이 컴파일 될까?
그렇지 않다...
그래서 가능하면 indices 같은거 정의하지 말고 간단한 form 으로 사용해라.
-
Kotlin 의 숨겨진 비용을 최종적으로 보고 돼지왕 왕돼지가 결론을 내려보면..
1. Kotlin 이 발전함에 따라 최적화는 조금씩 이루어질 것이지만, 원리를 이해하고 가급적 최적화된 코드를 짜는 것이 좋다. 최적화를 할 수 없는 경우도 분명히 있을 것이고, 언제 최적화가 이루어질지도 모르기 때문!
2. 가급적 제공되는 api 와 기능을 정확하게 full 로 이해하고 쓰는 것이 좋다. (예를 들면 inline, const, primitive specific classes 등 )
3. 가급적 간단한 형태로 쓰는 것이 좋다. 괜히 꼬아서 쓰면 optimize 가 더 힘든가보다.
'프로그래밍 놀이터 > Kotlin, Coroutine' 카테고리의 다른 글
[Kotlin] Parcelable 을 쉽게 만들어보자 (0) | 2018.04.01 |
---|---|
[Kotlin] initializer 이야기 (0) | 2018.01.19 |
[Kotlin] Kotlin 의 숨겨진 비용 #2 (0) | 2018.01.17 |
[Kotlin] Kotlin 의 숨겨진 비용 #1 (2) | 2018.01.16 |
[Kotlin] 장점, 단점, 그리고 아쉬운 점 이야기 (0) | 2018.01.15 |
댓글