안녕하세요. KataRN입니다.
오늘은 WWDC 2016 - Understanding Swift Performance에 대해 공부한 내용에 대해 적어보려고합니다.
영상주소 - WWDC 2016 - Understanding Swift Performance
자료주소 - Presentation Slides (PDF)
참고 정리 블로그1 - https://levenshtein.tistory.com/508
참고 정리 블로그2 - https://zeddios.tistory.com/596
제가 정리한것보다는 위의 블로그 혹은 직접 공부하시는걸 권해드립니다.
세션의 주제
Swift Performance 이해??
추상화를 빌딩하고 추상화 메커니즘을 선택할때 3가지 고민이 필요하다.
1. 인스턴스를 생성할 때 스택/힙 중 어느 곳에 Allocation되는지
2. 인스턴스를 전달할 때 Reference Counting 오버헤드가 얼마나 발생하는지
3. 메서드를 호출할 때 Method Dispatch의 방식은 어느 것을 따르는 지
내용 목차
1. Allocation
2. Reference Counting
3. Method Dispatch
4. Protocol Types
5. Generic Code
Allocation
- Swift 는 사용자를 대신하여 자동으로 메모리를 할당 및 할당 해제한다.
- 스택은 정말 간단한 데이터 구조이다. 스택의 끝으로 push하고 스택의 끝에서 pop할 수 있다.
- 스택 끝에서만 추가하거나 제거할 수 있기 때문에, 스택 끝에 포인터를 유지함으로써 스택을 구현하거나 푸시 및 팝을 구현할 수 있다.
- 그리고 스택 끝에 있는 포인터를 스택 포인터라고 부른다.
- 함수를 호출할 때 공간을 만들기 위해 스택 포인터를 약간 감소시키는 것만 으로 필요한 메모리를 할당할 수 있다.
- 그리고 함수 실행이 끝나면 스택 포인터를 이 함수를 호출하기 전의 위치로 다시 증가시켜 메모리 할당을 간단하게 해제할 수 있다.
- 이 스택 할당은 정말 빠르다.
- 힙은 스택보다 동적이지만 효율성이 떨어진다 .
- 힙은 동적 lifetime으로 메모리를 할당할 수 있지만 스택은 이것이 불가능하다.
- 하지만 이를 구현하기 위해서는 더 고급 자료 구조가 필요하다.
- 힙에 메모리를 할당 하려면 적절한 크기의 '사용되지 않은 블록'을 찾아야 한다.
- 그리고 작업이 끝난 뒤 할당을 해제 하려면 해당 메모리를 적절한 위치 에 다시 삽입해야 한다.
- 힙에는 스택보다 더 많은 것이 관련되어 있다.
- 여러 스레드가 동시에 힙에 메모리를 할당할 수 있으므로 힙은 lock 또는 기타 동기화 메커니즘을 사용하여 무결성을 보호해야 한다. 그리고 이것은 꽤 큰 비용이 된다.
내용을 정리하면
- Stack이 Heap보다 빠르다.
- Class는 Heap할당이 필요하기 때문에 더 많은 비용이 필요하다.
- reference sementi가 발생할 수 있다.(의도하지 않은 상태 공유로 이어질 수 있다.)
이를 적용 하여 일부 Swift 코드의 성능을 향상시키는 방법을 살펴보자.
여기서 발표자는 2가지문제를 이야기한다.
1. 안전성
- 해당 키에 강아지 이름을 쉽게 넣을 수 있다는 것이다. 그래서 안전하지 않다고한다. 흠... 무슨뜻인지 잘 모르겠다...
2. String은 실제로 힙에 간접적으로 해당 문자의 내용을 저장한다.
속도개선을 위해 딕셔너리를 만들었지만 캐시가 hit하든 안하든 무조건 힙 할당이 발생한다.
String은 구조체이지만 실제로 힙에 내용을 저장한다고한다.
이유는 가변적이기 때문이라고한다. 내용이 늘어날수도 줄어들수도 있기 때문에 Heap에 저장해둔다는 것이다.
(그렇다면 왜 구조체로 만들었을까?..)
이렇게 바꿔주면 캐시가 hit가 있는 경우 Attribute와 같은 구조체를 구성하는데 힙 할당이 필요하지 않다.
-> 스택에 할당된다.
-> 안전하고 빠르다.
(키값이 String에서 구조체로 바뀌었기 때문에 힙할당이 안되는것 같아 보인다.)
Reference Counting
ARC는 참조 타입의 인스턴스가 참조될 때마다 Reference Count를 증가시키고 참조가 해제되면 Reference Count를 감소시켜서 카운트가 0이 되면 인스턴스를 메모리에서 할당 해제시킨다.
이 과정은 원자적(atomically)으로 일어나기 때문에 쓰레드 안정성이 보장된다.
-> 참조 카운팅 작업의 빈도로 인해 이 비용이 증가될 수 있다.
왼쪽 그림을 보면 Class는 참조타입이기 때문에 point2 = point1 로 하는 순간 Point1의 주소값을 복사한다.
따라서 ARC에 의해 카운트가 1 증가하고 이후에 감소한다.
오른쪽 그림을 보면 Struct는 애초부터 복사를 안하기때문에 증가도 감소도 안한다. 참조 카운팅 작업이 없었으니 비용이 덜들었다.
그렇다면 Struct는 무조건 좋은것일까?
아래 그림은 좀 더 복잡한 구조체이다.
1. Sturct Label 안에 있는 text는 String이다.
-> 실제 해당 문자의 내용을 힙에 저장한다
-> 참조 카운트 1 증가
2. Struct Label 안에 있는 font는 Class이다.
-> 참조 카운트 1증가
3. 복사본을 만들면서 2배가 되었다.
-> 참조카운트 2 증가
Struct지만 참조 카운트가 4 증가하게 된것이다.
Alocation 신경쓰다가 Reference Counting가 많아졌다.
-> 따라서 참조가 두 개 이상인 경우 클래스보다 참조 계산 오버헤드가 더 많이 들게 된다.
예제를 보도록 합시다.
위의 그림을 보면 Attachment 구조체안에서 3번의 참조가 일어난다.
어떻게 개선할까?
1. uuid의 String -> UUID 타입변경
- UUID는 Struct이며 128비트 무작위로 생성된 식별자이다. 그렇기 때문에 참조 카운팅 오버헤드(처리시간)를 제거한다.
- (2016년 보고있는 세션에서 UUID를 최초로 소개하였다고한다.)
2. mimeType의 String -> enum MimeTyepe 타입변경
- 같은 String이지만 enum에서의 String은 가변적이지 않고 값이 고정되어있기 때문에 힙에 할당되지 않는다.
그림과같이 3번 지불하던것을 1번 지불했다.
개이득....
Method Dispatch
1. Static Dispatch
- 컴파일 타임에 실행할 구현(implementation)을 결정할 수 있는 경우 이를 정적 디스패치라고 한다.
- 이 경우 런타임에 올바른 구현으로 바로 이동할 수 있다.
- 그리고 이것은 컴파일러가 실제로 어떤 구현이 실행될 것인지에 대한 가시성을 가질 수 있기 때문에 정말 멋지다.
- 인라인과 같은 것을 포함하여 이 코드를 매우 적극적으로 최적화 할 수 있다.
- 이것은 동적 디스패치와 대조된다.
2. Dynamic Dispatch
- 동적 디스패치는 어떤 구현으로 이동할지 직접 컴파일 시간에 결정할 수 없다.
- 따라서 런타임에 실제로 구현을 찾은 다음 바로 실행한다.
- 동적 디스패치 자체는 정적 디스패치보다 비용이 많이 들지는 않는다.
- 여기에는 한 가지 수준의 간접 참조만 있을 뿐이다.
- 참조 카운팅 및 힙 할당과 같은 스레드 동기화 오버헤드는 없다.
- 컴파일러는 정적 디스패치에 대해 모든 멋진 최적화(inlining과 optimization)를 수행할 수 있지만 동적 디스패치는 컴파일러의 가시성을 차단 하므로 최적화를 할 수 없다.
inlining
컴파일러가 메서드의 호출을 메서드의 바디 부분으로 교체하는 작업이다.
메서드를 호출할 때 메서드 종료 이후 돌아올 곳을 기억하거나 스택에 전달인자를 푸쉬하는 등의 오버헤드가 줄어들기 때문에 의미 있는 성능 향상을 줄 수 있다.
왼쪽이 일반적인 코드이다.
마지막줄에 있는 drawoPoint(point)를 호출하면 param.draw()를 호출하고 다시 Point안의 draw()를 호출한다.
이것은 결국 오른쪽에 있는것과 같다.
inlining을 통해 컴파일러가 최적화되고 스택 포인터의 오버헤드를 줄여준다.
그럼 또 Static Dispatch가 무조건 좋아보여서 Dynamic Dispatch가 쓸모없어 보이지만...
이유가 있다... 다형성을 위해서다.
위와 같은 코드가 있다.
마지막줄 d.draw()를 호출하면 Point의 draw()인지 Line의 draw()인지 알 수가 없을것이다.
그렇기 때문에 위에서처럼 컴파일러는 이를 알기 위해 클래스에 type이라는 필드를 추가하는데,
이 필드는 클래스의 타입 정보에 대한 포인터다.
인스턴스를 생성할 때 만들어진 파란색 메모리 부분은 Type 포인터와 Reference Count다.
이는 포인터를 따라서 Virtual Method Table (= vtable) 이라는 곳으로 이동한다. (우상단 테이블)
d.draw를 컴파일러가 우리를 대신하여 수행하는 작업으로 변경하면 실제로 실행할 올바른 draw 구현을 찾기 위해 virtual method table을 검색하는 것을 볼 수 있다.
이 테이블에는 안에는 실행해야 할 메서드들에 대한 포인터가 있고 따라가면 정말 우리가 실행해야 할 메서드를 찾을 수 있다.
- 클래스는 기본적으로 메서드를 동적으로 dispatch한다.
- 이것은 그 자체로 큰 차이를 만들지 않지만 메서드 체인 및 기타 사항과 관련하여 인라인과 같은 최적화를 방지 할 수 있고 합산될 수 있다.
- 그러나 모든 클래스에 동적 디스패치가 필요한 것은 아니다.
- 클래스를 하위 클래스로 만들 의도가 없다면 final로 표시하여 팀원과 미래의 나에게 그것이 의도한 것임을 전달할 수 있다.
- 컴파일러는 이것을 선택하고 해당 메서드를 정적으로 디스패치 할 것이다.
- 또한 컴파일러가 앱에서 클래스를 서브클래싱하지 않을 것임을 추론하고 증명할 수 있다면 기회에 따라 이러한 동적 디스패치를 사용자를 대신해 정적 디스패치로 전환한다.
이제 어떻게 해야하는가
- 그렇다면 우리는 이제 어떻게 해야하는가?
- 이 강연의 전반부에서 자기 자신에게 던져야 하는 질문이 있었다.
- Swift 코드를 읽고 작성할 때마다 "이 인스턴스가 스택에 할당될 것입니까 아니면 힙에 할당될 것입니까?"를 생각해야 한다.
- 이 인스턴스를 전달할 때 오버헤드가 포함된 참조가 얼마나 발생됩니까?
- 이 인스턴스에서 메서드를 호출하면 정적 또는 동적으로 디스패치됩니까?
- 필요하지 않은 dynamism(역동성)을 위해 비용을 지불한다면 성능이 저하될 것이다.
- 그리고 Swift를 처음 사용하거나 Objective C 에서 Swift 로 이식된 코드 기반에서 작업 하는 경우 현재보다 구조체를 더 많이 활용할 수 있다.
- 예제에서 본 것처럼 문자열 대신 구조체를 사용했던 것들과 같이.
- 그러나 한 가지 질문은 "구조체를 사용하여 다형성 코드를 작성 하는 방법은 무엇입니까?"이다.
- 우리는 아직 그것을 보지 못했다.
- 답은 프로토콜 지향 프로그래밍이다.
응? 이건 프로토콜을 강조하기 위한 큰그림?...
오늘은 여기까지! 2부에서 만나요~
오늘도 읽어주셔서 감사합니다.