본문 바로가기

benchmark

[Benchmark] Java Static vs Kotlin Companion Object

https://unsplash.com

 

안녕하세요.

이번에 자바 -> 코틀린으로 리펙토링 작업을 진행 중 입니다.

자바 코드를 코틀린으로 바꾸면서 두 코드 간의 성능 차이가 궁금해졌고,

직접 분석해보고 싶어서 안드로이드 스튜디오에서 벤치마크 하는 방법을 찾아봤습니다.


Microbenchmark

벤치마크에는 두 종류가 있습니다.

  • Microbenchmark : 작은 단위 또는 개별 컴포넌트의 성능을 측정
  • Macrobenchmark : 큰 단위 또는 전체 성능을 측정

 

내가 원하는 부분만 빠르게 테스트해보고 싶어서 Microbenchmark를 사용하기로 결정했습니다.

 

Android Developers 사이트에서 제시하는 benchmark 방법이 하나 나와있습니다.

아래 페이지를 따라 세팅하면 벤치마크를 돌릴 수 있는 환경이 마련됩니다.

https://developer.android.com/topic/performance/benchmarking/microbenchmark-overview?hl=ko

 

Microbenchmark  |  App quality  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Microbenchmark 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Jetpack Microbenchmark 라이브러리를 사용하여 An

developer.android.com

 

+) 휴대폰을 연결시켜서 테스트를 해야 합니다.


성능 비교

제가 오늘 비교해보고자 할 성능은 아래 두 상황입니다.

  1. 코틀린 클래스에서 java static method 호출
  2. 코틀린 클래스에서 kotlin companion object method 호출

코드 준비

짧은 시간이 걸리는 메서드와 길게 걸리는 메서드 각각 1개씩 준비를 했습니다.

 

Java Static Method

public class MathUtils {
    public static int addShort(int a, int b) {
        return a + b;
    }
    
    public static int addLong(int a, int b) {
        int sum = 0;
        
        for(int i = 0; i < 10000; ++i) {
            if (i % 2 == 0) sum += a;
            else sum += b;
        }
        
        return sum;
    }
}

 

Kotlin Companion Object Method

class MathUtilsKt {
    companion object {
        fun addShort(a: Int, b: Int): Int {
            return a + b
        }

        fun addLong(a: Int, b: Int): Int {
            var sum = 0

            repeat(10000) {
                sum += if (it % 2 == 0) a else b
            }

            return sum
        }
    }
}

테스트 코드

benchmark할 코드는 아래 디렉토리에 위치키셔야 합니다

(androidTest)

import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class CompareStaticTest {
    @get:Rule
    val benchmarkRule = BenchmarkRule()

    @Test
    fun javaStaticShort() {
        benchmarkRule.measureRepeated {
            var sum = MathUtils.addShort(1, 3)
            sum += MathUtils.addShort(1, 3)
            sum += MathUtils.addShort(1, 3)
            println(sum)
        }
    }

    @Test
    fun javaStaticLong() {
        benchmarkRule.measureRepeated {
            println(MathUtils.addLong(1, 3))
        }
    }

    @Test
    fun kotlinCompanionObjectShort() {
        benchmarkRule.measureRepeated {
            var sum = MathUtilsKt.addShort(1, 3)
            sum += MathUtilsKt.addShort(1, 3)
            sum += MathUtilsKt.addShort(1, 3)
            println(sum)
        }
    }

    @Test
    fun kotlinCompanionObjectLong() {
        benchmarkRule.measureRepeated {
            println(MathUtilsKt.addLong(1, 3))
        }
    }
}

 

measureRepeated라는 메서드가 보이는데,

이는 충분한 반복을 돌려서 테스트이 일관성을 얻기 위함 입니다.


결과

종류 소요 시간
Java Static Function Short 5,337 ns
Long 15,193 ns
Kotlin Companion Object Function Short 5,230 ns
Long 13,344 ns

 


분석

코틀린이 자바에 비해 둘 다 빠른 결과가 나왔습니다.

코틀린으로 바꾸는 과정에서 뜻밖의 성능 향상이라는 결과를 얻었지만, 왜 이런 결과가 나오는 것인지 의문이 생겼습니다.

 

1) Bytecode 차이

두 함수를 자바 bytecode로 변환하면 아래와 같이 차이가 있습니다.

 

Java Static Method

INVOKESTATIC 명령어를 사용하여 처리됩니다.

 

Kotlin Companion Object Method

INVOKEVIRTUAL이나 INVOKESPECIAL 명령어를 사용하여 처리됩니다.

별도의 인스턴스를 거쳐 호출한다는 의미이고,

여기서는 ‘companion object’ 라는 인스턴스를 거쳐 호출합니다.

 

2) JVM 최적화

JVM은 INVOKESTATIC 명령어를 매우 효율적으로 실행하도록 설계되어 있다고 합니다

JVM이 static 메서드를 빠르게 찾고 실행할 수 있도록 합니다

 

이에 비해, INVOKEVIRTUAL이나 INVOKESPECIAL은 인스턴스를 통한 접근이 필요하므로 이론적으로는 약간의 성능 오버헤드가 발생합니다.

 

그러나 때때로 JIT 컴파일러가 효율적으로 이 코드를 최적화하는 방법에 따라 차이가 날 수 있습니다.

반복적으로 호출되는 부분을 감지하고 최적화하는데,

이러한 최적화는 때때로 Kotlin의 companion object 접근을 java static 만큼이나 효율적으로 만들 수 있습니다.

 

3) 클래스 로딩 및 초기화

Java Static Method

Java에서 클래스가 로드될 때, 클래스의 모든 static 메서드와 필드는 JVM 메모리에 로드됩니다.

이후 호출 시 별도의 인스턴스를 생성할 필요가 없기 때문에 빠르게 접근 가능합니다.

 

Kotlin Companion Object

클래스 로딩 시 함께 초기화 되지만, 또다른 인스턴스로서 JVM에 의해 관리됩니다.

이로 인해 초기화 과정에서 추가적인 작업이 발생할 수 있지만,

클래스 로딩 이후에는 이 차이가 미미함


결론

단일 호출 시에는 Java Static Method가 훨씬 빠를 수 있습니다.

반복적인 작업의 경우, JVM 최적화 덕분에 실제 코드 실행에서 차이가 나지 않거나 결과가 반대로 날 수 있습니다.