Android Base64 Performance Benchmark

도입

안드로이드에서 String나 ByteArray를 Base64 로 인코딩 / 디코딩 하는 라이브러리는 많다. 기본적으로 안드로이드가 제공하는 Base64도 있고, Apache의 Commons-codec에도 있고, JDK 1.8에 (이제야) 구현된 Base64가 있다.

그러면 ‘어떤 것을 사용하는 것이 더 성능이 나올까?’ 라는 단순한 질문이 나올 수 있는데, 이미 자바로는 2014년에 비교한 Base64 encoding and decoding performance 라는 글이 있다.

다만 해당 글에는 안드로이드에선 사용하지 못하는 DataTypeConverter 나 sun 패키지가 있어 참고로 하기에는 어려운 점이 많다.

그래서 이번 기회로 안드로이드에서 사용할 수 있는 Base64 라이브러리 6종에 대해 성능 비교를 하려고 한다.

비교 대상

성능 비교 방법

비교 방법으로는 크게 3가지가 있다.

  • ByteArray <-> ByteArray 인코딩/디코딩
  • ByteArray <-> String 인코딩/디코딩
  • 이미지 / 비디오 파일 인코딩

ByteArray <-> ByteArray 인코딩/디코딩

fun testBytes(bufferSize: Int): HashMap<String, TestResult> {
    val r = Random(125)
    val buffers = ArrayList<ByteArray>()
    for (i in 0 until bufferSize) {
        val buf = ByteArray(bufferSize)
        r.nextBytes(buf)
        buffers.add(buf)
    }

    val results = HashMap<String, TestResult>()
    for (codec in byteCodecList) {
        val name = codec.javaClass.simpleName
        results[name] = testByteCodec(codec, buffers)
    }

    return results
}

@Throws(IOException::class)
private fun testByteCodec(codec: Base64ByteCodec, buffers: List<ByteArray>): TestResult {
    val encoded = ArrayList<ByteArray>()
    val result = ArrayList<ByteArray>()

    val encodeTime =
        measureTimeStopWatch { for (buf in buffers) encoded.add(codec.encodeBytes(buf)) }

    val decodeTime =
        measureTimeStopWatch { for (ar in encoded) result.add(codec.decodeBytes(ar)) }

    return TestResult(encodeTime.toDouble(), decodeTime.toDouble())
}

buffersize 만큼의 사이즈를 가지는 ByteArray를 bufferSize개 만큼 생성하고, 이를 각 라이브러리마다 인코딩과 디코딩 시간을 측정한다. 예를 들어 1024로 지정했다면 length가 1024인 ByteArray를 1024개를 생성하여 인코딩 시간과 디코딩 시간을 측정하는 방식이다.

여기서 Base64ByteCodec 는 인터페이스로 이 인터페이스를 구현하는 클래스는 AndroidImpl, ApacheImpl, IHarderImpl, Java8Impl, MiGImpl 총 5개로 Guava는 아쉽게도 지원하지 않는다.

class AndroidImpl : Base64Codec, Base64ByteCodec {
    private val flag = Base64.DEFAULT

    override fun decodeBytes(base64: ByteArray): ByteArray {
        return Base64.decode(base64, flag)
    }

    override fun encodeBytes(data: ByteArray): ByteArray {
        return Base64.encode(data, flag)
    }

    override fun encode(data: ByteArray): String {
        return Base64.encodeToString(data, flag)
    }

    override fun decode(base64: String): ByteArray {
        return Base64.decode(base64, flag)
    }
}

내부 구현체는 이런 식으로 testByteCodec 메서드에서 decodeBytes, encodeBytes 등의 메서드를 호출하면 각 라이브러리의 코드가 호출되는 형식이다.

ByteArray <-> String 인코딩/디코딩

위 ByteArray <-> String 와 비슷한 방식이나 호출되는 메서드만 encode, decode가 불린다.

이미지 / 비디오 파일 인코딩

fun testFile(file: File): HashMap<String, TestResult> {
    val results = HashMap<String, TestResult>()
    val fileBytes = file.readBytes()

    for (codec in byteCodecList) {
        val name = codec.javaClass.simpleName
        results[name] = testByteCodecFile(codec, fileBytes)
    }

    // Guava doesn't support ByteArray -> ByteArray. so we ignore them.
    return results
}

@Throws(IOException::class)
private fun testByteCodecFile(codec: Base64ByteCodec, buffer: ByteArray): TestResult {
    val encodeTime = measureTimeStopWatch { codec.encodeBytes(buffer) }
    return TestResult(encodeTime.toDouble(), 0.0)
}

파일을 객체로 받아서 각 라이브러리마다 인코딩 시간만을 측정한다. (디코딩은 측정하지 않았다.)

벤치마크 결과

테스트 기기는 Galaxy S8, 데이터의 정확성을 위해서 전체 테스트 셋트를 3번 반복하여 나온 평균치로 산정했다.

ByteArray <-> ByteArray 인코딩/디코딩

전체적으로 Java8Impl 와 MiGImpl 가 비슷한 속도를 보이고, AndroidImpl는 디코딩에서는 강점을 보이는 반면 인코딩에서는 Apache와 다를바가 없는 속도를 보여주었다.

ByteArray <-> String 인코딩/디코딩

ByteArray <-> ByteArray를 지원하지 않는 Guava가 추가되었는데, 수치가 많이 튄 것을 볼 수 있다. 어떤 한번만 그런 것이 아닌 지속해서 발생하는 것으로 봐서는 테스트 방법이 적절치 않았다고 판단하거나 Guava가 다른 라이브러리와는 조금 다른 구현체를 가지고 있는 것 같다.

ByteArray <-> ByteArray때와 마찬가지로 인코딩 때는 Java8Impl, MiGImpl가 강점을 보이고 디코딩 때에는 Java8Impl 와 AndroidImpl가 강점을 보였다.

이미지 인코딩 (1.5MB JPG / 5MB PNG / 10MB JPG)

1.5MB에서는 5개 라이브러리가 큰 차이를 보이고 있지는 않았으나, 5MB와 10MB에는 Java8Impl – MiGImpl 가 서로 비슷하고, AndroidImpl – IHarderImpl > ApacheImpl 가 서로 비슷했다.

비디오 인코딩 (10MB MP4 / 30MB MP4)

이미지 인코딩의 5MB, 10MB 이미지와 큰 차이는 보이지 않았다. 마찬가지로 Java8Impl 와 MiGImpl가 서로 비슷했다.

정리

결론은 아래와 같이 정리된다.

  • JDK 1.8을 사용하고,  minSDKVersion을 26 이상으로 설정할 수 있다면 JDK 1.8 Base64가 평균적으로 제일 나은 성능을 보여준다.
  • 현실적으로  minSDKVersion를 그정도까지 높이는 것이 어렵다면, MiG Base64 가 제일 현실적이다.
  • Base64 디코딩을 주로 한다면 Android Base64가 그나마 대안이 될 수 있다. 다만 인코딩은 Apache와 크게 다를바가 없음을 알아두자. 그리고 클라이언트 입장에서는 인코딩을 자주 하지, 디코딩을 자주 하지는 않는다.
  • Apache, IHarder는 사용하지 않는 것으로 한다.
  • Guava의 경우 이번 테스트에서는 수치가 튀었지만, 구성하는 데에 문제가 없다면 굳이 바꿀 필요는 없을 것 같다.

벤치마크시에 사용된 샘플 앱은 Github에 공개되어 있다.