Letswift swift performance cover

Swift 성능 이해하기: Value 타입, Protocol과 스위프트의 성능 최적화

Let’Swift는 Swift 개발자들이 만나고, 배우고, 즐기는 컨퍼런스로 총 13개의 세션으로 이뤄졌습니다. Swift 개발자들이 모여 iOS의 넥스트 스텝을 이야기한 행사였습니다. 빠르게 발전하는 언어 Swift에 대한 뜨거운 열의를 엿볼 수 있었던 Let’Swift 행사 동영상을 공유합니다

Swift는 비교적 젊은 언어이므로 아직 성능 개선보다는 활용에 초점을 맞추는 분들이 많습니다. Swift의 성능을 이해하고 성능 최적화를 위해 고려할 수 있는 기법들에 대해 카카오의 개발자, 유용하님이 Value 타입과 Protocol을 중심으로 설명해 주셨습니다.


Value Semantics

  • Value 타입을 이용한 메카니즘
  • Value Type Semantics / Copy-by-Value Semantics
  • Identity가 아닌 Value(값)에만 의미를 둠: Int, Double 등의 기본 타입들
  • 포인터만 복사되는 참조(Reference) 시맨틱스와 비교됨: Objective-C, Java 등
  • 스위프트에서는 Objc에 없던 새로운 Value Type을 도입: struct, enum, tuple
  • Value type의 특징
    • 변수 할당 시 Stack에 값 전체가 저장됨
    • 다른 변수에 할당될 때 전체 값이 복사됨 (copy by value): 변수들이 분리되므로 하나를 변경해도 다른 것에 영향 없음
    • Heap을 안 쓰며 따라서 Reference Counting도 필요 없음
  • class vs struct
    • class의 경우(Reference type) Stack에 레퍼런스, Heap에 구조체 할당
    • Reference 타입은 복사해도 값이 하나를 향해 같은 값을 가짐

    • struct(value type)의 경우 포인트가 스택에만 쌓이고 복사해도 스택 사용
    • Value 타입의 경우 복사해도 분리됨
  • Value Semantics: ‘값’에 의해 구분되므로 Identity가 아닌 Value가 중요하며 동치를 위해서는 Equatable을 구현해야 함

이런 개발 뉴스를 더 만나보세요

protocol Equatable {
    func ==(lhs: Self, rhs: Self) -> Bool
}
extension CGPoint: Equatable { }
func ==(lhs: CGPoint, rhs: CGPoint) -> Bool {
    return lhs.x == rhs.x && lhs.y && rhs.y
}
  • value 타입은 Thread간 의도하지 않은 공유로부터 안전함
  • 값 모두를 복사해도 성능이 빠름
    • 기본 타입들, enum, tuple, struct은 정해진 시간 (constant time) 안에 끝남
    • 내부 데이터가 Heap과 혼용하는 struct의 경우는 정해진 시간 + 레퍼런스 copy등의 시간
    • String, Array, Set, Dictionary 등 쓰기 시 Copy-on-write로 속도 저하 보완
  • class가 중요한 경우
    • Value보단 Identity가 중요한 경우: UIView 같이 모든 변수에서 단 하나의 state를 갖는 개체
    • OOP 모델: 여전히 상속은 아주 훌륭한 도구
    • Objective-C 연동
    • Indirect storage (특수한 경우 struct내의 간접 저장소 역할)

성능을 위해 고려할 것들

  • 성능에 영향을 미치는 세 가지
    • Memory Allocation: Stack or Heap
    • Reference Counting: No or Yes
    • Method dispatch: Static or Dynamic
  • Heap 할당 문제
    • 할당 시에 빈 곳을 찾고 관리하는 것은 복잡한 과정이 필요
    • 과정이 thread safe해야 하므로 lock 등의 동기화 동작으로 인해 성능 저하
    • 반면 Stack 할당은 단순히 스택포인터 변수 값만 바꿔주는 정도
    • Heap 할당 최적화 예제: key가 Value type으로 stack에서만 메모리를 할당해서 Heap 할당 오버헤드가 없음
enum Color { case red, green, blue }
enum Theme { case eat, stay, play}
struct Attribute: Hashable {
    var color: Color
    var theme: Theme
    var selected: Bool
}
var cache = [String: UIImage]()
func makeMapMarker(color: Color, theme: Theme, selected: Bool) -> UIImage {
    let key = Attribute(color: color, theme: theme, selected:selected)
    if let image = cache[key] { 
        return image 
    }
    // ...
}
  • Reference Counting 문제
    • 변수를 복사할 때마다 실행되는 등 자주 실행됨
    • thread safe하게 카운트를 Atomic하게 증감해야 함
  • Method Dispatch (Static)
    • 컴파일러의 최적화, 메서드 인라이닝 가능: 컴파일 시점에 메서드 실제 코드 위치를 안다면 실행 중 찾는 과정 없이 그 주소로 점프 가능

메서드 인라이닝:

  • 컴파일 시점에 메소드 호출 부분에 메소드 내용을 붙여넣음 (효과가 있다고 판단되는 경우에만)
  • Call stack 오버헤드 줄임
    • CPU icache나 레지스터를 효율적으로 쓸 가능성 -컴파일러의 추가 최적화 가능
    • 최근 메소드들이 작으므로 더더욱 기회가 많음
    • 루프 안에서 불리는 경우 큰 효과
  • Method Dispatch (Dynamic)
    • Reference 시맨틱스에서의 다형성은 컴파일 타임에 어떤 클래스의 메서드인지 알 수 없으며 런타임에서만 파악 가능
    • 실제 Type을 컴파일 시점에서 알 수 없으므로 코드 주소를 런타임에 찾아야 함
    • 따라서 컴파일러가 최적화를 못하므로 속도 저하 요소
  • static dispatch로 강제하기
    • final, private를 쓰면 해당 메서드와 프로퍼티는 상속되지 않으므로 static하게 처리 가능
    • dynamic 사용 줄이기
    • Objc 연동 최소화
    • WMO(whole module optimization): 빌드 시에 모든 파일을 한번에 분석하여 static dispatch 변환 가능성 판단하여 최적화 하나 몹시 느리고 안정화가 안되므로 사용에 주의 필요


Swift 추상화 기법들과 성능

  • Class
    • class는 성능 상관 없이 레퍼런스 시맨틱스가 필요할 때 사용
    • final class는 class 보다 고성능
  • Struct
    • 참조 타입이 없는 struct는 고성능
    • 참조 타입이 있는 struct는 성능 저하: 내부 storage로 class 타입을 가지면 복사시 reference counting이 동작함
    • 따라서 값의 제한이 가능하면 enum 등의 Value type으로 변경하고 다수의 class들을 하나로 합쳐서 성능 개선


  • Protocol Type
    • 코드 없이 API만 정의함
    • Objective C의 protocol, Java의 Interface 매우 유사함
    • Value type인 struct에도 적용 가능: Value semantics에서의 다형성
    • 값 관리는 Existential Container로
    • Copy 동작: Value 타입이므로 값 전체가 복사됨
    • 3 words 이하 Copy: 단순히 새로운 Existential container에 전체가 복사됨
    • 3 words 이상 Copy: 새로운 Existential container 생성하고 값 전체가 새로운 Heap할당 후 복사됨

Existential Container:

  • protocol type의 실제 값을 넣고 관리하는 구조
  • 프로토콜을 통한 다형성을 구현하기 위한 목적으로 사용
  • 성능은 class 사용과 유사
    • 초기화 시 Heap 할당
    • Dyamic dispatch (class: V-Table / protocol: PWT)
  • Value Witness Table(VWT)
    • Existential container의 생성/해제를 담당하는 인터페이스


  • Generics Type
    • Generic 특수화: 컴파일러가 실행
    • 더 효과를 보려면 WMO(Whole Module Optimization) 이용
    • WMO 사용이 완전하지 않으므로 주의 필요
    • 정적 다형성: 컴파일 시점에 부르는 곳마다 타입이 정해져 있고 런타임에 바뀌지 않으므로 특수화가 가능함


정리

  • Swift의 성능
    • Objective-C에 비해 큰 향상이 있었으나 Value 타입과 Protocol 타입 등의 성격을 고려해야 함
    • 성능 최적화를 고려해야하는 경우의 예
      • 렌더링 관련 로직 등 반복적으로 매우 빈번히 불리는 경우
      • 서버 환경에서의 대용량 데이터 처리
  • 추상화 기법의 선택
    • Struct: 엔티티 등 Value 시맨틱이 맞는 부분
    • Class: Identity가 필요한 부분, 상속등의 OOP, Objective-C
    • Generics: 정적 다형성으로 가능한 경우
    • Protocol: 동적 다형성이 필요한 경우
  • 고려할 수 있는 성능 최적화 기법들
    • Struct에 클래스 타입의 Property가 많으면
      • enum, struct등 Value type으로 대체
      • Reference counting 줄임
    • Protocol Type을 쓸 때 대상이 큰 struct면
      • Indirect storage로 struct 구조 변경
      • Mutable해야하면 Copy-on-Write 구현
    • Dynamic method dispatch를 static하게
      • final, private의 생활화
      • dynamic 사용 최소화
      • Objc 연동 최소화 하기
      • 릴리즈 빌드에 WMO 옵션 적용 고려 (주의!)

참고 자료

  • WWDC 2016 Session 416: Understanding Swift Performance
  • WWDC 2015 Session 409: Optimizing Swift Performance
  • WWDC 2015 Session 414: Building Better Apps with Value Types in Swift

발표 슬라이드는 아래를 참고해주세요.

컨텐츠에 대하여

이 컨텐츠는 저자의 허가 하에 이곳에서 공유합니다.

유용하