스위프트 타입별 메모리 분석 실험

Jung Kim
8 min readOct 17, 2021

--

본 실험은 인텔 iMac 27인치. macOS 11.6 Big Sur. Xcode 13. Swift 5.5 기반이다

흔히 운영체제가 관리하는 프로세스 메모리 모델을 설명할 때 사용하는 그림이다. 스위프트로 만드는 앱도 비슷한 구조를 사용한다. macOS에서 프로세스가 사용하는 메모리 모델을 확인하려면 실행 중에 vmmap 명령에 PID를 인자값으로 넘기면 된다.

vmmap으로 확인하면 이렇게 어떤 영역이 얼마나 차지하는지 알 수 있다. 위로 스크롤해보면 영역별로 VM 가상 메모리 주소를 확인할 수도 있다. vmmap에서 영역별 확인은 잠시 후에 자세히 살펴보자.

오늘 실험은 `struct 타입은 정말 값타입인가? 그걸 어떻게 확인할 수 있을까?`에서 출발한다. 스위프트에서 대표적인 struct 타입은 Int와 같은 숫자 타입과 문자열 String 타입을 떠오른다. 우선 Int 타입부터 살펴보자.

value1 과 value2 는 각각 0x00007ffeefbff3d0 과 0x0007ffeefbff3d8 에 있다. 이 주소는 어디에 있는 걸까? 주소 범위를 확인하려면 앞에서 언급한 vmmap 명령으로 확인할 수 있다.

vmmap <PID> | grep Stack

Stack 범위가 0x00007ffeef400000–0x00007ffeefc00000 으로 8192KB를 차지한다고 표시된다.

브레이크포인터를 걸고 디버깅 중에 메모리 값을 확인할 수 있는 방법은 두 가지가 있다.

  1. memory read 디버그 명령

(lldb) 디버그 콘솔에 memory read <메모리주소>를 입력하면 32바이트를 표시해준다. 이 값으로 value1 과 value2가 각각 0x0b와 0x0c가 값을 가지고 있는 것을 확인할 수 있다.

2. view memory of … 디버그 메뉴

두 번째 방법은 잘 안쓰는 방법인데 (언제부터인지 정확히 기억이 안나지만) Xcode 디버그 메뉴에 숨겨져 있다.

디버그 콘솔 좌측에 Variables View에서 확인하려는 변수를 선택하고 우클릭을 하면 나온다.

그 중에서 View Memory of “xxx”를 선택하면 된다.

이걸로 확인할 수 있는 것은 Int 타입 struct는 8바이트 길이로 스택에 직접 값을 기록한다는 것이다.

그럼 다음으로 String 타입을 살펴볼 차례인데, 그 전에 위에서 Int 메모리 주소를 확인하는 데 사용한 Memory.dump(variable:) 함수를 살펴보자.

스위프트에서 변수값을 메모리 주소 형태로 출력하기 위해서 여러 가지 방식이 있는데 크게 3가지로 정리가 된다. (이외에 다른 방법이 더 있다면 알려주세요😚)

코드에서 알 수 있는 것처럼 dump(variable:) 함수는 withUnsafePointer(to:) 기능을 활용한 것이다. T 타입을 제네릭으로 선언하고 inout으로 넘겨서 주소값을 출력한. 그 아래있는 dump(with:) 와 dump(object:)는 차례대로 설명할 예정이다.

String 값을 확인하는 예제 코드와 함께 전체 코드는 다음과 같다

꽤 긴 것 같지만, 문자열을 확인하는 dumpString() 동작 순서는 다음과 같다.

  • str1에 “abcd”를 넣는다
  • str1에 조금 더 긴 문자열을 다시 넣는다
  • str2에 str1을 할당한다
  • str2를 다른 문자열로 변경한다
  • str1을 또 다른 문자열로 변경한다

각 단계를 지나갈 때마다 dump(variable:)과 dump(with:)를 이용해서 출력한다.

실행 결과를 살펴보자. 결론적으로 말하면 String 타입은 값 타입이기도 하고, 아니기도 하다.

str1에 “abcd”를 할당하면 0x00007ffeefbff418 스택 영역에 16바이트가 할당되고, 0x000000001006ca510 힙 영역인 MALLOC_TINY 영역에 내부 문자열이 공간이 또 잡힌다. dump(with:)를 사용하면 이렇게 문자열처럼 힙 영역에 저장 공간 주소도 확인할 수 있다. 특이한 점은 앞서 살펴본 Int 타입은 dump(with:)로 넘길 수 없다.

str1에 또 다른 문자열을 할당하면 스택 영역 주소는 변화가 없고, 내부 문자열을 저장하는 힙 영역 주소만 변경된다. 이것은 마치 클래스 래퍼런스 포인터가 동작하는 구조와 비슷하다.

str2에 str1을 할당하는 단계로 넘어가보자. str2는 스택 영역 0x00007ffeefbff370 주소를 사용하고, 힙 영역은 바로 str1의 힙 영역 주소와 동일한 주소가 나타난다. 이 상태에서 str2에 다른 문자열을 할당하면 다시 새로운 힙 영역 주소가 사용된다. 특이한 점은 앞에서 str1을 처음 할당했던 공간을 재사용하고 있다. (여러 번 실행해보면 항상 그런 것은 아니다)

마지막으로 str1에 일부러 조금 긴 문자열을 할당해봤다. 여전히 스택 영역은 변경되지 않고 힙 영역에 새로운 공간을 사용하는 것을 확인할 수 있다. 실제 주소 Memory를 확인하면 Int와 다른 점이 또 드러난다

str2에 넣은 ‘zzzz’처럼 짧은 문자열은 스택 공간에도 직접 저장하고 있다. 그렇지만 str1에 두 번째와 마지막에 바꾼 것처럼 긴 문자열은 스택 공간에 저장되지 않고 힙 영역을 그대로 사용하는 것으로 보인다. String 타입의 MemoryLayout이 16바이트라서 15글자까지만 이렇게 스택 영역에 직접 저장한다. 길이가 더 긴 경우는 어떻게 매칭하는지 모르겠다. 아마도 프로토콜 extension storage처럼 별도로 관리하는 테이블이 있는 것 같다.

String이 항상 스택에만 있는 게 아니라 마치 클래스 래퍼런스처럼 동작한다는 사실은 알고 있었지만 놀랍다. 스위프트 문자열은 계속해서 버전업이 되고 있어서 최적화에 신경을 많이 쓰고 있기도 하다. 특히 str2가 str1과 달라지는 이 부분에서 copy-on-write를 설명하는 경우가 많다.

이제 String과 비슷할 꺼라고 예상해볼 수 있는 Array를 살펴보자.

struct 타입으로 구현된 Array도 스택 영역에 값이 생기지만, 읽어보면 그냥 8바이트에 곧바로 힙 영역 주소가 들어가 있다. 그래서 struct 타입이지만 참조가 1개만 있는 래퍼런스처럼 동작한다.

array1과 array2는 서로 다른 스택 영역 포인터 변수에 같은 힙 영역을 저장하고 있다가, 어느 한쪽이 값이 바뀌면 힙 영역에 새로운 공간을 사용하게 된다. 값 타입이라서 매번 힙 영역에 새로운 공간을 차지하게 된다. 그래서 매우 빈번하게 값이 추가되는 경우는 NSMutableArray를 함께 비교해야 한다.

마지막으로 이미 다 알고 있는 래퍼런스 타입 class 변화를 살펴보자. MyPoint라는 class 타입을 선언하고 있지만, NSArray와 같은 파운데이션 클래스를 사용해도 결과는 동일하다.

실행 결과는 당연하게도 스택에 포인터 주소는 다르지만, 같은 힙 영역 주소 0x00000001006564e0 공간을 사용한다. 클래스 내부 속성이 바뀌어도 힙 영역은 변경되지 않는다.

특이한 점은 클래스 인스턴스 경우에 ObjectIdentifier() 인스턴스 구분을 도와주는 글로벌 함수가 있다. 이 함수가 리턴하는 값이 바로 힙 영역의 메모리 주소값이라는 것이다. dump(object:)와 동일한 값이다.

여기까지 실험을 정리하고 나니 같은 코드를 M1 맥북에서 돌리면 결과가 달라질까 궁금했다. 메모리 주소 체계가 조금 다르지만 동일한 결과를 확인할 수 있었다.

그렇지만 macOS에 있는 Swift Playgrounds 에서 처음 실행한 결과는 의외였다.

왜냐하면 문자열은 copy-on-write가 전혀 동작하지 않는 것처럼 보였기 때문이다. 그렇지만 종료후에 다시 실행하면 또 같은 주소가 찍히기도 했다. Array는 위와 동일하게 copy-on-write처럼 변경될 때 주소가 바뀌고 있다.

마지막으로 iPad Pro 11인치에서 Swift Playgrounds로 실행한 결과다. str1 포인터는 유지되지만 str2는 이전에 살펴본 copy-on-write처럼 동작하지 않았다.

아무래도 Array와 달리 String 문자열의 경우는 16자 이내는 스택 공간을 최대한 활용하도록 최적화되어 있는 것 같다. swift 소스 코드와 함께 다음에 이 부분을 좀 더 살펴봐야겠다.

--

--