[Apple] Swift 성능 - 1. 성능 요소

J.Noma·2022년 1월 16일
0

Swift : 중요한 주제

목록 보기
3/5

Reference


🤖 Swift 성능을 결정짓는 요소들

⚙️ Overview

  1. 인스턴스가 할당될 영역이 어디인가? (Stack / Heap)
  2. 인스턴스를 주변으로 전달할 때 얼마나 많은 reference counting 오버헤드가 발생할 것인가?
  3. 인스턴스의 메서드를 호출할 때, 어느 Dispatch (Static/Dynamic)를 사용할 것이냐?

✅ 성능을 위한 코딩 가이드라인

✔️ 값타입을 사용하라
빠른 메모리인 Stack을 사용하며, reference count 관리가 필요없기 때문.

✔️ String을 사용하지 말라
String은 Heap에 할당되며 reference count 관리를 요구하기 때문

✔️ Static Dispatch를 사용하도록 유도하라
다형성이 불필요한 class 메서드에 대해 final처리를 하는 등 불필요한

⚙️ 1. 인스턴스가 할당될 메모리 영역이 어디인가

조금만 신경쓰면 드라마틱한 성능향상을 도모할 수 있다

🔸 Stack

Stack에서 인스턴스를 할당/해제하는 경우, 단순히 Stack Point라는 정수값 하나만 decrement/increment하면 된다

위와 같이, Stack에 할당되는 value type의 경우 다른 변수로 할당하면 새로운 copy 인스턴스가 생성되어 할당된다. 이를 value semantic이라 부른다

🔸 Heap

Heap은 Stack이 할 수 없는 dynamic lifetime등을 할 수 있지만 more advanced 자료구조가 필요하다. 데이터를 새로이 할당할 때, Stack처럼 메모리를 순차적으로 사용하지 않으므로 적절한 크기의 미사용 공간을 찾는 작업이 필요하다 비효율적이다. 또한, 해제시키기 위해 다른 공간으로 reinsert하는 것도 필요하다

이것만 봐도 Stack에 비해 할게 많은걸 알 수 있는데, 사실 Heap의 진정한 overhead는 멀티스레딩에서 발견된다. 여러 스레드가 동시에 할당을 시도하면 싱크를 맞추기 위해 lock 메커니즘이 필요하다. 이게 굉장히 큰 비용으로 작용한다

class같은 참조타입은 Heap 영역에 할당되며, 위와 같이 class를 관리하기 위한 2 word(reference count와 V-Table)의 메모리영역(blue)이 추가로 필요하다 . 그리고 다른 변수에 할당할 때, copy가 아닌 동일한 인스턴스에 대한 참조만 전달한다. 따라서, point1.x를 변경하면 point2.x도 함께 변경된다. 이를 reference semantic이라 부른다

이후 해제를 위해 Heap을 잠그고 사용되지 않는 Heap 영역을 적절한 위치로 반환합니다. 그리고 Stack에 있는 point1/point2를 pop합니다

🔸 결론

class가 Heap 영역에 할당되고 reference semantic을 가지기에 취할 수 있는 강력한 특징들도 존재합니다 (identity / indirect storage).

하지만 이로 인해, stack에 비해 많은 비용이 들기 때문에, 이런 특장점들이 필요하지 않은 경우라면 struct를 사용하는게 좋습니다. 그리고 struct는 reference semantic에 의한 의도치않은 sharing을 방지할 수 있습니다

🔸 Heap -> Stack Example

위 코드는 Heap allocation을 요구하는 String을 key로 사용하지 않음으로써 성능 향상을 도모하는 예제입니다

⚙️ 2. Reference Counting의 대상인가

🔸 Reference Counting 오버헤드

Heap에 할당되는 인스턴스들은 Reference counting 관리가 필요합니다

Swift는 Heap에 할당된 메모리를 언제 해제해야 할지를 판단하기 위해 reference counting을 사용합니다. Reference counting을 관리하기 위해선 사실 정수값을 increment/decrement하는 것 외에도 고려할 부분들이 있습니다.

먼저, Reference count를 increment/decrement 하기 위한 몇단계 level의 간접적인 조치들이 필요하다는 점이 있습니다. 그리고 (사실 가장 중요한 포인트인) Thread-safety 오버헤드입니다. 여러 스레드에서 동시에 reference count를 증가/감소시킬 수 있기 때문에 counting 행위를 atomic하게 만들어야 했습니다

reference counting은 매우 빈번하게 발생하기에 실질적으로 많은 비용을 요구하게 됩니다

🔸 Referece -> Value Example


⚙️ 3. 어떤 Method Dispatch 방식을 사용하는가

🔸 Static Dispatch

컴파일 타임에 어떤 메서드 구현체가 호출될지 알 수 있어 런타임에 바로 해당 구현체로 점프할 수 있는 경우를 Static Dispatch라고 합니다. 이 경우 컴파일러가 충분한 정보를 갖고 inlining같은 각종 최적화를 시도해볼 수 있습니다

🔸 Dynamic Dispatch

반면, Dynamic Dispatch는 이를 컴파일 타임에 결정할 수 없는 경우로, 어떤 구현체를 실행할지를 런타임에 찾아야 합니다. 사실 그렇다고 해서 Dynamic Dispatch가 Static Dispatch에 비해 매우 비용이 큰 정도는 아닙니다. 그저 한 단계 정도의 간접적인 조치(indirection)가 필요합니다. 또한, 다른 측면들처럼 Thread-safety에 영향을 주지도 않습니다. 하지만 Dynamic Dispatch는 컴파일러에게 충분한 정보를 주지 못하므로 최적화 기회가 적습니다. 이런 컴파일러 최적화는 함수 내에서 또 다른 함수를 호출하는 체인의 규모가 크면 클수록 큰 성능차이를 보입니다

참고로, inlining이란?
메서드 호출부를 실제 구현체로 치환하여, 메서드 구현체 주소값으로 점프하거나 Stack push/pop과 같은 관련 동작을 생략할 수 있습니다

🔸 class의 V-Table

또한, class는 "다형성"을 지원하므로 위와 같이 V-Table을 이용한 Dynamic Dispatch가 필요합니다. 이는 method chaining 케이스에서 유의미한 성능차이를 보일 수 있습니다. class 성능을 개선하기 위한 방법으로 subclassing을 하지 않을 메서드에 대해선 final 처리를 통해 Static Dispatch가 수행되도록 처리해주는 것이 좋습니다

profile
노션으로 이사갑니다 https://tungsten-run-778.notion.site/Study-Archive-98e51c3793684d428070695d5722d1fe

0개의 댓글