Effective Kotlin #7 비용 줄이기 (Make it cheap)

yeji·2022년 12월 10일
0

Effective Kotlin

목록 보기
7/7

item 45. 불필요한 객체 생성을 피하라

객체 생성은 언제나 비용이 들어가고 상황에 따라 굉장히 큰 비용이 들어갈 수 있다. 따라서 불필요한 객체 생성을 피하는 것이 최적화 측면에서 좋다.
JVM에서는 동일한 문자열을 처리한다면 기존의 문자열을 재사용한다. (리터럴을 저장하는 경우 heap영역에 있는 string constant pool에 저장하여 재사용)

var str1:String = "Lorem" // literal
var str2:String = "Lorem" // literal
var str3:String = StringBuilder("Lorem").toString() // new

println(str1 == str2) // true, 같은 주소
println(str1 === str2) // true, 같은 값
println(str2 === str3) // false
println(str2 === str3.intern()) // true, string pool에 값이 이미 있는지 찾고 해당 리터럴 값을 반환

Integer와 Long처럼 박스화한 기본 자료형도 작은 경우에는 재사용된다. (-128~127 범위를 캐시)
- 2^7 인 이유? 가장 많이 사용하는 범위이기 때문. 그래서 memory reference cached된다.

val i1:Int? = 1
val i2:Int? = 1
    
println(i1 == i2) // true
println(i1 === i2) // true, i2를 IntegerCache로부터 읽어 들이기 떄문
    
val i3:Int? = 1234
val i4:Int? = 1234
    
println(i3 == i4) // true
println(i3 === i4) // false, 캐싱되지 않음

객체 생성 비용은 항상 클까?

객체 wrapping 시 발생하는 비용

  • 객체는 더 많은 용량을 차지한다.
    - 현대의 64bit JDK에서 객체는 8바이트의 배수, + 앞부분은 12바이트 헤더
    • int는 4바이트지만 Integer는 16바이트 + 레퍼런스 8바이트이다.
  • 캡슐화된 객체는 함수 호출 비용이 발생. 비용이 크진 않지만 티끌모아 태산이다.
  • 객체 생성 시 비용 발생
    - 메모리 영역에 할당
    • 이에 대한 레퍼런스 생성 등의 비용이 발생

불필요한 객체 제거 방법

객체 선언

  • 객체를 재사용 (싱글톤)
  • 팩토리 함수가 가지는 캐시 활용
  • 무거운 객체를 외부 스코프로 보내기 (톱레벨로 빼기)
    - 컬렉션 처리에서 이루어지는 무거운 연산이 있다면 컬렉셔 처리 함수 내부에서 외부로 빼는 것이 좋다. (여러 번 수행하지 않도록 캐싱된 값을 사용)
  • 지연 초기화
    - lazy를 통해 초기화를 지연. 하지만 호출에 대한 응답 시간이 길어질 수 있기에 항상 지연 초기화가 좋은 것은 아니다. 백엔드 애플리케이션에서 좋지 않을 수 있음.
    • 성능 테스트도 복잡해짐
  • 기본 자료형 사용하기
    - 기본 자료형은 일반적인 객체와 다르게 추가적으로 포함되는 것이 없어 가볍고, 값에 접근할 때 추가 비용이 들지 않기에 빠르다.
    • 코틀린/JVM 컴파일러는 내부적으로 최대한 기본 자료형을 사용하지만 nullable type을 연산할 때, 타입을 제네릭으로 사용할 때 기본자료형을 wrapping한 자료형을 사용한다. (Int?은 Integer를 사용)
      • int는 4byte고, 64bit 기준으로는 16byte이기에 왜 Int? 사용해서 nullable하지 않게 사용하는지에 대한 답이 될 수 있다. 불필요한 용량 사용

item 46. 함수 타입 파라미터를 갖는 함수에 inline 한정자를 붙여라

inline 한정자의 역할

  • 컴파일 타입에 함수를 호출하는 부분을 함수의 본문으로 대체하는 것

inline 한정자를 사용하면 함수 본문으로 점프했다가 함수를 호출했던 위치로 다시 점프하는 과정이 일어나지 않는다.

inline modifier 사용 장점
1. 타입 아규먼트에 reified 한정자를 붙여 사용 가능
- JVM 바이트 코드에는 제네릭이 존재하지 않는다. 컴파일을 하면 제네릭 타입과 관련된 내용이 제거된다. 타입 제한을 주기 위해 타입 파라미터를 사용하면 컴파일 시 타입 파라미터를 사용한 부분이 타입 아규먼트로 대체되게 된다.
2. 함수 타입 파라미터를 가진 함수가 훨씬 빠르게 동작
- 모든 함수는 inline을 붙이면 조금 더 빠르게 동작한다. 함수 호출과 리턴을 위해 점프하는 과정과 백스택을 추정하는 과정이 일어나지 않기 때문이다.
- 함수파라미터 컴파일 (매개변수 개수에 따라 functionN 타입으로 변경됨)
- ()->Unit은 Fuction0
- (Int)->Int는 Fuction1<Int, Int>
- (Int, Int)->Int는 Fuction2<Int, Int, Int>
3. 비지역적 리턴을 사용할 수 있다.
- inline을 사용하지 않은 함수의 경우에는 내부에서 return을 사용할 수 없다.
- 이는 함수가 객체로 wrapping되어서 발생하는 문제로, 함수가 다른 클래스에 위치(non-local)하기에 return을 사용해서 main 측으로 돌아올 수 없게 되는 것이다.
- inline 함수는 main 함수 내부에 박히기에 돌아올 수 있다.
non inline function인 경우 @ scope 지정해주어야 return이 가능하다.

inline 한정자의 한계와 비용

  • inline 한정자를 남용하면 코드의 크기가 기하급수적으로 커질 수 있다. 또한, 재귀적으로 동작하게된다면 무한하게 대체되는 문제가 발생하게 된다.
  • public inline 함수 내부에서 private과 internal 가시성을 가진 함수와 프로퍼티를 사용할 수 없다. (구현을 거의 숨길 수 없음) -> 외부 파일에서 접근하는거라 접근이 불가해짐

crossinline, noinline

함수를 인라인으로 만들고 싶지만, 어떤 이유로 일부 함수 타입 파라미터는 inline으로 받고 싶지 않은 경우가 있다면? crossinline, noinline 한정자 사용

  • crossinline
    - inline 함수를 매개변수로 받지만, 비지역적(non-local) 반환은 불가하게 만들 때 (non-local return 허용 X)
    - inline으로 만들지 않은 다른 람다 표현식과 조합해서 사용할 때 문제가 발생하는 경우 활용
  • noinline
    - inline 함수를 매개변수로 받을 수 없게 만들 때
    - inline 함수가 아닌 함수를 인자로 사용하고 싶을 때 활용

item 47. 인라인 클래스의 사용을 고려하라

비즈니스 로직을 위한 wrapper class 생성이 필요하다. 그러나 추가 heap 할당으로 인해 런타임 오버헤드가 발생하고, wrapping된 타입이 원시타입인 경우 성능이 크게 저하된다.
이러한 문제를 해결하기 위해 코틀린에서는 inline class를 제공한다.
inline class는 값 기반 클래스들의 부분집합으로 identity를 갖지 못하고, 값만 가지게 된다.

inline class
기능 및 사용법

  • 하나의 값을 보유하는 객체도 inline으로 만들 수 있음 (kotlin 1.3부터 도입된 기능)
  • 기본 생성자 프로퍼티가 하나인 클래스 앞에 inline을 붙이면 해당 객체를 사용하는 위치가 모두 해당 프로퍼티로 교체됨

다른 자료형을 wrapping해서 새로운 자료형을 만들 때 많이 사용한다.

  • 측정 단위 표현 (millis, minutes, kb, mb..)
  • 타입 오용으로 발생하는 문제를 방지

item 48. 더 이상 사용하지 않는 레퍼런스를 제거하라

JVM의 GC가 메모리 관리를 해준다고 메모리 관리를 완전히 무시한다면, 메모리 누수가 발생할 수 있다. 상황에 따라 OOM가 발생할 수 있다.
객체에 대한 참조를 companion으로 유지하면 GC가 해당 객체에 대한 메모리 해제를 할 수 없다.
따라서 의존 관계를 정적으로 저장하지 말고 다른 방법을 활용하는 것이 좋다.
객체에 대한 레퍼런스를 다른 곳에 저장할 때는 메모리 누수가 발생할 가능성을 언제나 신경써야 한다.

Array
안쓰면 null로 바꿔주어라
사용하지 않는 객체를 Null로 해라 -> gc 수거 대상이 됨

SoftReference & WeakReference & StrongReference

  • SoftReference : 메모리가 부족한 경우에만 gc가 해당 객체를 수거
  • WeakReference : GC가 값을 정리하는 것을 막지 않는다. 다른 레퍼런스가 값을 사용하지 않으면 곧바로 제거된다. -> GC cycle에 해당 객체를 반드시 수거
  • StrongReference : new로 객체를 생성하여 연결하는 일반적인 경우 -> 해당 레퍼런스가 계속 유지된다면 gc가 수거하지 않음
profile
🐥

0개의 댓글