[WWDC 16 / Swift] Understanding Swift Performance with Allocation

박준혁 - Niro·2024년 5월 3일
1

WWDC

목록 보기
9/11
post-thumbnail

안녕하세요 Niro 🚗 입니다!

갈수록 Swift 를 깊게 알아야겠다는 생각이 들어 WWDC 16 에서 소개된
🔗 Understanding Swift Performance 를 보며 공부하고 정리를 해보고자 적게 되었습니다.

해당 영상에서 중심적으로 얘기하는 3가지의 주제가 있습니다.

  1. 인스턴스가 생성될 때 Stack 과 Heap 중 어디에 할당되는가?
  2. 인스턴스를 할당할 때 Reference Counting 이 발생하는가?
  3. 인스턴스에서 Method 를 호출할때 정적 or 동적 Dispatch 가 되는가?

앞으로 해당 주제에 맞춰 3편으로 나누어 살펴보고자 합니다.


1. Allocation - 어디에 할당 되는가?

Swift 는 사용자를 대신해서 인스턴스가 생성될 때 메모리를 Stack 과 Heap 중 어디에 할당하는지 정하고 해제하는 역할을 합니다.

그 전에 Stack 과 Heap 에 대해 알아보겠습니다.

🗄️ Stack 이 뭐야..?

Stack 은 매우 간단한 데이터 구조를 갖고있어 할당하거나 해제하기 매우 편리합니다. StackPointer 를 통해 항상 마지막 부분에서 추가(push), 제거(pop) 가 가능한 특징을 갖고 있습니다.

이런 간단한 구조와 특징을 통해 정수를 할당하는 시간복잡도(O(1))와 거의 비슷하다고 합니다.

🗄️ Heap 이 뭐야..?

Heap 은 Stack 과 매우 대조적인 모습을 볼 수가 있습니다. 동적으로 할당된 메모리를 관리하게 되는데 할당된 메모리 블럭은 연속적이지 않을 수 있어 적절한 크기를 찾기 위해 Heap 영역을 계속 검색해야만 합니다.

즉, 메모리를 해제하기 위해선 해당 메모리를 적절한 위치에 다시 삽입 해야만 합니다.

💡 "해당 메모리를 적절한 위치에 다시 삽입 해야만 합니다." 의미

이게 무슨 말인가.. 굉장히 고민이 많았습니다.
만약 100 byte 의 메모리 블럭이 존재한다고 가정해봅시다. 현재 50, 20 byte 의 메모리 블럭이 할당되어있다면 30 byte 의 빈공간이 남아있겠죠?

20 byte 메모리 블럭이 더 이상 필요하지 않아 해제를 하게 되면 해당 공간은 가용 가능한 메모리 블럭으로 변합니다.

하지만 이후 할당 될 메모리 블럭의 크기에 따라 내부 단편화나 외부 단편화가 발생할 수 있기 때문에 20 byte 블럭을 30 byte 블럭과 합쳐 50 byte 의 메모리 블럭으로 만들 수 있게 됩니다.

즉, 어떤 메모리 공간을 해제할 때는 내부, 외부 단편화가 발생하지 않도록 아주 적절한 공간에 다시 삽입하여 공간을 효율적으로 관리해야합니다!

이해가 잘 되셨을까요....?

또한 여러 Thread 가 동시에 Heap 에 메모리 할당이 가능한 특징을 갖고 있습니다. 그러면.. 동일한 메모리 블럭에 할당하는 상황이 발생할 수 있겠죠?

그래서 locking 이나 동기화 매커니즘을 추가적으로 구현해주어야만 데이터 무결성을 보호할 수 있게 되므로 개발적으로 상당히 큰 비용이 들어갈거 같습니다.

자, 정리해보자면 Stack 은 Heap 보다 더 적은 비용으로 성능을 향상시킬 수 있는 장점이 있기 때문에 Heap 에 메모리 할당하는 시기나 위치에 주의를 기울어야만 합니다.


2. Struct Allocation 방법

Swift 의 여러 유형 중 Struct 가 메모리에 할당되는 방법에 대해 알아보겠습니다.

Strcut 는 값 타입 (value type) 이라 많이 들어보셨을 겁니다. Enum 과 Tuple 등 다양한 유형들이 값 타입 입니다!

이러한 값 타입 인스턴스들은 Stack 에 할당되는 특징을 갖고 있습니다.

Point Struct 를 통해 point1 인스턴스를 만들었고 해당 인스턴스를 point2 프로퍼티에도 할당해주었습니다.

여기서 point2.x 에 5 를 할당했지만 point1.x 에는 변화가 없는 것을 잘 보아야 합니다.

point1 인스턴스를 point2 에 할당했으면 같은 인스턴스를 공유하고 있는거 아닌가 라고 생각할 수 있지만

값 타입 (value type) 유형은 값이 복사되기 때문에 point1 과 point2 는 독립적인 인스턴스 입니다!


또 하나의 중요한 점이 있습니다.

왼쪽 코드를 보면 실행되고 있는 줄을 표기한 네모칸이 보이시나요?
point1 이라는 변수를 아직 실행하지 않은 모습을 볼 수 있습니다.

아직 인스턴스가 만들어지지 않았는데도 두 개의 인스턴스를 위한 공간이 이미 Stack 에 할당된 모습을 볼 수가 있습니다.

Stack 영역은 컴파일 시점에 크기가 결정된다는 특징을 갖고 있습니다. 컴파일 시점이란 우리가 작성한 코드가 기계어로 변환되는 시점을 의미합니다. 실제로 값이 할당되는 시점은 런타임 시점에 할당이 되겠지만 크기를 결정하는 것은 컴파일 시점이라는 것을 알아두면 좋을거 같습니다!


3. Class Allocation 방법

자, 다음은 Class 유형입니다!

Class 는 참조 타입 (reference type) 이라 하고 Closure 등이 존재합니다.

Struct 와 다르게 메모리가 stack 에 직접 할당되는 것이 아닌 주소값이 할당되고 실제 데이터는 heap 에 할당됩니다.

위의 이미지와 함께 Class 가 할당되는 과정을 살펴보겠습니다.

  1. (x: 0, y: 0) 인 Point 인스턴스를 할당 할 때 Heap 영역을 접근할 수 없도록 잠근다
  2. Heap 영역에 적절한 크기의 할당되지 않은 메모리 블럭을 검색한다
  3. (x: 0, y: 0) 으로 메모리를 초기화 함과 동시에 Heap 상의 메모리 주소도 함께 할당 시킨다

Stack 에 데이터를 할당하는 Struct 과 달리 Class 는 굉장히 복잡한 과정을 갖고 있죠..?

여기서 중요한 점은 Swift 가 실제로 Point Class 에 대해 4개의 word 저장 공간(파란 + 초록)을 할당한 것입니다. 우리를 대신해서 x, y 를 저장한 것 이외에도 word 2개(파란박스) 를 추가해준 모습이 보입니다.

Struct 와 동일하게 인스턴스를 만들고 다른 property 에 할당해주었습니다. 여기서는 실제 데이터를 복사하는 것이 아닌 주소값을 복사하게 됩니다.

즉, point1 과 point2 는 Stack 에 동일한 주소값을 갖고 있고 실제로 Heap 에 동일한 Point 인스턴스를 참조하게 됩니다.

결과적으로 두 property 가 하나의 Point 인스턴스를 참조하고 있기 때문에 point2 의 값을 바꾸면 point1 의 값도 변경된 것처럼 보이게 되는 것이죠!


자, Struct 와 Class 가 어떤 메모리 영역에 할당되는지 알아보았습니다.

Class 경우 실제 데이터는 Heap 에, 실제 데이터를 가리키는 주소값을 Stack 에 저장하기 때문에 Struct 보다 더 많은 비용이 든다는 것을 확인할 수 있었습니다.

그러면.. Class 의 특징이 필요없는 경우라면 Struct 를 사용하는 것이 더욱 좋겠죠..?


4. 이제 성능을 향상시켜보자!

예시로 메세지 풍선을 만드는 코드이고 makeBalloon 메서드를 통해 풍선 이미지를 그리게 됩니다. 매개변수에는 Color, Orientation, Tail 이라는 case 를 통해 Image 를 반환하도록 되어 있습니다.

해당 메서드는 사용자가 스크롤을 할때 자주 호출되는 상황입니다. 무거운 작업이라면 사용성에 큰 영향을 미치겠죠?

여기서 중요한 점은 enum 을 통해서 말풍선 이미지의 경우의 수가 발생하게 되는데 위의 코드에서는 이미 만들어졌던 이미지를 다시 또 만들어지는 단점이 있습니다. 그렇기 때문에 더 많은 비용이 들겠죠?

한번 만들어진 이미지는 굳이 다시 만들 필요는 없기 때문에 캐싱 레이어를 추가하여 View 를 빠르게 처리해보고자 합니다.

Dictionary 타입의 cache 프로퍼티를 만들어 한번 만든 말풍선 이미지를 cache dictionary 를 통해 바로 가져오고자 합니다. 여기에서는 Color, Orientation, Tail 을 문자열인 key 로 직렬화시키는 방법을 적용했습니다.

makeBalloon 메서드를 호출할 때마다 cache hit 가 발생한다면 Heap 할당이 필요하지 않게 된다는 장점이 있을 수 있지만 key 는 String 타입이기 때문에 휴먼 에러의 가능성이나 다른 값이 할당될 수 있기 때문에 안전하지 않다는 점입니다.

또한 String 값 타입이지만 Heap 에 데이터를 간접적으로 저장될 수 있습니다.

즉, String 을 통해 값을 불러와야 하기 때문에 Heap allocation 이 발생할 수 밖에 없는 상황이 존재합니다.

💡 "String 값 타입이지만 Heap 에 데이터를 간접적으로 저장될 수 있습니다” 의미

이 부분에서도 굉장히 많은 고민을 했습니다.
Swift 에서는 String 도 Struct 로 구현 되어있습니다. 당연히 Stack 에 저장이 되겠구나 싶었지만 항상 모든 것에는 100% 라는건 없는거 같네요..

값 타입 유형 중에서도 Heap 에 할당되는 경우가 존재합니다.
바로 Collection 입니다. 주로 Array, Dictionary 를 떠올릴텐데 생각해보면 자유롭게 길이를 줄였다 늘렸다 시킬 수 있습니다. 즉, 메모리 크기를 동적으로 변화를 시켜야 한다는 점이 Heap 의 특장과 유사하지 않나요?

컴파일 시점에 정확히 크기를 알 수 없기 때문에 Heap 에 할당 한 후 사이즈를 변화시킨다고 합니다.
일단… 내용이 너무 많으니 이정도만 설명하고 넘어가겠습니다!

💡 그럼 Cache Hit 는 뭔가요…?

흔히 Cache 라고 하는 것은 CPU 가 빠른 속도로 데이터를 주고 받을 수 있게 CPU 내부에 저장할 수 있는 메모리를 의미합니다. 주기억장치와 같은 데이터를 미리 가져와 임시 저장소 역할을 수행하게 되는데 굉장히 크기가 작기 때문에 자주 사용하는 데이터를 갖고 있다고 합니다.

그렇다면… 어떤 데이터를 가져올려고 캐쉬 메모리에 접근해보니 있을 수도 있고 없을 수도 있는 상황이 존재하겠죠?

있는 경우를 Cache Hit, 없는 경우를 Cache Miss 라고 합니다. Cache Miss 가 발생하면 필요한 데이터를 찾아 캐쉬 메모리에 로드한다고 합니다.

위의 설명을 다시 보자면 Cache Hit 가 발생한다는 것은 우리가 Dictionary 로 만든 cache 라는 프로퍼티에 말풍선 UIImage 가 존재한다는 의미 입니다.

그럼 더 나은 방법을 찾아보겠습니다.

간단하게 생각해보자면 Heap 영역에 할당하는 동작을 수행하지 않으면 성능적으로 이점을 얻을 수 있겠죠?
우리는 위에서 cache 의 key 를 String 으로 만들었는데 이 부분을 Struct 로 바꿔보겠습니다.


struct Attributes: Hashable {
    var color: Color
    var orientation: Orientation
    var tail: Tail
}

해당 Struct 에서는 String 을 사용하지 않게 되면서 훨씬 안전한 방법으로 바뀌었고 Struct 는 Swift 에서 일급타입이기 때문에 Dictionary 의 Key 로 사용하기에도 더욱 적합합니다.

다음과 같이 코드를 수정해주면 makeBalloon 메서드에서 cache hit 가 발생하더라도 key 는 Struct 로 구성했기 때문에 더이상 Heap 할당이 발생하지 않게 됩니다.


5. 정리하자면

내용이 너무 많았던 관계로 정리해보고 끝내보려고 합니다

  • Struct 처럼 값 타입은 Stack 에 할당하고
  • Class 처럼 참조 타입은 Stack 에 주소가 할당되고 Heap 에 실제 메모리가 할당됩니다.

이런 동작으로 인해

  • Struct 는 Class 보다 더욱 빠르고
  • Struct 는 Class 보다 더욱 안전하고
  • Struct 는 Class 보다 더 적은 비용을 갖고 있는 특징을 갖고 있습니다.

무조건 Struct 가 더욱 좋다는 의미는 아니지만 Class 의 특징이 필요한 곳에서는 Class 를 사용해야겠죠?

그렇다면 어떻게 우리는 성능을 향상시킬 수 있는지도 알아보았습니다.

  • 자주 사용하는 이미지에 관해 사용하는 곳마다 이미지를 만드는 것이 아닌 미리 만들어 놓고 가져다 쓰는 방식으로 성능을 높혔고
  • String 유형의 경우 Heap 에 저장되는 경우도 있으니 최대한 사용을 지양하는 것이 좋을거 같습니다!

이렇게 첫번째 글인 인스턴스가 생성될 때 Stack 과 Heap 중 어디에 할당되는가? 에 대해 알아보았습니다.
CS 관련 내용이 많이 포함되어있어 많은 시간이 필요했고 꾸준히 CS 를 공부하는 것이 필요하다고 느꼈습니다...

두번째 글인 인스턴스를 할당할 때 Reference Counting 이 발생하는가? 에 대해서도 많은 관심 부탁드리고
아주 긴글 읽어주셔서 감사합니다! 피드백은 환영입니다!

profile
📱iOS Developer, 🍎 Apple Developer Academy @ POSTECH 1st, 💻 DO SOPT 33th iOS Part

0개의 댓글

관련 채용 정보