iOS 공부하는 감자

WWDC16) Understanding Swift Performance - 1 본문

WWDC

WWDC16) Understanding Swift Performance - 1

DongTaTo 2023. 3. 1. 13:58
반응형

순서

  1. Allocation
  2. Reference Counting
  3. Method Dispatch
  4. Protocol Types
  5. Generic Code

 

 

Understanding Swift Performance

Swift의 추상화 메커니즘이 성능에 미치는 영향을 이해하기 위한 가장 좋은 방법은 기본 구현을 이해하는 것.

개발자는 추상화를 구축하고 추상화 메커니즘을 선택할 때, 다음의 3가지를 고려해야 한다.

  1. 인스턴스가 Stack에 할당되는지, Heap에 할당되는지? (Allocation)
  2. 인스턴스를 전달할 때, 얼마나 많은 Reference Counting Overhead가 발생하는지?
  3. 인스턴스 메서드를 호출할 때, Method Dispatch 방식이 Static인지 Dynamic인지?

보다 빠른 Swift 코드를 작성하기 위해서는 Dynamism, Runtime에 대한 비용을 피해야 한다.

 

 

1. Allocation

1-1. Stack 할당

Swift는 자동으로 메모리를 할당하고, 해제한다.

메모리 중 일부는 Stack 영역에 할당되는데, Stack은 LIFO로 동작하는 단순한 데이터 구조이다.

Stack의 끝 부분으로만 Push와 Pop이 가능하기 때문에 해당 끝 부분에 포인터를 유지하는 것 만으로도 메모리 할당과 해제를 구현 가능하다. 이 포인터를 스택 포인터라고 부른다.

함수를 호출하는 상황에서는 스택 포인터가 차지하는 메모리 공간을 줄여서 스택 프레임이 할당되어야 하는 공간을 확보한다.

그리고 함수가 종료되면 스택 프레임이 메모리에서 해제되고, 스택 포인터를 함수 호출 이전의 상태로 다시 증가시킨다.

 

중요한 것은 Stack 영역에서의 메모리 할당과 해제가 굉장히 빠르다는 것!! (상수 시간의 시간복잡도를 갖는다)

 

1-2. Heap 할당

Stack 영역은 더 Dynamic하지만 효율은 떨어지는 Heap 영역과 비교된다.

Heap 영역에서는 Stack에서 할당할 수 없는 Dynamic Lifetime을 갖는 메모리를 할당할 수 있다.

Heap에 메모리를 할당하기 위해서는 적절한 크기의 사용되지 않은 블록을 검색한 후, 할당한다.

작업이 끝나고 해제하기 위해서는 메모리를 다시 적절한 위치에 삽입해야 한다.

스택 포인터가 가르키는 부분에서 할당과 해제가 발생하는 Stack과 다르게 메모리 영역을 검색하고, 재삽입하는 과정이 필요하기 때문에 속도 측면에서 효율이 떨어진다.

 

이처럼 동적 할당을 위한 비용도 Heap과 Stack의 속도 효율에 영향을 주지만, Heap 할당과 관련된 주요 비용은 따로 있다.

다중 스레드 환경에서 Heap 영역은 공유되기 때문에, 여러 스레드가 동시에 Heap에 메모리를 할당할 수 있다.

따라서 Heap 영역은 잠금(Locking) 또는 기타 동기화 메커니즘을 사용하여 무결성을 보호해야 하며, 이는 큰 비용을 필요로 한다.

 

현재 프로그래밍을 하면서 Heap에 메모리를 할당하는 시기와 장소에 신경쓰지 않았다면, 

프로그램에서 힙에 메모리를 할당하는 시기와 위치에 주의를 기울이지 않는다면, 조금만 더 신중을 기하면 성능을 크게 향상시킬 수 있습니다.

 

프로그래밍을 하면서 Heap에 메모리를 할당하는 시기와 할당되는 장소를 신중하게 고려하면, 성능을 많이 향상시킬 수 있다.

 

1-3. 코드 예시 (구조체)

 

 

 

 

저장 프로퍼티로 x, y를 갖는 Point 구조체를 정의했다.

 

point1에 x, y 프로퍼티가 0으로 초기화된 인스턴스를 생성하여 할당한 후, point2에 point1을 복사했다.

 

point2의 x 프로퍼티 값을 5로 수정했다.

 

그 이후에 point1, point2를 사용한다.

 

 

 

 

 

이 코드가 동작하는 방식을 추적해보자..

먼저 함수에 들어가면 코드가 실행되기도 전에 point1, point2 인스턴스를 위한 Stack 영역을 할당한다. (스택 프레임)

그리고 정의된 Point는 구조체이기 때문에 x, y 프로퍼티들은 In line 방식으로 저장된다.

 

따라서 인스턴스를 생성하여 point1에 저장하는 코드를 실행하는 부분은 이미 할당된 메모리를 초기화하는 것뿐이다.

point1을 point2에 할당할 때에는 해당 포인트의 복사본을 만들고 스택에 이미 할당된 point2 메모리를 다시 초기화한다.

 

point1, point2는 독립적인 인스턴스이기 때문에 point2의 저장 프로퍼티 x값을 변경한다고 해서 point1에 영향을 미치지 않는다.

 

이후에 point1, point2를 사용하고, 함수가 종료되면 1. 스택 프레임이 메모리에서 해제되고, 2. 스택 포인터가 함수 실행 이전의 위치까지 증가되면서 point1, point2에 대한 메모리 할당이 간단하게 해제된다.

 

1-4. 코드 예시 (클래스)

위 예시는 동일한 코드에서 구조체를 클래스로만 변경했다.

마찬가지로 함수에 들어가면 코드가 실행되기 전에 point1, point2가 저장될 Stack 메모리를 할당한다. 

하지만, 프로퍼티에 직접 인스턴스를 저장했던 구조체와 다르게 point1, point2에는 Heap에 저장될 인스턴스의 참조를 저장하기 위한 메모리를 할당한다.

 

실제로 인스턴스를 초기화하는 코드가 실행되면, Heap을 잠그고(무결성을 보호하기 위해) 적절한 크기의 사용되지 않은 메모리 블록을 검색한다.

 

적당한(?) 공간을 찾아서 Heap에 클래스의 인스턴스가 할당되면, 해당 인스턴스에 대한 메모리 주소로 point1을 초기화 한다.

Heap에 할당된 클래스의 인스턴스는 구조체 인스턴스와 다르게 2개의 파란색 칸을 더 사용하는데, 이는 Swift가 우리를 대신하여 관리할 데이터에 대한 공간이다. (ReferenceCount를 저장하는 공간, Class Type의 메모리 주소를 저장하는 공간)

 

그리고 point1를 point2에 할당할 때에는 point1이 저장하고 있는 참조를 복사하기 때문에 point1, point2는 동일한 인스턴스를 참조하게 된다.

즉, point2의 x값을 5로 변경하면, point1, point2가 참조하는 인스턴스의 값이 변경된다.

 

point1, point2에 대한 작업이 모두 종료되면, Swift는 Heap을 잠그고 메모리를 할당 해제한 후, 사용되지 않는 블록들을 적절한 위치로 다시 삽입시킨다.

 

Heap에 할당되었던 인스턴스가 해제된 후, 스택 프레임 영역도 메모리 할당이 해제된다.

 

이처럼 클래스는 Heap 할당이 필요하기 때문에 구조체보다 인스턴스 생성 비용이 더 많이 요구된다.

클래스가 가지는 특성(상속, 참조 등)이 필요하지 않다면? 구조체를 사용하는 것이 좋다. (선택 가이드(?))

 

1-5. 성능 최적화 예시

채팅 기능을 구현하는 예시로, makeBalloon() 함수를 통해 메시지 뒤에 사용될 풍선 이미지를 그리는 코드 예시이다.

매개변수로 3가지 열거형 값을 받아서 UIImage를 반환한다.

makeBalloon() 함수는 사용자 스크롤 중에 자주 호출되기 때문에 매우 빠르게 동작해야 한다.(는 가정)

 

 

빠른 속도를 구현하기 위해 캐시 레이어를 Dictionary로 추가한다.

매개변수 열거형의 값을 조합하여 문자열을 만들고, 해당 문자열을 Dictionary의 Key값으로 사용한다. Dictionary는 중복이 없으므로, 한번 만들어진 말풍선은 다시 생성할 필요가 없어졌다.

 

하지만, Key값으로 사용되는 문자열(String)은 강력한 타입이 아니다.

Key값으로 "dong", "apple" 등등 아무 문자열이나 실수로(?) 넣을 수 있기 때문에 안전하지 않고,

String은 실제로 해당 문자의 내용을 Heap에 간접적으로 저장하기 때문이다.

더보기

<구조체인 String이 Heap에 간접적으로 저장되는 이유>

String Type의 Memory Layout은 16바이트로 구성되어 있다.

때문에 15자의 문자까지는 Stack에 저장할 수 있지만, 더 긴 문자열이 저장되어야 하는 경우에는 Heap 메모리에 값이 할당된다.

15자 이하의 문자열의 경우 Stack에 직접 저장하긴 하지만 MALLOC_TINY(Heap)에 내부 문자열 공간이 추가적으로 잡힌다.

Collection Type들도(Array, Dictionary, Set) String처럼 구조체로 정의되어 있지만, Heap 메모리 영역을 사용한다.

참고 : https://medium.com/@jungkim/스위프트-타입별-메모리-분석-실험-4d89e1436fee

따라서 makeBalloon() 함수를 호출할 때마다 cache가 hit되어도 Heap 할당이 발생할 수 있다.

 

 

이를 개선하기 위해 Attributes 구조체 타입을 정의하고, Hashable 프로토콜을 채택함으로써 Dictionary의 Key값으로 사용될 수 있도록 만들었다.

 

우선 리터럴 문자열이 아닌, 3개의 열거형 저장 프로퍼티를 갖는 인스턴스를 Key로 사용하기 때문에 아무 문자열이나 넣을 수 있던 이전 코드보다 훨씬 안전해졌다.

또한 makeBalloon() 함수를 호출할 때 cache hit가 있는 경우, Attributes와 같은 구조체의 인스턴스를 생성하는데 Heap 할당이 필요하지 않기 때문에 할당에 대한 오버헤드가 없다.

 

반응형

'WWDC' 카테고리의 다른 글

WWDC16) Understanding Swift Performance - 2  (0) 2023.03.05