iOS/WWDC

[WWDC 2016] Understanding Swift Performance - Struct, Class (1)

iOS_Assin 2019. 11. 4. 01:43

WWDC 2018 Understanding Swift Performance 를 정리한 포스트입니다.

더 자세한 내용을 원하시면 위 링크를 참조하시길 바랍니다.

 

1. [WWDC 2016] Understanding Swift Performance - Struct, Class (1)

2. [WWDC 2016] Understanding Swift Performance - Protocol (2)

2. [WWDC 2016] Understanding Swift Performance - Generic (3)

Dimensions of Performance

우리는 다양한 차원의 성능을 식별할 수 있습니다.

 

 

1. 클래스가 Heap or Stack 어느 곳에 할당되는 지?

2. 이 인스턴스를 전달할 때 참조 카운트 오버 헤드는 얼마나됩니까?

3. 이 인스턴스에서 메소드를 호출 할 때 정적 또는 동적으로 전달됩니까?

 

 

빠른 Swift 코드를 작성하려면 활용하지 않는 동적 및 런타임에 대한 비용을 피해야합니다.

 

 

 

더 나은 성능을 위해 서로 다른 Diments 사이에서 적절한 방법을 알아야 합니다. 

 

Allocation

Stack

함수 실행을 마치면 스택 포인터를 이 함수를 호출하기 전의 위치로 다시 증가 시켜서

메모리를 해제할 수 있습니다.

스택은 정적(static)이며 정수를 할당(memory pointer)하는 적은 비용이 필요합니다.

 

Heap

힙은 동적(dynamic)이며 스택과 달리 동적 Lifetime 을 가질 수 있습니다.

 

고급 데이터 구조가 필요합니다.

(Advanced data structure)


힙에 메모리를 할당하려면 실제로 힙 데이터 구조를 검색하여 적절한 크기의 사용되지 않는 블록을 찾아야합니다.(Search for unused block of memory to allocate)

 

할당을 해제하려면 해당 메모리를 다시 적절한 위치에 다시 삽입해야 합니다.
(Reinsert block of memory to deallocate)

 

여러 스레드가 동시에 힙에 메모리를 할당 할 수 있으므로 동기화 메커니즘이 필요합니다.
(Thread safety overhead)

 

Allocation Stack

 

 

함수가 실행되면서 point1,point2인스턴스를 위한 스택 공간을 할당하였습니다.

point는 구조체이기 때문에 xy 속성은 스택에 할당됩니다.

 

 

함수 실행이 완료되면 메모리가 해제됩니다.

 

 

 

 

Allocation Class

함수를 입력하면 스택에 메모리가 할당됩니다.

그러나 속성을 실제로 저장하는 대신 point1  point2에 대한 참조를 위해 메모리를 할당합니다.

 

 point1point2에 할당 할 때 point1struct 와 달리 classpoint가 복사되지 않습니다.
 대신 참조를 복사합니다.


 따라서 point1과 point2는 실제로 힙에서 동일한 정확한 포인트 인스턴스를 나타냅니다.

 

 Swift는 힙을 잠그고 사용되지 않은 블록을 적절한 위치로 retain 하기 위해 메모리를 할당 해제합니다.
 그런 다음 스택을 POP할 수 있습니다.

 

클래스는 힙 할당이 필요하기 때문에 클래스가 구조체보다 생성하는 것이 더 비쌉니다.

 

Modeling Techniques: Allocation

StringKey 로 선택하는 것은 적절하지 않습니다.

  1. 이름이 쉽게 중복될 수 있습니다.
  2. String은 실제로 문자의 내용을 힙에 간접적으로 저장합니다.

즉, 캐시 적중이 있더라도이 makeBalloon 함수를 호출 할 때마다 힙 할당이 발생합니다. 

 

let key = "\(color):\(orientation):\(tail)"

 

makeBalloon 함수를 호출 할 때 구조체를 생성하는 데 힙 할당이 필요하지 않으므로 할당 오버 헤드가 없습니다.
(스택에 할당되기 때문에)

 

 

Reference Counting

Class

 

Swift는 힙의 인스턴스에 대한 총 참조 수를 유지합니다.

 

해당 횟수가 0에 도달하면 Swift는 더 이상 힙에서이 인스턴스를 가리키고 있지 않고 할당된 메모리 해제를 합니다.

 

참조 카운팅 작업은 자주 수행되며 실제로 정수를 늘리거나 줄이는 것 외에 많은 작업이 있습니다.

(There’s more to reference counting than incrementing, decrementing)

 

증가 및 감소를 실행하기 위해 몇 가지 수준의 간접적인(Indirection) 지시가 있습니다.

 

여러 스레드의 힙 인스턴스에 동시에 참조를 추가하거나 제거 할 수 있기 때문에 스레드 안전성이 필요합니다.

(Thread safety overhead)

 

Swift 는 refCount 를 위한 Generated Code 를 생성합니다.

  • Retain은 기준 카운트를 원자적으로 증가
  • Release는 기준 카운트를 원자적으로 감소

 

lass Point {
 var refCount: Int
 var x, y: Double
 func draw() { … }
}
let point1 = Point(x: 0, y: 0)
let point2 = point1
retain(point2)
point2.x = 5

 

 

 

release(point1)

 

release(point2)

 

Struct

Struct 은 힙 할당이 없기 때문에 참조 카운트 오버헤드가 없습니다. 

 

UIFont Class 참조포인트 +1

String (위에서 언급 String 은 힙에 할당) 참조 카운트 +1

 


label2 사본을 만들때 UIFontString에 각각 다른 두 개의 참조를 추가합니다.

 

참조 카운트 오버헤드는 2배로 늘어났습니다. 

클래스는 힙에 할당되므로 Swift는 해당 힙 할당의 수명을 관리해야합니다.

 

 

Struct containing reference

 

구조체에 참조가 포함되어 있으면 참조 카운트 오버 헤드도 지불하게됩니다.

 


따라서 2개 이상의 참조가 있으면 클래스보다 더 많은 참조 계산 오버 헤드가 유지됩니다.

(Struct 2개 이상 참조 > 클래스)

 

 

실제 어플리케이션 예

 

 

구조체는 3번의 참조 카운트 오버헤드가 발생합니다. 

let fileURL: URL
let uuid: String
let mimeType: String

 

최적화해보자

 

UUIDFoundation 에 잘 정의되어 있는 struct 입니다.

그러므로 참조 카운트가 발생하지 않습니다.

 

 

 

MimeType 도 String 이므로 힙 할당이 되면서 참조 카운트 오버헤드가 발생합니다. 

 

enum MimeType : String {
 case jpeg = "image/jpeg"
 case png = "image/png"
 case gif = "image/gif"
}

 

MimeType 을 enum 열거형으로 바꾸면 참조 카운트 오버헤드가 발생하지 않습니다. 

 

 

 

uuidmimeType은 참조 횟수 또는 힙 할당이 필요하지 않기 때문에

거의 참조 횟수 오버 헤드를 지불하지 않습니다.

 

struct Attachment 에서 fileURL 만 참조 카운트를 지불하고 있습니다. 

 

 

Method Dispatch

정적 디스패치

Compile time에 실행할 구현을 결정

 

컴파일러가 실제로 어떤 구현이 실행 될지에 대한 가시성을 가질 수 있기 때문에

inline과 같은 것을 포함하여 코드를 매우 적극적으로 최적화 할 수 있습니다

 

동적 디스패치

Compile time으로 결정할 수는 없습니다.

Runtime time한개의 Pointer 로 실제로 구현을 찾습니다. 

따라서 자체적으로 동적 디스패치는 정적 디스패치보다 그렇게 비싸지 않습니다.

참조 카운트, 힙 할당, 스레드 동기화 오버 헤드는 없습니다.

 

하지만 동적 디스패치는 컴파일러의 가시성을 차단하므로 최적화 할 수 없습니다.

 

정적 디스패치 inline

아래 두가지 메소드는 모두 정적 디스패치입니다.

func draw() 

func drawAPoint(param:) 

 

컴파일러가 어떤 구현이 실행 될지 정확히 알고 있으므로 실제로 drawAPoint 디스패치를 ​​가져 와서 drawAPoint의 구현으로 대체 할 것입니다.

 

 

 

런타임에 Point.draw 함수를 실행하면 지점을 구성하고 구현을 실행할 수 있습니다.

 

 

동적 디스패치 예

Inheritance-Based Polymorphism

Drawable 수퍼 클래스를 사용하여 draw를 재정의하는 Point 클래스와 Line 클래스를 정의 할 수 있습니다.

 

 

Polymorphism Through Reference Semantics

Drawable Array를 만들 수 있습니다.

(배열에서 참조로 저장하기 때문에 크기가 모두 같습니다.)

 

따라서 컴파일러가 컴파일 타임에 실행할 올바른 구현인지 결정할 수 없습니다.

 

 

Polymorphism Through V-Table Dispatch

override draw를 호출 할 때 TypeV-Table을 탐색합니다. 

V-Table 에는 구현에 대한 포인터가 포함되어 있습니다.

즉, 컴파일러가 우리를 대신하여 수행중인 작업을 그리면 실제로 V-Table을 통해 실행하여 draw() 구현을 찾습니다.
그런 다음 실제 인스턴스를 implicit self-parameter로 전달합니다.

 

 

 

기본적으로 클래스는 동적 디스패치 메소드를 전달합니다.

final class

모든 클래스가 동적 디스패치를 ​​필요로하는 것은 아닙니다.

클래스를 서브 클래스로 만들지 않는 경우는 동적 디스패치를 ​​대신하여 정적 디스패치로 전환 할 수 있습니다.