[kotlin] sequence
Sequence
Sequence란?
일반적인 map, filter 같은 컬렉션 함수는 컬렉션을 즉시 생성. 컬렉션 함수가 연쇄할 때 마다 계산 중간 결과를 새로운 컬렉션에 임시로 담음. 반면, 시퀀스는 중간 임시 컬렉션을 사용하지 않고도 컬렉션 연산을 연쇄할 수 있음. 예를 들어,
people.map { it.name }.filter { it.startsWith("A") }
연쇄 호출이 리스트 2개를 만듬. 그리고 즉시 생성. 원소가 수백만개면 효율이 떨어질 수. 시퀀스를 쓰면,
people.asSequence().map { it.name }.filter { it.startsWith("A") }
시퀀스는 중간 연산, 최종 연산으로 나뉨. map, filter는 중간 연산이고 toList와 같은 최종연산을 통해 결과를 뽑기 전에는 컬렉션을 생성하지 않음. 즉, 지연 연산 toList를 통해 최종 연산을 해도 중간 결과를 저장하는 컬렉션이 생기지 않아서 원소가 많을 경우에는 성능에 더 좋음. 1번 예제는 map이 다 돌고 filter를 돈다. 2번 예제는 map, filter ….의 반복
- 그렇다면 모두 Sequence로 쓰면 되지 않을까??
정답은 ‘그렇지 않다’ 이다.
왜냐하면, 시퀀스는 람다를 인라인하지 않기 때문이다.
구체적으로 설명하기 전에, 간략하게 인라인 함수의 한계를 살펴보자.
inlining은 함수 본문에서 파리미터로 받은 람다를 호출한다면 그 호출을 쉽게 람다 본문으로 바꿀 수 있다.(인라이닝의 특징인데 자세한 설명은 여기서는 생략)
하지만, 파라미터로 받은 람다를 다른 변수에 저장하고 나중에 그 변수를 사용한다면 람다를 표현하는 객체가 어딘가는 존재해야 하기 떄문에 람다를 인라인 할 수 없다.
그 좋은 예시가 바로 시퀀스이다. 시퀀스에 대해 동작하는 메소드 중에서는 람다를 받아서 모든 시퀀스 원소에 그 람다를 적용한 새 시퀀스를 반환하는 함수가 많다. 그런 함수는 인자로 받은 람다를 시퀀스 객체 생성자의 인자로 넘기곤 한다.
예를 보자.
fun <T, R> Sequence<T>.map(transform: (T) -> R) : Sequence<R> {
return TransformingSequence(this, transform)
}
이 map함수는 transform 파라미터로 받은 함수를 직접 호출하지 않고 TransformingSequence라는 클래스에 전달한다. 그리고 TransformingSequence 생성자는 전달 받은 람다를 프로퍼티로 저장한다.
이런 기능을 지원하려면 map에 전달되는 transform 인자를 일반적인(인라이닝하지 않는) 함수로 만들 수 밖에 없다. 즉, transform 함수 인터페이스를 구현하는 무명 클래스 인스턴스로 만들어야 한다.
그럼 이제 원래 논제로 돌아가보자.
asSequence()를 통해 리스트 대신 시퀀스를 사용하면, 중간 리스트로 인한 부가 비용은 줄어든다. 이 때 각 중간 시퀀스는 람다를 필드에 저장하는 객체로 표현되며, 최종 연산은 중간 시퀀스에 있는 여러 람다를 연쇄 호출한다. (바로 계산하는 것이 아니기 떄문에, 최종연산 때를 위해 람다를 저장을 해둬야함.)
따라서, 앞에서 설명한대로 시퀀스는(람다를 저장해야 하므로) 람다를 인라인 하지 않는다.
결론적으로, 지연 계산을 통해 성능을 향상시키려는 이유로 모든 컬렉션에 asSequence()를 붙여서는 안 된다. 시퀀스 연산에서는 람다가 인라인되지 않기 때문에, 크기가 작은 컬렉션은 오히려 일반 컬렉션연산이 더 성능이 나을 수도 있다. 시퀀스를 통해 성능을 향상시킬 수 있는 경우는 컬렉션 크기가 클 때 뿐이다.
참고 자료 : 코틀린 인 액션