티스토리 뷰

Struct Class 기초

https://baechukim.tistory.com/75

 



developer.apple.com/videos/play/wwdc2016/416/

 

 

Understanding Swift Performance - WWDC 2016 - Videos - Apple Developer

In this advanced session, find out how structs, classes, protocols, and generics are implemented in Swift. Learn about their relative...

developer.apple.com

 

 struct와 class로 다음을 살펴봅니다.

 

왼쪽으로 갈 수록 성능이 좋아집니다.

 

Allocation

스택은 매우 간단한 데이터 구조입니다.

스택의 마지막에서 push와 pop을 할 수 있습니다.

스택은 끝에서만 추가와 제거 할 수 있습니다.

스택 끝에만 포인터가 있으면 돼서 구현이 쉽습니다.

함수를 호출할 때, 스택 포인터를 줄이는 것만으로 필요한 메모리를 할당할 수 있습니다.

함수가 종료될 때, 스택 포인터를 포인터를 다시 증가시키면 메모리를 해제할 수 있습니다.

 

힙은 스택보다 동적이고 효율성이 떨어집니다.

힙은 스택이 할 수 없는 동적인 lifetime을 가지는 메모리 할당을 할 수 있습니다.

 

그러나 이를 위해서는 더 진보된 데이터 구조가 필요합니다.

따라서 힙에 메모리를 할당하려면 실제로 힙 데이터 구조를 검색하여 적절한 크기의 사용되지 않은 블록을 찾아야 합니다.

그런 다음 작업을 완료하면 해당 메모리를 적절한 위치에 다시 삽입해야 합니다.

 

이런 과정이 힙 할당에 높은 비용을 차지하는 것이 아닙니다.
여러 스레드가 동시에 힙에 메모리를 할당할 수 있기 때문에 힙은 locking 또는 다른 동기화 메커니즘을 사용하여 무결성을 지켜야합니다.

이것이 비용이 많이 듭니다.

 

 

먼저 포인트1 인스턴스와 포인트 2인스턴스이 스택에 할당됩니다.

x와  y프로퍼티는 스택에 저장됩니다.

포인트 2 = 포인트 1에서 해당 포인트의 복사본을 만들고 스택에 이미 할당한 포인트 2 메모리를 초기화합니다.

 

 

 

포인트2.x = 5를해도 포인트1.x = 0입니다.

포인트1과 포인트2는 독립적인 인스턴스고 이걸 '값'이라고 합니다.

 

그리고 스택포인트를 높이면서 메모리를 해제할 수 있습니다.

 

 

이제 클래스를 살펴보겠습니다.

이 함수를 들어오면 point1과 point2가 스택에 할당됩니다.

하지만 스택에 저장하는 것이 아니 포인트1과 포인트2를 참조하기 위해 메모리에 할당합니다.

참조를 위해서 힙에 할당됩니다.

 

포인트에 0,0을 만들 때, 스위프트는 힙을 잠그고(lock) 적절한 크기의 사용되지 않은 메모리 블록을 찾습니다.

 

 

 

그 다음, x=0, y=0을 가진 메모리로 초기화합니다.

그리고 힙에 해당하는 주소로 포인트1를 초기화합니다.

 

struct에서는 2word로 할당했는데 class는 4word로 할당했습니다.

이유는 스위프트가 우리를 대신해서 관리를 위해 2word를 더 할당하기 때문입니다. (파란 부분)

 

포인트1과 포인트2는 똑같은 힙에 있는 포인트의 인스턴스를 참조하기 때문에 값이 똑같이 바뀝니다.

 

힙에 메모리가 해제되고 나서 스택에서 pop할 수 있습니다.

 

결론 : class가 struct보다 비용이 높다.

class의 특성(추상화 등)이 필요 없다면 struct를 쓰는게 좋다.

 

 

예시

enum을 이용해서 말풍선을 만드는 코드입니다.

 

스크롤을 위해서 캐시를 만들었습니다.

그런데 string은 key에 좋은 타입이 아닙니다.

이 key에 강아지 이름을 넣을 수도 있는데(아무 글자나 넣을 수 있기 때문에) 안전한 방법이 아닙니다.

그리고 String은 characters로 heap에 저장되는데 그렇기 때문에 heap allocation을 하게됩니다.

(String은 value타입이지만 heap allocation이 발생)

이것을 struct를 사용해서 개선할 수 있습니다.

 

String보다 훨씬 안전한 방법입니다.

Struct는 일급객체(first class)이기 때문에 딕셔너리의 key로 사용할 수 있습니다.

그리고 heap 할당이 없고 stack할당을 사용하기 때문에 훨씬 빠릅니다.

 

 

Reference Counting

Swift는 힙에 있는 참조 횟수를 카운팅하면서 메모리를 관리합니다.

참조 카운트 횟수가 0이 되면 아무도 참조하지 않기 때문에 안전하게 메모리를 해제할 수 있습니다.

중요한점!

힙 인스턴스는 멀티 스레드에서 동시에 추가되거나 제거될 수 있기 때문에 Thread safety를 고려해야합니다.

참조 카운팅 작업의 빈도 때문에 비용이 추가됩니다.

 

이것은 참조횟수가 있는 pseudocode입니다. 

 

일단 포인트1 포인트2가 스택에 할당됩니다.

포인트 1을 초기화하면서 참조횟수가 1증가했습니다.

 

 

let point2 = point1, 포인트1을 포인트2에 할당하면서 2개의 참조를 가지게 됩니다.

포인트1사용이 끝나서 참조횟수가 1 내려갑니다.

포인트2사용이 끝나고 참조횟수가 0이됩니다.

이제 참조가 없으니 Swift가 힙을 잠그고 메모리 블럭을 반환하는게 안전하다는걸 알게 됩니다.

 

 

Struct는 heap allocation이 없으니 참조카운팅이 없습니다.

하지만 Struct에  String처럼 heap allocation을 하는게 있으면 참조 횟수가 생깁니다.

font도 클래스니까 참조 횟수가 있습니다.

Label은 지금 두번씩 참조하니까 클래스보다 overhead가 더 발생합니다.

 

"""

Here we have a label struct which contains text which is of type String and font of type UIFont. String, as we heard earlier, actually stores its -- the contents of its characters on the heap. So, that needs to be reference counted. And font is a class. And so that also needs to be reference counted. If we look at our memory representation, labels got two references. And when we make a copy of it, we're actually adding two more references, another one to the text storage and another one to the font. The way Swift tracks this -- these heap allocations is by adding calls to retain and release.

"""

 

 

 

 

예시

이렇게 만들면 3개의 레퍼런스 카운팅 오버헤드가 발생합니다.

uuid를 String에서 UUID라는 struct(128비트로 구성)로 변경합니다. 

 

mimeType도 String으로 되어있습니다.

enum은 값 이기 때문에 heap allocation이 없습니다.

이제 더 강력하고 효율적이고 type safe합니다.

참조 횟수 오버헤드도 줄였습니다.

 

Method Dispatch

컴파일 시 실행할 구현을 결정할 수 있는 경우 static dispatch라고 합니다.

컴파일러가 어떤 구현이 실행될지 알 수 있기 때문에 최적화가 가능합니다. (예를 들면 인라이닝)

 

이것은 Dynamic dispatch와 반대됩니다.

런타임에 구현을 찾아보고 넘어갑니다.

static보다 엄청 비용이 많이 드는 것은 아닙니다.

참조 카운트나 힙 할당 같은 오버헤드는 없습니다.

 

예시

여기서 draw()와 drawAPoint()는 모두 static dispatch입니다.

 

여기서 point.draw()로 대체할 수 있습니다.

싱글 dispatch에서는 차이가 별로 안나는데 chain에서는 차이가 납니다.

 

static dispatch는 전체 chain을 통해 가시성을 갖게됩니다.

하지만 dynamic dispatch는 스텝마다 block이 된다고 합니다.

 

 

Dynamic Dispatch

그럼 Dynamic dispatch는 왜 사용할까요?

 

한 가지 중요한 이유는 다형성입니다.

객체지향에서는 슈퍼클래스가 있고 서브클래스에서 오버라이딩해서 사용하는 경우가 있습니다.

컴파일러가 컴파일 타임에 실행할 올바른 구현을 결정할 수 없는 이유에 대해 이해할 수 있습니다.

d.draw()가 라인일지 점일지 알 수가 없습니다.

 

컴파일러는 클래스에 다른 필드를 추가하는데 해당 클래스의 type에 대한 정보를 나타내는 포인터입니다.

이 필드는 해당 클래스의 타입 정보고 static 메모리에 저장됩니다.

 

우리가 draw를 호출할때 컴파일러가 실재로 생성하는 것은 타입과 정적 메모리의 virtual method table이라고 불리는 것을 찾아보는 것입니다. 

이 테이블은 실행할 올바른 구현에 대한 포인터를 포함하고 있습니다.

 

 

 

상속하지 않으려면 final을 사용해서 표시하면됩니다.

 

'Swift' 카테고리의 다른 글

[Swift] inout 매개 변수, 앰퍼샌드(&)  (0) 2021.05.17
[RxSwift] Hot Observable vs Cold Observable 차이  (0) 2021.05.14
[Swift] static 타입 프로퍼티, 타입 메서드  (0) 2021.05.06
[Swift] 상속  (0) 2021.05.01
[Swift] 접근제어  (0) 2021.05.01
댓글
공지사항