카테고리 없음

[Kotlin] 코딩 테스트 Collection 방식과 Sequence 방식의 성능 비교

yujinius 2024. 12. 26. 20:21

실험 배경

프로그래머스의 기초 코딩 테스트 홀짝에 따라 다른 값 반환하기 문제를 풀다가 Collection 방식과 Sequence 방식의 성능 차이에 대해 궁금증이 생겼다. 이에 따라 간단한 연산부터 복잡한 연산까지 두 방식을 비교하여 어떤 상황에서 어떤 방식이 더 적합한지 알아보는 실험을 진행하였다.


실험 목적

  1. Collection 방식과 Sequence 방식의 성능을 비교한다.
  2. 간단한 연산과 복잡한 연산에서 두 방식의 성능 차이를 분석한다.
  3. 데이터 크기와 연산 복잡성에 따른 적합한 방식을 도출한다.

실험 환경 및 코드

코드 설명

실험에서는 Collection 방식을 사용하는 코드와 Sequence 방식을 사용하는 코드를 각각 작성하였다.

Collection 방식

fun solutionWithCollection(n: Int): Int {
    return if (n % 2 == 1) {
        val k = n / 2 + 1
        k * k
    } else {
        (2..n step 2).sumOf { it * it }
    }
}

Sequence 방식

fun solutionWithSequence(n: Int): Int {
    return if (n % 2 == 1) {
        val k = n / 2 + 1
        k * k
    } else {
        (2..n step 2).asSequence().map { it * it }.sum()
    }
}

메인 함수

fun main() {
    val solution = Solution()
    val testSizes = listOf(10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000, 100_000_000)

    println("Performance Comparison (Time in nanoseconds):")
    println("-------------------------------------------------------------")
    println(String.format("%10s | %15s | %15s | %10s", "n", "Collection", "Sequence", "Faster"))
    println("-------------------------------------------------------------")

    for (n in testSizes) {
        val collectionTime = measureNanoTime {
            solution.solutionWithCollection(n)
        }

        val sequenceTime = measureNanoTime {
            solution.solutionWithSequence(n)
        }

        val faster = if (collectionTime < sequenceTime) "Collection" else "Sequence"

        println(
            String.format(
                "%10d | %15d | %15d | %10s",
                n, collectionTime, sequenceTime, faster
            )
        )
    }
}


실험 결과

실험 1: 간단한 연산

연산이 간단한 경우, Collection 방식이 Sequence 방식보다 빠른 성능을 보였다.

분석

  • Collection 방식은 즉시 연산(Eager Evaluation)으로 데이터를 한 번에 처리하기 때문에 오버헤드가 적다.
  • Sequence 방식은 지연 연산(Lazy Evaluation)으로 설정 비용과 함수 호출 오버헤드가 발생하여 작은 데이터에서는 더 느리다.

실험 2: 복잡한 연산

복잡한 연산에서는 Sequence 방식이 Collection 방식보다 빠른 성능을 보이는 경향이 있었다.

실험 코드

// Collection 방식
fun solutionWithCollection(n: Int): Int {
    return if (n % 2 == 1) {
        val k = n / 2 + 1
        k * k
    } else {
        (2..n step 2)
            .filter { it % 3 == 0 }
            .map { it * it }
            .filter { it % 5 == 0 }
            .sum()
    }
}

// Sequence 방식
fun solutionWithSequence(n: Int): Int {
    return if (n % 2 == 1) {
        val k = n / 2 + 1
        k * k
    } else {
        (2..n step 2).asSequence()
            .filter { it % 3 == 0 }
            .map { it * it }
            .filter { it % 5 == 0 }
            .sum()
    }
}

실험 결과

분석

  • 중간 연산이 많을수록 Sequence 방식의 지연 평가가 빛을 발한다.
  • Sequence 방식은 중간 데이터를 저장하지 않으므로 메모리를 효율적으로 사용한다.

종합 분석

연산이 단순할 때: Collection 방식이 유리하다

  1. 즉시 평가 (Eager Evaluation):
    • sumOf는 컬렉션을 한 번 순회하며 모든 연산을 즉시 수행한다.
    • 중간 데이터를 생성하지 않아 메모리 사용량이 적다.
  2. 함수 호출 오버헤드 없음:
    • Collection 방식은 각 단계에서 함수 호출 비용이 발생하지 않는다.

연산이 복잡할 때: Sequence 방식이 유리하다

  1. 중간 연산이 많은 경우:
    • Sequence 방식은 지연 평가(Lazy Evaluation)로 불필요한 계산을 생략한다.
  2. 데이터 크기가 큰 경우:
    • Sequence 방식은 메모리를 효율적으로 사용하며, 중간 데이터를 저장하지 않아 성능이 높다.

최종 결론

  1. Collection 방식 사용 권장
    • 연산이 단순하고 중간 연산이 적으며 데이터 크기가 작은 경우.
    • 예: (2..n step 2).sumOf { it * it }
  2. Sequence 방식 사용 권장
    • 연산이 복잡하거나 중간 연산이 많고 데이터 크기가 큰 경우.
    • 예: (2..n step 2).asSequence().map { it * it }.sum()
  3. 데이터 크기 및 복잡성 고려
    • 데이터 크기가 작을 때는 Collection 방식이 더 빠르며, 복잡한 연산이 많아질수록 Sequence 방식의 효율성이 커진다.
    •  

이번 실험을 통해 두 방식의 특성과 사용 시점을 명확히 이해할 수 있었다. 앞으로 문제의 특성에 따라 적합한 방식을 선택하여 더 효율적인 코드를 작성할 수 있을 것이다.