swift

WWDC 16 Understanding Swift Performance (1/2)

Beom 2024. 6. 19. 11:01

서론

  • swift 코드를 작성할 때 내가 만든 인스턴스가 스택에 할당될 건지?
  • 아니면 힙에 할당되는 것인지?
  • 이 인스턴스를 전달할 때 참조 계산 오버헤드가 얼마나 되는가?
  • 동적 할당인가? 정적 할당인가? 알고 있어야한다.
    •  그동안 문제가 생기면 생각을 해도 코드를 작성하기 전에, 스스로 생각하면서 코드를 작성하지는 않는 듯하다.

Allocation

Stack

스택이란??
후입선출의 데이터 구조로 가장 나중에 들어간 데이터가 가장 먼저 나오게 된다.

  • 함수를 호출할 때 공간을 만들기 위해 스택 포인터를 간단하게 줄이는 것으로 필요한 메모리를 할당할 수 있다.
  • 함수의 실행이 끝나면 스택 포인터를 함수를 호출하기 전의 위치로 다시 증가 시키는 것만으로 간단하게 해당 메모리 할당을 해제할 수 있다.
  • 스택은 힙에 비해 상대적으로 빠른 처리 속도를 가진다.
  • 컴파일 당시에 크기가 결정이 된다.

 

Heap

힙이란?
선입선출의 데이터 구조로 먼저 들어간 데이터가 먼저 나오게 된다.
  • 스택보다 더 동적이지만 효율성이 떨어진다.
  • 스택이 동적 수명으로 메모리를 할당할 수 없는 작업을 수행한다
    • 동적? 프로그램의 실행 중에 필요에 따라 크기가 결정되는 것
  • 힙에 메모리를 할당하려면 힙 데이터 구조를 검색하여 적절한 크기의 사용하지 않는 블록을 찾아야 한다.
  • 작업이 끝나면 할당 해제 하기 위해 적절한 위치에 해당 메모리를 다시 넣어줘야 한다
  • 여러 스레드가 동시에 힙에 접근할 수도 있기 때문에 힙은 여러 메커니즘을 통해 무결성을 갖추고 있어야 한다.
    • 이게 큰 비용이 든다

 

  • 프로그램을 만들 때 힙에 할당하는 시기와 위치에 주의를 기울이면 퍼포먼스가 향상된다.

코드의 실행과정 (Stack)

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'
  • 다음과 같은 코드를 실행해보면 각 point의 x를 출력하면 다음과 같이 나온다.
    • point1.x = 0.0
    • point2.x = 5.0
  • 그러면 다음과 같은 코드를 실행하기 위해서 어떤 일이 일어나는지 살펴보자

  • 첫번째로 코드가 실행되기전 컴파일의 단계에서 point1과 point2를 위한 스택 공간이 할당이 된다.
  • 각각 초기화를 진행이 된다.
  • point1과 point2를 실행하면 함수를 입력했을 때의 위치까지 스택 포인터를 다시 증가 시키는 것으로 point1과 point2의 메모리 할당이 해제 된다.

 

  • 해당 코드를 struct를 class로 변경하여 처리하면 다음과 같은 일이 발생한다.
  • point1과 point2가 스택에 할당이 된다. 하지만 point들의 프로퍼티들이 저장되는 것이 아닌 참조를 위해 메모리를 할당하게 된다.
  • let point1 = Point(x: 0, y: 0) 구문에서 heap의 데이터 구조에서 적절한 크기의 사용되지 않은 메모리 블록을 검색한다.

  • 클래스 포인터에 4단어의 저장 공간을 할당했다. 왜?????
    • point가 class이고 x와 y말고 관리할 정보들이 있기 때문이다.

 

  • struct일 때 처럼 point2가 point1을 복사하지 않는다.
    • 동일한 Point를 참조하게 되는 것이다.
  • 따라서 point2.x에 5의 값을 할당하면 다음과 같이 된다.
    • point1.x → 5.0
    • point2.x → 5.0
  • point를 사용하면 swift에서 사용자를 대신하여 메모리를 할당 해제하고 사용하지 않는 블럭을 적절한 위치로 보낼 것이다.

 

 

결론

  • 클래스는 힙 할당이 필요하기 때문에 구조체보다 생성하는데 비용이 더 많이 든다.
  • 클래스는 참조 타입의 특성을 가진다.
  • 참조와 관련된 특성이 필요 없다면 구조체를 사용하는 것이 더 좋다.

적용하여 코드의 성능을 향상 시켜보자

  • 간단한 메시지 앱이다.

  • makeBallon함수는 사용자 스크롤 중에 자주 호출하기 때문에 정말 빨라야 한다.

 

  • 따라서 makeBallon함수를 호출할 때마다 캐시를 사용한다고 해도 힙 할당이 발생
  • 구조체를 사용하여 해당 말풍선의 색, 꼬리, 방향등을 나타낼 수 있다.
  • struct는 1급 객체이기 때문에 dictionary에서 키로 사용될 수 있다.
    • Hashable 준수
  • string보다 훨씬 안전한 방법
  • makeBallon 함수를 사용하여 캐시 사용이 일어나도 구조체를 구성하는 데 힙을 사용하지 않기 때문에 할당 오버헤드가 없다.

 

 

Reference Counting

  • swift는 참조를 추가하거나 제거하면 해당 참조 개수가 증가하거나 감소한다.
  • 0에 도달하면 아무도 인스턴스를 참조 안한다고 판단하여 해당 메모리를 할당 해제 한다
  • reference counting은 매우 빈번하게 일어나는 작업이다.

  • 첫째, 증가 및 감소를 실행하기 위한 몇 가지 수준의 간접 참조가 존재한다.
  • 더 중요한 것은 힙 할당과 마찬가지로 여러 스레드의 모든 힙 인스턴스에 동시에 추가하거나 제거할 수 있기 때문에 고려해야 할 thread safety가 있다.

 

RC를 계산하는 방법

 

 

  • point1이라는 인스턴스가 생성이 될 때 RC 1 증가

 

  • point 2에 point1을 할당하면 2개의 참조가 생기기 때문에 RC를 1 증가 시켜준다.
  • 이후, release를 하는 경우 RC가 1씩 감소해 최종적으로 0이 된다.

 

 

  • 이제 참조하는 것이 없기 때문에 swift는 메모리 해제를 해도 된다는 것을 알게 된다.
  • heap을 lock하고, 해당 메모리 블록을 반환하게 된다.

 

struct

  • 구조체는 어떻게 될까?
  • 구조체는 힙 할당을 안하기 때문에 참조 계산 오버헤드가 없다.
  • 더 복잡한 구조체는???
    • 힙 할당을 하는 string과 class인 UIFont를 사용하는 구조체는 ??

  • string과 UIFont는 힙에 할당하기 때문에 RC계산이 필요하다.
  • 메모리 표현을 보면 기본적으로 Label에는 2개의 참조가 존재한다.
    • string
    • font

 

 

  • 복사본을 만드는 경우에는 2개의 참조를 추가하게 된다.

 

  • swift가 이러한 힙 할당을 추적하는 방법은 retain, release를 통해서 하는 것이다.
  • label은 클래스가 가지는 참조 카운팅의 2배의 오버헤드를 발생 시키는 것을 알 수 있다.

 

  • 클래스는 힙에 할당되기 때문에 swift는 힙 할당의 lifetime을 관리해야 한다.

 

 

  • 구조체에 참조를 하는 값이 존재하면 참조 계산 오버헤드를 지불한다.
  • 그렇기 때문에 참조 값이 많다면 struct보다는 class를 사용하는 것이 좋다.
    • 참조가 2개 이상이면 클래스보다 참조 계산 오버헤드가 더 많아 진다.

 

메시지 앱에 적용해보기

  • 상황
    • 사용자들이 문자말고 이미지도 보내고 싶다.

  • 기본적으로 3가지의 프로퍼티가 모두 참조 카운팅 오버헤드를 발생시킨다.
  • 기존의 UUID는 String이였지만 2016년에 foundation에 UUID가 추가 되었기 때문에 UUID를 통해 기존의 String으로 아무런 값을 넣을 수 있던 상황을 UUID만 넣도록 변경했다.

 

  • mimeType은 extension을 통해 구현 했다.

 

  • enum 타입으로 변경을 해서 적절한 값을 매핑해준다.
  • 안전성을 가지게 되고 힙에 저장할 필요가 없기 때문에 성능도 향상되었다.

  • 동일한 코드지만 작성하기 편한 코드

  • 기존에 uuid와 mimeType이 string일 때와 다르게 각각의 타입으로 구현해서 참조 카운트와 힙 할당될 필요가 없기 때문에 참조 카운트 오버헤드를 지불하지 않아도 된다.

 

Method Dispatch

  • 컴파일 타임에 구현을 결정할 수 있으면 정적 디스패치라고 한다.
  • inline과 같은 것을 이용해서 최적화 하는 것이 가능하다.

 

  • 동적 디스패치는 어디로 이동할지 컴파일 단계에서 결정이 불가능하다.
  • 런타임 단계에서 가능해진다.
  • 참조 카운팅 및 힙 할당과 같은 스레드 동기화 오버헤드는 없다.
  • 하지만 동적 디스패치는 컴파일러의 가시성을 차단하기 때문에 최적화가 힘들다.

정적 디스패치?

  • 인라인이란?

 

  • 이 코드에서 drawPoint(point)를 실행하는 것과 point.draw()를 실행하는 것과 동일하다.
  • 그렇기 때문에 drawPoint를 draw로 대체할 수 있다.

 

  • 둘다 정적 디스패치 이므로, draw를 호출할 필요없이 그냥 draw() 구현부를 그냥 실행하는 것도 가능하다.
  • 정적 디스패치의 오버헤드와 호출 스택 관련이 필요가 없다.
    • 바로 stack에 Point를 생성하고 함수의 구현부를 실행하면 되기 때문에 빠르다.
  • 이러한 이유들이 정적 디스패치가 동적 디스패치보다 빠른 이유다.
  • 단일로 봤을 때는 단일 동적 디스패치와 단일 정적 디스패치가 별차이가 없지만 정적 디스패치는 전체 체인을 통해 가시성을 가지게 된다.
  • 동적 디스패치 체인은 매 단계마다 추론을 하지 않고 상위 레벨에서 차단된다.

결론: 컴파일러는 호출 스택 오버헤드가 없이 단일 구현처럼 정적 메서드 디스패치 체인을 축소하는 것이 가능하다. 위의 drawPoint→draw→구현부 이것 처럼

 

동적 디스패치

  • 동적 디스패치를 사용하는 이유

  • 다형성을 가지게 한다
  • Drawable이라는 상위 클래스를 바탕으로 하위 클래스를 가질 수 있다.

  • Drawable타입의 배열을 만들어줬다.
    • 이 배열에는 Point와 Line이 포함될 수 있다.
    • 각 배열의 요소에 따라서 draw를 호출하는 것이 가능하다.

  • Point와 LIne모두 클래스이고 배열에 참조를 저장하기 때문에 크기가 모두 동일하다.

  • 이 d.draw는 point를 그리는 것일 수도 있고, line을 그리는 것일 수도 있다.
  • 그렇다면 어떤코드를 불러올지 어떻게 결정할까?
  • 컴파일러가 해당 클래스의 유형 정보에 대한 포인터를 클래스에 추가하고 이를 정적메모리에 저장한다.
  • draw를 호출할 때 컴파일러가 실제로 생성하는 것은 정적 메모리에 있는 virtual method table이라는 타입을 조회하는 것이다.
  • 일단 실행을 하면 컴파일러가 적합한 draw메소드를 찾고 이것을 파라미터로 실제 인스턴스를 전달한다.
  •  

 

  • 기본적으로 클래스는 메서드를 동적으로 디스패치한다.
  • 이것은 기본적으로 큰 차이를 만들지는 않지만 메소드 체인과 기타 사항들이 쌓인다면 최적화를 방해하는 요소로 남게 된다.
  • 하지만 모든 클래스가 동적 디스패치가 필요한 것은 아니다.

 

  • final 키워드를 사용하면 동적 디스패치에서 정적 디스패치로 변경이 가능하다.
    • 상속을 안하기 때문에 그런듯
  • 또한 컴파일러가 앱에서 클래스를 서브클래싱하지 않을 것임을 추론하고 증명할 수 있다면 동적에서 정적 디스패치로 전환한다.

정적 디스패치

  • 장점
    • 컴파일러가 최적화할 정보가 충분하므로, inline과 같은 최적화가 가능하기 때문에 성능상의 이점이 존재한다.
  • 단점
    • 유연성이 낮기 때문에 상속이나 오버라이딩을 통한 다형성 구현이 어렵다.

동적 디스패치

  • 장점
    • 유연하기 때문에 다향성을 구현하는 것이 가능하다.
    • 같은 형태? 이름의 메소드를 호출할 때 다른 메소드를 실행하는 것이 가능하다.
  • 단점
    • 메소드를 호출할 때 어떤 메소드를 호출을 할지 결정해야 하기 때문에 소모되는 비용이 크다.
    • 최적화가 제한적이다.

어떻게 수행되는지 알고 싶으면 optimizing swift performance WWDC를 봐야한다.