WWDC16 - Understanding Swift Performance
Swift에는 다양한 Abstraction mechanism이 있고 다양항 일급 객체가 있다. 어떻게 알맞은 Abstraction Mechanism 선택할 수 있을까?
바로 각각 추상화 메케니즘이 모델링(성능)에 미치는 영향을 고려해야한다. 영향을 이해하려면 기본 구현을 이해하는 것이 중요하다 !


추상화 메케니즘을 선택할 때 질문해야하는 것
불필요한 오버헤드나 디스패치를 통해 성능을 떨어지는 문제가 발생할 수도 …

스택은 매우 간편한 메모리 공간. (스택의 끝을 가르키는 주소를 스택포인터라고 함 )
스택은 메모리 할당, 해제가 매우 빠르다. 바로 스택포인터를 활용하기 때문!
→ 이렇게 정수를 할당하기만 하면 되므로 간단하고 매우 빠름
스택 (빠름, 효율적, 정적) vs 힙(비효율적, 동적)
동적 생명주기로 메모리를 할당할 수 있는 메모리 공간
발생하는 cost
좀 더 복잡한 data structure
메모리 할당/해제 과정
→ 스택에 비해 더 많은 절차가 필요 (비용 발생 )
Thread safety overhead
여러 스레드가 힙에 메모리 할당 가능하므로 locking, synchronization 매커니즘을 사용하여 무결성을 지켜야 함
point1, point2 독립적인 인스턴스// Allocation
// Struct
struct Point {
var x, y: Double
func draw() { … }
}
let point1 = Point(x: 0, y: 0)
var point2 = point1
point2.x = 5
// use `point1`
// use `point2

스택에 메모리 할당
속성을 저장하는 것이 아닌 point1, point2에 대한 참조값을 저장
heap에 메모리 할당 (실제 값 속성 저장)
heap을 lock하고 데이터 구조에서 적절한 크기의 사용되지 않은 메모리 블록 검색
x, y를 각각 0으로 메모리 초기화하고 힙에 있는 해당 메모리 주소를 사용하여 point1 참조 초기화
// Allocation
// Class
class Point {
var x, y: Double
func draw() { … }
}
let point1 = Point(x: 0, y: 0)
var point2 = point1
point2.x = 5
// use `point1`
// use `point2

구조체(2개)와 달리 4개 words 메모리 공간 할당
저장공간 개수가 차이나는 이유 → 포인트가 클래스이기 때문 x, y 외에도 저장할 것 존재

point2가 point1 copy할 때 참조값만 복사하고 동일한 인스턴스를 참조한다. → 한 인스턴스에서 값이 변경되면 모두 공유, reference semantics



// Modeling Techniques: Allocation
enum Color { case blue, green, gray }
enum Orientation { case left, right }
enum Tail { case none, tail, bubble }
var cache = [String : UIImage]()
func makeBalloon(_ color: Color, orientation: Orientation, tail: Tail) -> UIImage {
let key = "\(color):\(orientation):\(tail)"
if let image = cache[key] {
retrun image
}
...
}
key : string type으로 되어 있음키를 struct으로 구성하자
struct Attributes {
var color: Color
var orientation: Orientation
var tail: Tail
}
Swift에서 struct은 최상위(first class) 타입이기 때문에 dictionary에서 key로 사용 가능
func makeBalloon(_ color: Color, orientation: Orientation, tail: Tail) -> UIImage {
let key = Attributes(color: color, orientation: orientation, tail: tail)
if let image = cache[key] {
retrun image
}
...
}
→ makeBallon 함수 호출할 때 힙 할당하지 않으므로 (스택 할당) 할당 오버해드 없음.
더 안전하고 빠른 방법
Swift는 힙의 모든 인스턴스에 대한 총 참조 수 유지, 참조 카운트 증가(retain), 감소(release) 작업 수행하고 참조 카운트가 0일 때 인스턴스 해제
→ 참조 카운팅은 매우 빈번한 작업이며 정수를 늘리고 줄이는 것보다 더 많은 작업
Reference Counting에 고려해야할 것
간접 참조(Indirection)
Thread safty overhead → 동시에 참조 추가, 해제 막아야 함
참조 계산 빈도로 인해 이 비용 추가될 수 있음

point1, point2 인스턴스가 할당되고 해제되는 과정



아래 그림과 같이 포인트 인스턴스에 대한 참조가 더이상 없으면 Swift는 힙을 잠그고 해당 메모리 블록을 리턴하는 것이 안전하다고 판단




일반적으로 struct 인스턴스는 힙에 할당되지 않으므로 reference counting이 일어나지 않으므로 overhead가 존재하지 않는다.
But, complicated struct의 경우는?
// Struct containing references
struct Label {
var text: String
var font: UIFont
func draw() { … }
}
let label1 = Label(text: "Hi", font: font)
let label2 = label1
// use `label1`
// use `label2`
UIFont, String 타입인 text가 포함된 Label 구조체
→ Label이 실제로 클래스가 갖는 reference counting overhead 2배 발생


참조 카운팅은 자주 발생하고 thread safety를 위한 원자성 때문에 사소하지 않다.
클래스 경우 reference counting overhead 발생

구조체 경우 reference counting overhead 발생하지 않음

참조가 포함된 구조체의 경우, 포함된 참조 수와 비례하여 reference counting overhead 발생

참조가 2개 이상인 구조체인 경우 클래스보다 overhead가 더 많이 발생

첨부파일을 보내는데 필요한 struct

많은 Reference counting overhead 발견: 성능 개선 여지
// Modeling Techniques: Reference Counting
struct Attachment {
let fileURL: URL
let uuid: String
let mimeType: String
init?(fileURL: URL, uuid: String, mimeType: String) {
guard mimeType.isMimeType
else { return nil }
self.fileURL = fileURL
self.uuid = uuid
self.mimeType = mimeType
}
}
uuid: 무작위로 생성된 128 bit이다.
이를 String 타입에서 Foundation의 uuid에 대한 새로운 value type인 UUID 사용
→ 문자열이었던 필드에 대한 오버헤드 제거, 또한 UUID만 넣을 수 있어 안전성 up
mimeType를 고정된 케이스를 담는 타입 사용
Enum 타입으로 변경
→ 참조 카운팅 오버헤드 제거, 안전성 up
enum MimeType : String {
case jpeg = "image/jpeg"
case png = "image/png"
case gif = "image/gif"
}

컴파일 시점에 실행할 코드 결정 가능한 호출한다.
컴파일러가 어떤 구현이 실행될 것인지 확인할 수 있어 인라인을 포함한 여러 최적화 작업을 수행할 수 있다.
런타임 시점에 구현을 찾아본 다음 해당 구현으로 이동하여 실행한다.
→ 참조카운팅과 같이 쓰레드와 관련된 오버헤드는 존재하지 않지만 컴파일러의 가시성을 차단하므로 정적 디스패치와 다르게 동적 디스패치의 경우 추론할 수 없어 최적화를 수행할 수 없습니다.
코드 설명
(0,0) Point를 생성하고 해당 점을 drawAPoint에 전달
// Struct (inlining)
struct Point {
var x, y: Double
func draw() {
// Point.draw implementation
}
}
func drawAPoint(_ param: Point) {
param.draw()
}
let point = Point(x: 0, y: 0)
drawAPoint(point)
drawAPoint 함수와 point.draw 메서드는 모두 정적으로 전달 → 컴파일러가 어떤 구현을 실행해야하는지 정확히 알고 있음
따라서 이를 아래와 같이 대체가 가능하다.
point.draw() // drawAPoint(point)를 대체한 것
다시 한번 아래와 같이, drawAPoint 디스패치를 가져와 drawAPoint 구현으로 대체한 것
// Point.draw implementation
이를 통해, 2가지 정적 디스패치의 오버헤드와 호출 스택 관련 설정 및 해제가 필요하지 않음
→ 이런 과정 때문에 정적 디스패치가 동적 보다 훨씬 빠르다.
컴파일러는 호출 스택 오버헤드 없이 단일 구현처럼 정적 메서드 디스패치 체인 축소 가능
동적 디스패치의 강력한 기능 중 하나, 다형성!
코드 설명: 다형적으로 Drawable 배열을 생성하는 프로그램
Drawable 배열이고 모두 클래스이므로 배열에 참조로 저장하므로 크기가 모두 동일
각 항목을 검토할 때, 해당 항목에 대해 draw 호출
컴파일러가 실행할 구현이 컴파일 타임에 무엇인지 결정할 수 없다. → Point가 될 수도, Line이 될 수도 있기 때문 (전혀 다른 코드 구현)

그럼 어느 것을 호출할지 결정하는가?
클래스 인스턴스를 저장할 때 정적 메모리에 저장되는 클래스 타입 정보에 대한 포인터를 필드에 추가하여 저장한다. 따라서 draw를 호출할 때, 컴파일러가 실행할 올바른 구현을 가리키는 포인터를 통해 가상 메서드 테이블(V-Table)이라는 항목을 조회한다.
→ v-table을 조회하여 올바른 구현을 찾아 실행한다.

class는 dynamic dispatch 방식이다. 그 자체로 큰 차이는 없지만 메서드 체이닝, 인라인과 같은 최적화를 방해할 수 있다.

모든 클래스에 대해서 동적 디스패치가 필요한 것은 아님. 클래스를 하위클래스로 분류할 의도가 없을 경우 final class로 선언한다. 이러면 컴파일러는 해당 메서드를 정적 디스패치한다.
클래스를 서브클래싱하지 않을 것임을 추론하고 증명된 경우 또한 정적 디스패치한다.


필요하지 않은 역동성을 위해 비용을 지불한다면 성능 저하
많은 비용 절감을 위해서는 구조체를 사용해야하는데, 구조체를 사용하여 다형성 코드를 작성하는 방법은 무엇입니까?
답은 Protocol Oriented Programming
다음 편에 계속…..