Jetpack Compose Optimation

Jetpack Compose의 Collection Fast API 사용하기

ui-util의 random-access 활용

Ji Sungbin
성빈랜드

--

Photo by Jonathan Larson on Unsplash

Computer Science에서 데이터에 접근하는 방법은 여러 가지가 있지만 이번 글에서는 2가지의 방법을 다룹니다.

  • Sequential Access
  • Random Access

Sequential Access는 특정 요소에 접근하기 위해 순차적으로 접근하여야 함을 나타내고, Random Access는 특정 요소로 바로 접근 가능함을 나타냅니다.

https://en.wikipedia.org/wiki/Random_access#/media/File:Random_vs_sequential_access.svg

Sequential Access를 사용하는 자료 구조는 대표적으로 Linked List가 있고, Random Access를 사용하는 자료 구조는 대표적으로 Array가 있습니다.

코틀린으로 설명한다면 Array는 Random Access, List는 Sequential Access를 지원합니다.

fun main() {
arrayOf(1, 2).forEach(::println)
listOf(3, 4).forEach(::println)
}

위 코드를 자바로 디컴파일해 본다면 다음과 같습니다.

Object[] $this$forEach$iv = new Integer[]{1, 2};
for(int var3 = $this$forEach$iv.length; var2 < var3; ++var2) {
Object element$iv = $this$forEach$iv[var2];
int p1 = ((Number)element$iv).intValue();
System.out.println(p1);
}

Iterable $this$forEach$iv = (Iterable)CollectionsKt.listOf(new Integer[]{3, 4});
Iterator var8 = $this$forEach$iv.iterator();
while(var8.hasNext()) {
Object element$iv = var8.next();
int p1 = ((Number)element$iv).intValue();
System.out.println(p1);
}

Array는 Random Access를 지원하기에 Array#get으로 요소를 바로 가져온 반면, List는 Sequential Access를 지원하기에 Iterable#hasNext -> Iterable#next로 요소를 조회하며 가져옴을 확인할 수 있습니다.

또한 ArrayList의 정의에서도 이를 확인할 수 있습니다. JVM에서 돌아가는 코틀린의 Array는 자바의 ArrayList를 나타냅니다.

// Java
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable


// AbstractList<E> <- AbstractCollection<E> <- Collection<E> <- Iterable<E>

ArrayListRandomAccess를 확장하고 있지만,

// Kotlin
public interface List<out E> : Collection<E>
public interface Collection<out E> : Iterable<E>

List는 단순히 Iterable만 확장하고 있습니다. 이러한 차이에서 위와 같은 다른 결과가 나타나게 됩니다.

Jetpack Compose의 내부 구현을 살펴보면 List#fastForEach 확장 함수가 종종 보입니다. 이는 List에도 Random Access 접근을 추가하여 조금이라도 빠르게 컴포지션을 마치기 위함으로 보입니다.

@Suppress("BanInlineOptIn")
@OptIn(ExperimentalContracts::class)
inline fun <T> List<T>.fastForEach(action: (T) -> Unit) {
contract { callsInPlace(action) }
for (index in indices) {
val item = get(index)
action(item)
}
}

실제 구현은 위와 같으며 Array의 Random Access 구현과 동일하게 작동하도록 설계돼 있습니다. 그렇다면 Sequential Access와 Random Access의 작동 시간 차이는 얼마나 발생할까요? JMH 벤치마크를 돌려 보았습니다.

  • M1 램 16gb 기준
  • JVM에는 4g 할당
  • JIT를 위해 1번의 warmup을 진행함
  • avgt 모드로 5번의 iteration을 진행하였고 평균값을 사용함

모든 경우에서 List#fastForEach가 적은 시간이 소요됐으며, 적게는 약 800ns, 많게는 약 300ms까지 차이가 발생하였습니다. 제가 진행한 벤치마크 기준으론 저는 큰 차이를 느끼지 못해 정말 속도가 중요한 서비스가 아니면 stdlib인 List#forEach를 계속 사용할 거 같습니다. (첫 벤치마크 작성이다 보니 잘못됐을 수 있습니다. 이런 경우라면 댓글로 가르침 부탁드립니다.)

이런 속도 차이가 발생하는 원인은 Iterator#next의 구현을 보면 이해할 수 있습니다.

public E next() {
checkForComodification();
int i = cursor;
if (i >= size) throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}

next 요청이 들어오면 ConcurrentModification을 감지하기 위해 현재 요소의 사이즈와 요청된 인덱스를 비교하는 assertion을 진행합니다. 이 과정에서 사이즈를 매번 재계산하므로 추가 시간을 요구하는 오버헤드가 발생됨으로 이해하였습니다.

Jetpack Compose의 fast api는 ui-util 아티팩트에 모두 public으로 작성되었습니다. 따라서 아래와 같이 의존성을 추가하여 사용할 수 있습니다.

// 이 글을 쓰는 시점에서 최신 버전은 1.3.3 입니다.
implementation("androidx.compose.ui:ui-util:${version}")

1.3.3 버전 기준으로 다음과 같은 api를 제공합니다.

- List<T>.fastForEach(action: (T) -> Unit)
- List<T>.fastForEachReversed(action: (T) -> Unit)
- List<T>.fastForEachIndexed(action: (Int, T) -> Unit)
- List<T>.fastAll(predicate: (T) -> Boolean): Boolean
- List<T>.fastAny(predicate: (T) -> Boolean): Boolean
- List<T>.fastFirstOrNull(predicate: (T) -> Boolean): T?
- List<T>.fastSumBy(selector: (T) -> Int): Int
- List<T>.fastMap(transform: (T) -> R): List<R>
- List<T>.fastMaxBy(selector: (T) -> R): T?
- List<T>.fastMapTo(destination: C, transform: (T) -> R): C

끝!

ui-util 아티팩트는 컴포즈에 의존성이 걸려있지 않습니다. 진행하는 프로젝트에서 List의 빠른 접근이 필요하다면 ui-util 도입을 컴포즈와 무관하게 고려해 볼 수도 있습니다. 끝까지 읽어주셔서 감사합니다.

안드로이드 개발자분들을 위한 카카오톡 오픈 채팅방을 운영하고 있습니다.

벤치마킹에 사용된 전체 코드는 여기서 확인하실 수 있습니다.

--

--

Ji Sungbin
성빈랜드

Experience Engineers for us. I love development that creates references.