오늘도 더 나은 코드를 작성하였습니까?

컬렉션 연산 inline 본문

Kotlin in Action/코틀린 답게 사용하기

컬렉션 연산 inline

hik14 2021. 2. 25. 19:34

컬렉션에 작용하는 코틀린 표준라이브러리 성능을 알아보자.

 

코틀린 표준라이브러리 컬렉션 함수는 대부분 람다를 인자로 받는다. 

표준라이브러리 함수를 사용하는 것과 직접 구현하여 사용하는 것 무엇이 효율적일까?

 

data class Person(val name: String, val age: Int)

fun main() {

    val people = listOf( Person("Alice", 29), Person("Bob",31))
    println(people.filter { it.age < 30 })


    val result = mutableListOf<Person>()
    for (person in people){
        if (person.age < 30) result.add(person)
    }
    println(result)
}

코틀린 filter 함수는 inline 함수이다.

따라서 filter 함수의 바이트코드는 그 함수에 전달된 람다 본문의 바이트코드와 함께 filter를 호출한 위치에 생긴다.

 

결과 직접 구현한 것과 filter를 사용한 코드 둘다 바이트 코드는 비슷하게 생겼다. 

 

println(people.filter{ it.age > 30 }.map(Person::name))

위 코드는 람다와 멤버 참조를 사용한다. 여기서 사용한 filter 와 map 은 인라인 함수다. 둘다 추가적인 객체, 클래스 생성은 없다.

이 코드는 filter와 map 사이에 결과를 저장하는 중간 리스트를 생성한다. 

 

이때, asSequence를 통해 리스트 대신 사용하면 중간 리스트 생성에 대한 부가비용은 삭제된다.

하지만,  이때 각 중간 시퀀스는 람다를 필드에 저장하는 객체로 표현되고 최종연산은 중간 시퀀스에 있는 여러 람다를 연쇄 호출한다. 

시퀀스는 람다를 인라인 하지 않는다. 따라서 성능향상을 위해  모든 컬렉션 연산자에 Sequence를 사용하면 안된다.

크기가 작은 컬렉션에 대해서는 오히려 일반 컬렉션 연산이 더 효율적이다. 시퀀스를 통해 성능향을 하기 위해선 컬렉션의 크기가 커야된다. 

 

 

함수를 인라인으로 선언해야 하는 경우

 

inline키워드를 모든 함수에 붙히면 안된다.

 

람다를 인자로 받는 함수만 성능이 향상될 수 있다.

JVM은 이미 일반 함수 호출시에도 강력한 인라이닝을 지원한다.

JVM은 코드를 실행분석하여 바이트 코드를 기계어로 번역하는 과정에서 최선의 방법을 택한다.

 

일반적인 함수를 인라인 함수로 하지말아야 되는건,

함수 코드의 구현을 1번 하고 그것을 여러번 호출하면 함수코드의 바이트 코드는 한 번만 필요하다.

하지만 코틀린에서 인라인으로 만들면 호출되는 시점에 지속적으로 동일한 바이트코드가 생성된다. 

 

람다를 인자로 받는 함수를 인라인하면 이익이 더 많다.

- 함수호출 비용

- 람다를 위한 클래스 / 인스턴스 생성 불필요

- 인라인을 통한 람다에서 사용할 수 있는 기능

 

단, 함수의 본문이 너무 길경우 주의해야 한다 모든 곳에 긴 바이트 코드가 생성된다. 이런경우 람다와 무관한 부분을 비인라인 함수로 재작성한다.

자원관리를 위해 인라인된 람다 사용하기

람다로 중복을 없앨 수 있는 일반적인 패턴중 한 가지는 어떤 작업을 하기 전에 자원을 획득하고 작업을 마친후 자원을 해제하는 자원관리다.

 

자원관리 패턴을 만들때 보통 try / finally 문을 사용한다.

try 블록을 시작하기 직전 자원을 획득하고 finally 블록에서 자원을 해제한다.

 

inline fun <T> synchronized(lock: Lock, action: () -> T): T{
    lock.lock()
    try {
        return action()
    }
    finally {
        lock.unlock()
    }
}

 

synchronized 함수는 락 객체를 인자로 취한다 코틀린 라이브러리에는 좀 더 코틀린다운 API를 통해 같은 기능을 제공하는 withLock이라는 함수도 있다. withLock은 Lock 인터페이스의 확장함수다.

val l: Lock = ...

l.withLock{
	// lock을 통해 얻은 자원을 사용한다.
}

fun <T> Lock.withLock(action: () -> T): T{

	lock()
 
 	try{
    	return action()
    }finally{
    	unlock()
    }
    
}

자바에서의 파일을 읽는 try-wtih-resource 구문.

   static String readFirstLineFromFile(String path) throws IOException {
        try (BufferedReader br = new BufferedReader(new FileReader(path))) {
            return br.readLine();
        }
    }

 

Close를 필요로 하는 Statement Class의 인스턴스나, Stream 타입의 클래스들이 동작후에 필요로하는 Close() 메소드를 자동 실행 해주는 공간이라고 한다 Java SE 7 생긴 구문이며, Finally 다음에 close() 메소드 동작을 하지 않아도 된다.

 

코틀린의 use 확장함수.

fun readFirstLineFromFile(path: String): String{
    BufferedReader(FileReader(path)).use { br ->  // 버퍼객체 생성후 use함수에 람다를 넣어넘김
    	return br.readLine() // readFirstLineFromFile함수에 대한 리턴값이다. 
    }    
}

use 함수는 자원에 대한 확장함수이며 람다를 인자로 받는다. use는 람다를 호출 후 자원을 닫아준다. 람다가 정상종료시에도 닫아주고 예외가 발생한 경우에도 닫아준다. use역시 inline 함수이다.