본문 바로가기

Swift

Memory에 대한 이해

초보 iOS 개발자로 일하면서, 내가 짜는 코드에 대한 원리와 성능보다는 의도한대로 작동하는지가 최우선이었고,

이에 대한 한계는 분명히 존재함으로 늦게나마 메모리에 대한 이해를 목표로 공부하고 그 과정을 남깁니다.

 

Swift 성능에 영향을 미치는 요소

  • Allocation: 인스턴스를 생성하면 Stack과 Heap 중 어느 곳에 할당 되는 지
  • Reference Counting: 인스턴스를 통해 레퍼런스 카운트가 몇개가 발생하는지
  • Method Dispatch: 인스턴스에서 메소드를 호출했을 때, 메소드 디스패치가 정적인지 동적인지

Allocation

Stack VS Heap

스택과 heap은 모두 RAM(Random Access Memory)의 영역입니다.

 

Stack은 LIFO(Last In First Out)의 단순한 구조로 메모리 할당과 해제가 편리합니다.

Stack은 Stack Pointer를 사용하여 할당・해제를 처리합니다.

Stack은 단순한 구조를 가진만큼 시간복잡도는 O(1)으로 속도가 매우 빠릅니다.

 

Heap은 stack보다 더 복잡한 구조를 가지고 있습니다.

Heap은 Stack보다 dynamic한 할당 방법을 사용하는데, Heap 영역에서 사용하지 않은 블록을 찾아서 메모리 할당을 처리합니다.

할당을 해제하기 위해서는 해당 메모리를 적절한 위치로 다시 삽입합니다.

여러 thread가 동시에 Heap에 메모리를 할당할 수 있기 때문에 locking 또는 기타 동기화 메커니즘을 사용하여 무결성을 보호해야합니다.

 

위 특징들을 살펴보면 알 수 있듯이, stack은 heap보다 비용이 더 적게 들어가며, 속도가 더 빠른 할당 방법입니다.

Semantics

메모리가 할당될때 Stack과 Heap 영역에 저장되는 기준?

-> semantic에 의해 결정 ( Value semantics와 Reference Semantics로 구분)

 

※ semantics는 어떤 타입, 기호가 내부적으로 어떤 의미인지를 뜻합니다.

 

Value Semantics: Struct

 

Value semantics 타입들의 인스턴스는 stack에 할당됩니다. 

struct가 대표적인 value semantics를 따르고 있으며, struct 외에도 enum, tuple 그리고 기본 타입들이 있습니다.

Struct 인스턴스를 생성하여 다른 인스턴스에 할당하면, 전체 값은 그대로 복사가 됩니다.

복사된 인스턴스는 기존 인스턴스와 구분되어져 stack에 저장되기 때문에 내부 값을 변경해도 원래 값에 영향을 주지 않습니다.

Heap을 사용하지 않기 때문에 reference counting도 사용하지 않습니다.

Reference counting은 heap에서 발생하며 아래에서 더 자세하게 설명할 예정입니다.

Reference Semantics: Class

 

Reference semantics는 stack에는 reference인 주소값을 할당하고, 실질적인 데이터는 heap에 할당합니다.

대표적으로 class가 있으며 function 또한 reference semantics입니다.

Class는 struct가 인스턴스 내부의 변수 개수에 맞추어 2words의 size로 stack에 할당되는 것과 달리 heap에 4words size로 할당됩니다. 이는 Swift가 우리 대신 클래스를 관리하기 위한 방법입니다. (파란색으로 표시된 박스)

 

 word 하나 하나의 변수를 저장할 수 있습니다.

 

Class는 reference semantics로 항상 하나의 identity라는 것을 유의해야합니다. 

Point 클래스의 인스턴스를 생성하고 복사하게 되면 stack에 있는 레퍼런스, 주소값이 복사되기 때문에 point1, point2는 모두 하나를 향해 같은 값을 가지게 됩니다. 그렇기 때문에 복사된 인스턴스를 수정하면 원래 인스턴스 데이터도 함께 변경되는 것처럼 보이게 됩니다.

Reference Counting

Reference counting 말 그대로 참조된 인스턴스의 개수를 세는 것입니다.

Heap 영역에 할당하는 것 자체가 레퍼런스를 사용하기 때문에 reference counting이 발생합니다.

 

Swift Heap영역의 메모리 해제

Reference Counting을 사용하는 이유에는 메모리 해제가 있습니다.

Swift는 reference counting을 통해서 할당 해제 여부를 결정하고, reference count는 퍼포먼스에도 영향을 줍니다.

 

Swift는 힙에 있는 모든 인스턴스의 레퍼런스 카운트를 가지고 있습니다.

또한 이 레퍼런스 카운트를 인스턴스가 직접 가지고 있게 합니다.

이러한 레퍼런스 카운트는 레퍼런스가 추가되거나 제거될 때, 레퍼런스 카운트는 증가되거나 감소됩니다.

레퍼런스 카운트가 0이되면 Swift는 해당 인스턴스를 아무도 사용하지 않는다고 간주하여 메모리에서 해제하기에 안전하다고 판단합니다.

 

Reference Count in Class

클래스에서 발생하는 Reference Count에 대해 살펴 보겠습니다.

아래는 Point 클래스에서 reference Counting의 상황을 구체적으로 설명하기 위한 의사코드입니다.

 

 

먼저, point1 인스턴스가 생성되었고 그에 따라 reference count를 뜻하는 refCount가 1 증가 하였습니다.

 

point2 인스턴스가 생성되었고 point1 인스턴스가 복사되었습니다.

point1 인스턴스의 reference가 추가적으로 발생하였고,

refCount가 1 증가 합니다.

 

point1의 인스턴스가 복사된 point2의 인스턴스에서 point 클래스의 x의 값을 변경했습니다.

point2 인스턴스의 프로퍼티를 수정하게 되면, Reference semantics를 따르는 class이기 때문에 두 인스턴스가 참조하고 있는 heap 영역의 인스턴스의 프로퍼티가 수정되게 되며, point1, point2가 모두 수정되는 것 처럼 보이게됩니다.

 

 

이제 point1과 point2의 인스턴스의 사용을 모두 마쳤고 release 시켜줍니다.

reference count를 나타내는 refCount는 0이 되게 됩니다.

이 때 release를 통해서 Swift는레퍼런스 카운트 감소를 원자적(atomically)으로 처리하게 됩니다.

 

 

 

이제 Swift는 해제해도 안전하다고 판단하고, heap을 lock하고 메모리 블럭을 반환합니다.

 

Reference Count in Struct

 

Heap 할당이 일어나지 않는 struct에서는 reference counting이 발생하지 않는다고 생각하기 쉽습니다. 하지만, Struct가 reference semantics를 따르는 타입을 프로퍼티로 가지게 된다면 reference counting은 발생합니다.

 

 

다음 Label 구조체는 다음과 같이 String 타입의 text와 UIFont 타입의 font를 프로퍼티로 가지고 있습니다.

 

String은 character들을 힙에 저장하기 때문에 레퍼런스 카운트가 필요합니다.

UIFont 또한 클래스로 만들어진 객체이기 때문에 레퍼런스 카운트가 필요합니다.

 

그러므로 Label 구조체의 인스턴스를 생성하게 되면,

아래 이미지와 같이 두개의 레퍼런스가 발생하게 되며 reference counting이 발생하게 됩니다.

 

 

이러한 Label 구조체를 복사하게 되면 레퍼런스 text, font의 레퍼런스 하나씩이 복사되어 총 카운트가 2개가 추가됩니다.

 

인스턴스들을 모두 사용 후에 레퍼런스들은 모두 release() 해줘야 합니다.

두개의 구조체 인스턴스를 사용했는데, 레퍼런스로 인해서 4번을 release하는 비용을 지불하게 됩니다.

 

정리하자면, 클래스는 힙에 할당되기 때문에 Swift는 heap allocation 라이프타임을 관리해야합니다.

이것은 레퍼런스 카운팅으로 처리하게 됩니다.

반면 구조체는 기본적으로 레퍼런스를 사용하지 않지만,

구조체가 레퍼런스를 가지게 되면 reference counting으로 오버헤드(overhead)를 처리하는 비용이 들게 됩니다.

 

※ 오버헤드(overhead)는 어떤 처리를 하기 위해 들어가는 간접적인 처리 시간·메모리 등을 말합니다.

 

여기서 구조체의 reference counting 오버헤드는 구조체에 있는 레퍼런스 개수에 비례하게됩니다.

그래서 만약 구조체에서 하나보다 더 많은 레퍼런스를 가지게 된다면,

reference counting 오버헤드가 클래스보다 더 많이 발생하게 됩니다.

 

 

출처:

 

[Swift] 스위프트 성능 이해하기 (1) - struct와 class의 성능 차이

struct와 class의 성능에 대해 자세히 알아보자

corykim0829.github.io

 

'Swift' 카테고리의 다른 글

생체인증을 통한 본인인증 제작기  (0) 2022.06.07
보안 핀코드 입력화면 제작기  (0) 2022.05.15
Initializer  (0) 2022.01.27
Lazy in Swift  (0) 2021.02.28