Struct

  • stack 영역을 사용한다. Heap을 쓰지 않으므로 Reference couting 이 필요하지 않음
  • 다른 변수에 할당한 경우 값 전체가 복사가 되며, 할당된 값을 변경하더라도 기존의 값이 변경되지 않는다.
    • 따라서 Thread 의 의도치 않은 공유로부터 안전하다.
  • Identity가 아닌 Value 자체가 중요하다.
    • 그렇다면 성능은?
      • 내부 데이터(property)가 만약 Heap 과 혼용되는 경우는 결국 값의 복사 + 레퍼런스도 카피가 된다.
      • 이때 말하는 내부 데이터를 예를 들자면 string, array, set, dictionary
    • 또한 COW(Copy On Write) 를 통해서 불필요한 작업을 줄임
    • 만약 immutable 하게 만들면 어떤가?
      • 실행할때 새로운 객체를 만드는 것은 비효율적이다. mutable + value type의 경우 값을 변경하면 되는데 immutable + reference 의 경우 객체를 생성해서 넘겨야하는 경우가 있다면. 변경하지 않아도 되는 값 마저 다 생성해서 넘기는 것이 맞는 것인가? 에 대한 생각이 든다.

Class

  • 언제쓰는가? Value 자체보다 Identity가 더 중요한 경우가 있다.
  • 대표적으로 UIView 를 생각해보자.

성능에 영향을 미치는 3가지

  1. stack vs heap
  2. reference counting
  3. method dispatch > static vs dynamic

stack vs heap - Heap 할당 줄이기

  • Heap 할당이 성능에 영향을 미치는 요소는 다음이 포함되어 있다.
    • 비워진 공간을 찾고 원하는 만큼 할당시켜야 하며, 이를 관리하는 것이 복잡하다.
    • 동시에 thread safe 해야하기 때문에 lock synchroniztion 동작등이 성능에 큰 영향을 미친다.
  • 예를 들어 Dictionary 의 key값을 설정할때 string 을 사용하지 말고 struct를 활용해보자
    • string을 사용하는 경우 변수에 할당할때 heap 에 저장하게 된다. 만약 loop 내부에서 돌게 된다면???
    • 따라서 value type인 struct를 써보자 이때 hashable 만 채택하면 된다.

reference counting

  • 정말 자주 사용된다. 또한 성능에 영향을 미치는 큰 이유중 하나가 thread safe 해야하기 때문
  • 따라서 최대한 줄이는 것이 좋다. 위와 마찬가지로 loop 내부에 있다면…?? value sementics에 비해 성능이 좋지 못할 것

Method Dispatch

  • static
  • 컴파일 시점에 메소드의 실제 코드 위치를 안다면 실행중 찾는 과정 없이도 해당 코드 주소로 점프가 가능함.
  • 컴파일러 최적화 및 메소드 인라이닝 가능
    • 메소드 인라이닝이란, 컴파일 시점에 메소드 호출부도 같이 넣은것 처럼 동작하는 것
    • 이로 인해 call stack overhead 가 줄어들고 루프 안에서 호출하는 경우는 큰 효과를 누릴 수 있음

  • dynamic
  • static 과 다르게 컴파일 시점에는 어디에 있는 메소드가 호출되는지 모른다. 따라서 런타임시 이를 찾아내야함. 이로 인해 컴파일러 최적화가 어려움.
  • 따라서 static 으로 강제하기 위해서 final, private 의 사용을 습관화 해야함
    • Xcode : Whole Module Optimization 옵션 사용할 것

추상화 기법들

  • Class 는 성능 안좋은 것은 다 가지고 있다.
    • 그나마 개선이 가능한 부분은 final 을 통한 static dispatch
  • Struct 를 사용하는데 내부에 Reference Sementics 가 많다면??
    • 값을 복사한다고 가정했을 때 내부의 reference Type 만큼 reference counting 이 일어나게 됨. 이는 성능에 영향을 미친다.
    • 참고로 String, Array, Dictionary 도 Struct 이지만 내부적으로는 reference 를 사용하고 있다.
    • 따라서 줄이기 위해서 enum 등을 사용하고, 가능하다면 reference type들을 묶을 수 있는 방법을 생각해보자.
  • Protocol 은 Value Type에서도 다형성을 구현할 수 있는 큰 장점이 있다.
    • 의문점 1 : 어떤 타입이 들어갈지 모르는데 사이즈를 어떻게 계산해 놓는가?
      • 일정 크기를 넘어가면 내부적으로 heap을 사용하고, 실제로는 주소만 갖고 있다. 프로토콜의 실제 값을 관리하는 Existential Container를 통해 이뤄진다.
      • 이렇게 일정 크기를 판단하고 Existential Container를 관리하는 주체는 VWT ( Value Witness Table ) 라고 한다. 이는 각 프로토콜 마다 존재한다.
    • 의문점 2 : 어떻게 적절한 함수를 호출하는가?
      • PWT ( Protocol Witness Table )이 있다. V-table과 유사함.
      • 위에서 설명한 VWT, PWT 는 Extential Containor 내부에서 참조할 수 있다.
      • 다시한번 볼것 VWT, PWT 차이점 등등
    • 만약 큰 값을 가진 Protocol 을 복사한다면?
      • 위에서 말한것 처럼 Heap을 사용하는 protocol을 복사하게 되면 heap을 또 사용해야한다. 즉, 복사를 할때마다 heap 을 할당해야함. 이는 비 효율적
      • 이는 COW ( Copy On Write ) 를 통해서 개선하게 되었다.
  • Generic
    • protocol 과 비슷하게 VWT, PWT 로 구현이 되어있다.
    • Generic 특수화 : 여러 타입을 대응하려고 Generic을 만들었지만, 만약에 성능을 생각해서 각 타입으로 나눠서 쓴다면 메소드 인라이닝이 가능해져서 성능이 좋아질 것이다. > 성능을 생각하면 Generic을 쓰면 안되는가?? > WMO가 이를 대신 해준다.
    • 이런 특수화는 정적 다형성(Static Polymorphism) 덕분에 가능하다. > 컴파일 시점에 부르는 곳 마다 타입이 정해져 있고, 런타임시 바뀌지 않으며, 특수화가 가능하다.
Reference

https://www.youtube.com/watch?v=z1Gf6EosaUQ

  • 발표자님께서 참고하신 WWDC
    • WWDC 2016 : S416 : Understanding Swift Performance
    • WWDC 2015 : S409 : Optimizing Swift Performance
    • WWDC 2015 : S414 : Building Better Apps With Value Types in Swift

'iOS & Swift' 카테고리의 다른 글

UICollectionViewCompositionalLayout  (0) 2022.10.31
UINib Basic  (3) 2021.10.04
Property Wrapper  (0) 2021.06.07
Apple 공식 Guide & Reference 모음  (0) 2021.05.21
Escaping Closure  (0) 2021.05.16

iOS 개발을 하다보면 access control 에 대해서 관심이 생길때가 있다. final 혹은 private 을 붙히면 성능에 더 좋다라고 막연히 듣기만 했는데 이번에 한번 그 이유를 정리하고 기록을 남기고자 한다.

Increasing Performance Reducing Dynamic Dispatch

프로그램의 성능을 높히기 위해서 Dynamic Dispatch 를 줄이는 방법이 있다. 그렇다면 Dynamic Dispatch란 무엇이고 이를 줄이면서 어떻게 성능이 개선되는지 한번 알아보자

Dispatch

먼저 Dispatch 의 개념부터 알아보도록 하자. Dispatch는 어떤 메소드를 호출할 것인가를 결정하여 그것을 실행하는 과정이다. Dispatch 방식은 Static Dispatch, Dynamic Dispatch로 두가지가 있다.

Static Dispatch

컴파일 시점에 어떤 메소드가 사용될지 명확하게 결정되는 것을 Static Dispatch 라고 한다.

Dynamic Dispatch

런타임 시점에 어떤 메소드가 실행될지 결정되는 것, 즉 컴파일 시점에서는 어떤 함수가 실행될 지 모른다. Swift 에서는 class 마다 vtable을 갖고 있고 이를 참조하면서 함수가 호출되기 때문에 이에 따른 overhead가 발생하게 된다.

Static VS Dynamic Dispatch

위의 설명만 들으면 이해가 잘 안될수도 있다. 아래 예시를 살펴보자

// Static Dispatch
struct HelloStruct {
  func printHello() {
    print("hello")
  }
}
let helloStruct = HelloStruct()
helloStruct.printHello()

struct 는 Value Type 이다. 즉, 다른곳에서 해당 값의 레퍼런스를 가지고 있을 필요가 없으며 상속도 되지 않는다. 따라서 컴파일러의 시각에서 생각해보면 helloStruct.printHello() 는 HelloStruct의 printHello()가 호출된다는 것이 명확해진다. 즉 다른 방법이 없다. 이때 Static Dispatch 방식으로 함수가 호출되는 것이다.

// Dynamic Dispatch
class HelloClass {
  func printHello() {
    print("hello")
  }
}

class HelloOtherClass: HelloClass { }

let helloClass: HelloClass = HelloOtherClass()
helloClass.printHello()

class 는 Reference Type 이며 Struct 와 다르게 상속이 가능해진다. 다른 곳에서도 함수를 호출할 가능성이 존재한다는 것이다. 위의 helloClass 변수를 보자 타입은 HelloClass 지만 실제 값은 HelloOtherClass 의 인스턴스이다.

실제로 함수가 override 되었는지 안 되었는지는 중요하지 않다. 즉, 컴파일러가 봤을 때 printHello()가 상속으로 인해서 다른 클래스에서 호출될수도 있겠구나 그러면 바로 HelloClass 의 printHello를 직접 접근하지 말고 참조 형식으로 둬야 겠다 라는 방향으로 컴파일을 한다는 것이다. 따라서 특별하게 지정하지 않는 이상 해당 함수는 Dynamic Dispatch 로 인해서 불리게 된다.

Increasing Performance by Reducing Dynamic Dispatch

이제 본문으로 돌아오면 스위프트도 다른 언어와 마찬가지로 여러 method 혹은 properties 를 슈퍼클래스로부터 override 할 수 있다. 이는 다시말해서 프로그램이 런타임시에 indirect call & indirect access 를 통해서 어떤 method 혹은 property를 호출하는지 정하는 것을 말한다. 이를 다이나믹 디스패치라고 말하고 indirect usage 를 사용할 때 마다 overhead 가 발생하게 된다. 따라서 성능이 중요한 코드에서는 이런 overhead 는 바람직하지 못하다. 이런 역동성을 제거하는 방식에는 크게 3가지가 있고 예시와 함께 이를 설명하겠다.

먼저 역동성을 제거하지 않은 예시를 살펴보자

class ParticleModel {
    var point = ( 0.0, 0.0 )
    var velocity = 100.0

    func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
        point = newPoint
        velocity = newVelocity
    }

    func update(newP: (Double, Double), newV: Double) {
        updatePoint(newP, newVelocity: newV)
    }
}

var p = ParticleModel()
for i in stride(from: 0.0, through: 360, by: 1.0) {
    p.update((i * sin(i), i), newV:i*1000)
}

위의 코드가 실행이 된다면 컴파일러는 dynamic dispatch call을 방출하는데 순서는 다음과 같다.

  1. Call update on p.
  2. Call updatePoint on p.
  3. Get the property point tuple of p.
  4. Get the property velocity of p.

ParticleModel의 Method나 Property 를 override 해서 새로운 구현을 하기 위해서는 이런 dynamic dispatch는 필수적이다 ( 즉, 반대는 필요 없다는 말 ). Swift에서는 Dynamic Dispatch를 구현할 때 Method Table에서 해당 function을 찾고 indirect call을 호출한다. 이는 direct call보다 느리며 compiler 의 최적화 방향에도 좋지 않은 영향을 미친다. 따라서 성능을 높히기 위해서는 이런 방식을 지양해야 한다.

Use Final

Use final when you know that a declaration does not need to be overridden.

해당 키워드는 클래스의 method 혹은 property 에 override 를 제한한다. 따라서 컴파일러는 이로 인한 indirect call, access 를 무시할 수 있다.

class ParticleModel {
    final var point = ( x: 0.0, y: 0.0 )
    final var velocity = 100.0

    final func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
        point = newPoint
        velocity = newVelocity
    }

    func update(newP: (Double, Double), newV: Double) {
        updatePoint(newP, newVelocity: newV)
    }
}        

final keyword를 붙힌 point, velocity, updatePoint() 를 살펴보자. 이제 point 와 velocity 의 경우 직접적으로 객체의 stored property 에 접근할 수 있게되며, updatePoint() 또한 direct function call로 호출할 수 있게 된다. 따라서 overhead 가 줄어들고 성능이 향상된다. 하지만 여전히 update()는 dynamic dispatch를 통해 indirect function call 로 호출이 되며 overhead 가 발생하고, 성능이 안좋아졌지만 서브클래스에서 override 가 가능하다.

final class ParticleModel {
    var point = ( x: 0.0, y: 0.0 )
    var velocity = 100.0
    // ...
}

final은 이렇게 class 앞에도 붙힐 수 있으며 이때는 클래스에서 구현된 모든 property 와 method는 direct call로 불리게 되고 override 가 불가능하다.

applying the private keyword

Infer final on declarations referenced in one file by applying the private keyword.

정의할 때 private keyword를 사용하는 것은 참조할 수 있는 곳을 현재 파일로 제한한다는 뜻이다. 따라서 컴파일러는 private 키워드가 참조될 수 있는 곳에서 잠재적으로 override 가 될 수 있는지 없는지를 판단한다. 이때 따로 override 하는 곳이 없다면 스스로 final 키워드를 추론하고 indirect call & access 을 제거한다.

class ParticleModel {
    private var point = ( x: 0.0, y: 0.0 )
    private var velocity = 100.0

    private func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
        point = newPoint
        velocity = newVelocity
    }

    func update(newP: (Double, Double), newV: Double) {
        updatePoint(newP, newVelocity: newV)
    }
}

이 또한 위의 final 과 마찬가지로 클래스 앞에 private Keyword를 붙이게 되면 내부의 모든 property 그리고 method에도 private keyword가 붙힌 것으로 간주된다.

private class ParticleModel {
    var point = ( x: 0.0, y: 0.0 )
    var velocity = 100.0
    // ...
}

Whole Module Optimization

Use Whole Module Optimization to infer final on internal declarations.

internal access level(default 접근 제한자)은 정의된 모듈 내에서만 접근이 가능하다. 기본적으로 swift complier 는 모듈별로 컴파일을 하기 때문인데. 컴파일러는 기본적으로 internal access level에 대해서 서로 다른 파일에서 override 되었는지 확인이 불가능하다. 만약 whole module optimization 을 사용한다면 모든 모듈을 한번에 compile을 하게 된다. 따라서 이때 internal level 에 대해서 override가 되는지 추론을 할 수있게 되고 그렇지 않은 경우 내부적으로 final을 붙힌다. ( 따라서 direct call을 하게 된다. ) 아래 예시를 살펴보자

public class ParticleModel {
    var point = ( x: 0.0, y: 0.0 )
    var velocity = 100.0

    func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
        point = newPoint
        velocity = newVelocity
    }

    public func update(newP: (Double, Double), newV: Double) {
        updatePoint(newP, newVelocity: newV)
    }
}

var p = ParticleModel()
for i in stride(from: 0.0, through: times, by: 1.0) {
    p.update((i * sin(i), i), newV:i*1000)
}

이때 whole module Optimization 을 키게 된다면 point, velocity, updatePoint() 에 대해서 자동으로 final을 추론하게 되고 direct call로 호출할 수 있게 되는것이다.

참고로 Xcode 8 부터 Whole Module Optimization은 release 할 때 켜져 있습니다. 프로젝트파일에서 build setting - Compilation Mode 를 확인하면 release 에서 whole module을 확인할 수 있습니다.

Reference

wiki - 동적 디스패치

Swift의 Dispatch 규칙

Apple Blog - Increasing Performance Reducing Dynamic Dispatch

Swift.org - Whole-Module Optimization

'iOS & Swift' 카테고리의 다른 글

SOLID Principle  (0) 2021.04.12
Responder Chain ( 앱은 유저의 인터렉션을 어떻게 처리하는가 ? )  (0) 2021.03.18
Encoding 정복하기  (0) 2020.11.23
Decoding 정복하기  (2) 2020.11.14
Codable  (0) 2020.10.25

+ Recent posts