Swift Allocation Explained
Swift Collection의 메모리 할당
Swift에서 Collection은 구조체(struct)로 선언되어 있어 값 타입(value type)입니다.
Array
Set
Dictionary
String (Collection을 준수함)
값 타입(value type)인 Collection은 Stack 메모리에 할당될까요?
일반적으로 Swift에서 값 타입(value type)은 Stack에 할당되지만, Collection의 경우 얘기가 달라집니다.
Collection은 내부적으로 참조 타입(reference type)구조를 사용하여 Heap에 할당합니다.
조금 더 정확히 얘기하면 Stack에는 참조(포인터)가 복사되며, 데이터는 Heap에 저장됩니다.
이 때 Copy-on-Write(COW)를 통해 최적화를 사용하게 됩니다.
Small String Optimization(SSO)
하지만 문자열의 경우 조금의 예외가 생깁니다.
문자열은 내부적으로 SSO를 활용하여 크기가 작은 경우 Stack, 크기가 큰 경우 Heap에 할당됩니다.
작은 문자열(≈ 15자 이하, ASCII 기준) | 큰 문자열(≈ 16자 이상, ASCII 기준) |
Stack 메모리에 저장 | Heap 메모리에 저장 |
UTF-16이나 이모지 같은 멀티바이트 문자열은 더 적은 글자만 스택에 저장될 수 있음 | 내부적으로 힙 메모리에 할당된 데이터를 가리키는 포인터를 저장 |
값 타입(value type)의 메모리 할당
그렇다면 이러한 메모리 할당의 예외는 문자열에만 해당이 될까요?
결론은 아닙니다.
값 타입(value type)의 메모리 할당의 경우에도 크기에 따라 달라질 수 있습니다.
조금 더 쉽게 정리하면 큰 값 타입(value type)의 경우는 Heap에 할당됩니다.
Swift의 내부구현은 언제든지 바뀔 수 있긴하지만, 현재 3machine words보다 큰 경우 거의 확실하게 Heap에 할당됩니다.
(e.g. 32비트 기계에서는 12바이트, 64비트 기계에서는 24 바이트보다 큰 경우)
심지어 매우 작은 구조체의 경우에는 온전히 CPU 레지스터에 저장되어 메모리에 할당되지 않을 수도 있습니다.
Swift & CPU Registers
기본 원리는 Swift의 구조체는 값 타입(value type)이고 크기가 작다면 레지스터에 할당될 수 있습니다.
성능 최적화를 위해 사용되며 당연히 메모리에 할당되는 경우보다 빠른 연산이 가능합니다.
컴파일러는 이를 통해 메모리 오버헤드를 줄이고 성능을 극대화 할 수 있습니다.
물론 레지스터는 크기가 매우 작기 때문에 크기가 큰 구조체는 메모리 Stack or Heap에 할당됩니다.
Int, Double & Heap Allocated
그렇다면 Int와 Double의 경우도 크기가 커지면(3machinery words보다 커지면) Heap에 할당될까요?
이론적(논리적)으로는 가능합니다만, Int와 Double은 선언 시에 크기가 정해집니다.
타입 | 64비트 시스템 | 32비트 시스템 |
Int | 8 | 4 |
UInt | 8 | 4 |
Double | 8 | 8 |
Float | 4 | 4 |
Bool | 1 | 1 |
Character | 2 | 2 |
String | 동적(길이에 따라 다름) | 동적(길이에 따라 다름) |
Array | 동적(길이에 따라 다름) | 동적(길이에 따라 다름) |
Dictionary | 동적(길이에 따라 다름) | 동적(길이에 따라 다름) |
Struct | 구성 요소의 합 | 구성 요소의 합 |
그렇기 때문에 일반적인 값 타입(value type)들은 Heap에 할당되지 않습니다.
Int vs String
Int, Double 등은 선언과 동시에 크기가 정해지는데 왜 String은 동적으로 크기가 정해질까요?
반대로 String은 동적으로 크기가 정해지는데 Int, Double 등은 왜 선언과 동시에 크기가 정해질까요?
고정된 크기로 메모리 접근 최적화
고정된 크기를 사용하는 가장 큰 이유 중 하나는 메모리 접근 속도입니다.
타입의 크기가 일정하면 CPU가 메모리를 효율적으로 읽고 쓰는 데 도움이 됩니다.
크기가 동적이라면 매번 메모리 위치를 계산하는 데 더 많은 시간이 소요됩니다.
메모리 관리의 단순화
- 항상 정해진 크기를 가지고 있기 때문에 메모리 크기와 배치 예측이 쉽습니다.
하드웨어 최적화
많은 하드웨어 시스템에서 정수형 및 부동소수점 연산을 하드웨어적으로 최적화하여 사용합니다.
예를 들어, CPU 64비트 시스템에서 8바이트(Int, Double등)는 하나의 memory word로 처리될 수 있습니다.
조금 더 자세히 살펴보면 더 다양한 이유가 있을 수 있겠지만 크기를 확정하는 것이 여러가지 측면에서 장점이 많습니다.
그렇다면 String도 크기를 정해놓으면 되지 않을까?
문자열을 고정 크기로 사용하지 않는 이유는 문자열 고유의 특성 때문입니다.
즉, 문자열은 길이와 내용에 따라 동적으로 변화하는 크기의 변화폭이 매우 크기 때문입니다.
문자열 길이가 변할 수 있음
문자열의 경우 “Hello” 5자와 “Hello, world!“ 13자를 처리할 때 필요한 메모리 양이 크게 달라집니다.
동적으로 변할 수 있다는 걸 가정하면 (데이터 크기의) 변화의 폭이 매우 큰 편이죠.
반면 Int의 경우 (64비트 기준) 8바이트만 할당하면 2⁶³ ~ 2⁶³ - 1로 표현할 수 있는 범위가 매우 크죠.
문자당 크기가 다를 수 있음
문자열은 보통 유니코드(UTF-8, UTF-16)로 저장됩니다.
유니코드에서 각 문자는 고정된 크기가 아니라 문자의 종류에 따라 크기가 달라집니다.
예를 들어, 영어 알파벳과 같은 ASCII 문자는 1바이트로 저장될 수 있지만, 한글 같은 문자는 2바이트 이상으로 저장됩니다.
동적크기와 메모리 관리
위에서 언급한 것처럼 문자열은 Stack 또는 Heap에 할당되어 관리 될 수 있습니다.
Heap에 할당되어 있을 때는 동적으로 메모리를 할당/해제 하는 것이 효율적이긴 하지만
고정 크기의 데이터를 저장하는 Stack에서는 메모리 크기가 동적으로 변할 수 없습니다.
즉, 변경가능하며 그 폭이 큰 문자열의 경우 적절하지 않습니다.