본문 바로가기
Dev/Swift 내 정리

[Swift 기본기] Value type과 Reference type의 메모리 할당

by Mintta 2024. 4. 12.

정말 오랜만에 다시 글을 써봅니다. 취준을 하면서 자소서를 하도 쓰다보니 블로그글을 안써도 항상 글을 쓰고 있긴 했으니 감각이 무뎌졌었네요..ㅎㅎ 이제 곧 아마도 2주안으로는 자소서도 일단락될 것 같기도 하고 여유가 생기면 그 주라도 항상 블로그글을 쓰도록 노력해보겠습니다!! 오늘 주제는 돌고돌아 value type과 reference type에 대한 내용입니다. Value type과 reference type은 어째서 왜 간단한 듯하면서도 어렵고 이렇게 공부할게 많은 걸까요?? 🤔

 

공부를 하면서 Swift의 String와 Array는 가변적인 크기때문에 데이터가 힙에 저장된다고는 어느정도 알고 있었는데, 생각보다 reference type이 stack에, value type이 heap에 저장되는 예외 상황들이 더 존재하다는 것을 알게되어서 한번 정리해보고자 합니다.

 

일반적인 경우에는 Value type은 stack에, Reference type은 heap영역에 할당되는 것이 맞습니다. 하지만 항상 그런 것은 아닙니다..!

즉, Value type이 heap에 할당되는 경우도 존재하고 Reference type이 stack에 할당되는 경우도 존재하다는 뜻입니다.

어떤 경우에 그런 일들이 생길 수 있는지 한번 살펴보겠습니다.

 

이런 예외 경우에 대해서도 알아보는 것이 중요하다고 생각하는 이유는 개발하는 데 있어서 인지의 차이에 있습니다.

"Stack을 사용했음에도 기대했던 성능적인 측면에서 우위가 없어질 수도 있다는 점과 Class을 사용했음에도 오히려 더 나은 성능을 가지게 되는 예외적인 경우가 생길 수 있다"라는 인지를 가지고 개발하는 것과 무조건적으로 stack이 항상 class보다 성능적으로 좋다고 맹신하는 것에는 분명히 큰 차이가 있을거라고 생각합니다. 제 생각엔 그렇습니다ㅎㅎ

 

그럼 그 예외적인 경우가 어떤 것들이 있나 살펴보겠습니다 !


Reference type이 Stack에 할당되는 경우

reference type의 크기가 고정되어 있거나 Swift 컴파일러가 수명을 예측할 수 있을 때

해당 상황일 때 Stack에 할당하도록 최적화할 수 있습니다.

아래 함수는 Swift Compiler코드 중 해당 최적화가 가능한지를 판단하는 메서드입니다. 주석을 보면 알 수 해당 최적화가 일어나고 있는 것을 알 수 있습니다.

/// At this point, we know that this element satisfies the definitive init
/// requirements, so we can try to promote loads to enable SSA-based dataflow
/// analysis.  We know that accesses to this element only access this element,
/// cross element accesses have been scalarized.
///
/// This returns true if the load has been removed from the program.
bool AllocOptimize::promoteLoadCopy(SILInstruction *Inst) { ... }

https://github.com/apple/swift/blob/62ccf81f7748e3e2c8626354d1ecb3adbd26b063/lib/SILOptimizer/Mandatory/PredictableMemOpt.cpp#L1531C1-L1531C58

 

swift/lib/SILOptimizer/Mandatory/PredictableMemOpt.cpp at 62ccf81f7748e3e2c8626354d1ecb3adbd26b063 · apple/swift

The Swift Programming Language. Contribute to apple/swift development by creating an account on GitHub.

github.com

 

최적화 이유?

heap 할당에는 heap에 빈공간을 찾고, RC에 대한 증감소를 atomic하게 하고, heap에 대한 동기화 등 Stack 할당보다 훨씬 많은 비용이 들기 때문에 Stack에 할당할 수 있는 상황이라면 heap 할당을 Stack 할당으로 최적화하는 것은 성능적인 측면에서 합리적으로 보입니다.

Value type이 Heap에 할당되는 경우

Struct이 protocol을 채택하는 경우

protocol을 채택하고 있는 구조체가 크기가 거대해서 existentail container의 3 wordss를 넘어가게 되면, container는 pointer를 저장하고 있고, 실제 데이터는 Heap에 할당되게 됩니다. 그리고 해당 lifetime은 Value Witness Table로 관리되게 됩니다. 해당 내용은 아래 글에 정리되어있으니 한번 보시길 추천드립니다. 추가로 제가 생각했을 때 정리가 정말 잘된 글을 하나 더 링크를 걸어놓겠습니다!

 

https://codingmon.tistory.com/31

 

[WWDC16] Understanding Swift Performance 2부

Protocol Oriented Programming (POP) 상속이나 참조 semantics없이 다형성 구현 protocol Drawable { func draw() } struct Point: Drawable { var x, y: Double func draw() { } } struct Line: Drawable { var x1, y1, x2, y2: Double func draw() {} } var d

codingmon.tistory.com

 

https://techblog.zepeto.me/%EB%A7%88%EB%B2%95-%EA%B0%99%EC%9D%80-swift-%EC%A0%9C%EB%84%A4%EB%A6%AD-%EC%9D%B4%EC%95%BC%EA%B8%B0-2c222ae2798

 

🪄 마법 같은 Swift 제네릭 이야기

NAVER Z의 iOS 팀은 개인과 팀의 성장을 위해서 다양한 노력을 하고 있습니다.  PR — 코드 리뷰를 통한 코드 레벨에서의 피드백과 개선뿐만 아니라 매주 스터디를 진행하면서 기술적인 성장에

techblog.zepeto.me

Value와 Referece type를 섞어 사용하는 경우

import Foundation


class A{}

struct B {
    let a = A()
}

struct C {}
class D {
    let c = C()
}

 

 

이런 경우에 struct MyStruct과 emptyStruct은 모두 Heap에 할당됩니다. 

 

 

실제로 컴파일된 코드를 확인한 결과 alloc_box를 통해 Heap에 저장되는 것을 확인할 수 있었습니다.

 

alloc_box란 ?

Allocates a reference-counted @box on the heap large enough to hold a value of type T, along with a retain count and any other metadata required by the runtime.

 

이라고 설명되어있습니다. 즉, heap에 저장할 때 사용하는 메서드임을 알 수 있습니다. alloc_box를 사용된다는 뜻은 Heap에 인스턴스가 저장될 것이다라고 이해하면 될 것 같습니다.

 

최적화 이유?

이 경우에는 Heap에 할당되는 class의 생명주기에 어느정도 맞추기 위해 이런 최적화가 이뤄진 것 이닌가하고 추측해볼 수 있을 것 같습니다. (하지만 그럼에도 class를 가진 struct의 경우에는 struct의 lifetime에 class가 따라야하는 것 아닌가 하는 조금의 의문은 남네요)

generic을 사용하는 value types의 경우

public struct BaseResponseType<T: Decodable>: Decodable {

  public let status: Int
  public let success: Bool
  public let message: String
  public let data: T?
}

 

이건 실제로 프로젝트에서 서버와 통신할 때 BaseResponse의 DTO로 사용하는 구조체를 가지고 와봤습니다. 실제로 해당 구조체가 어떻게 컴파일이 되는지 살펴보았습니다.

 

alloc_box 명령어로 미루어보아 실제로 BaseResponseType의 객체는 Heap에 저장되고 있음을 알 수 있었습니다.

 

최적화 이유?

여기서 이뤄진 최적화의 이유를 추측해보자면, 제네릭에는 다양한 타입들이 들어올 수 있기에 구조체의 경우 매번 새로운 인스턴스를 생성하지 않게끔 하기 위해 이런 최적화를 하지 않았을까 싶긴합니다. (제네릭 메서드의 경우에는 들어오는 타입만큼의 페어가 생기지만..)

 

Collections의 경우

Array, Dictionary, Set, String([Character])과 같은 가변 길이 Collections들은 일반적으로 내부 데이터를 Heap에 저장해서 사용합니다. 컴파일 타임에 크기를 정확히 알 수 없기 때문에 Heap에 할당 후, 보다 유연하게 메모리를 확장하거나 축소합니다.

 

이것을 COW(Copy-On-Write)상황과 연관지어 생각해보았습니다. Array는 기본적으로 COW가 적용되어있기 때문에 어떤 수정도 없이 다른 변수에 할당만 했다면 아래 그림과 같은 상황일 것 입니다.

 

이 상황에서 만약 copy2에서 수정이 발생했다고 생각해봅시다.

 

 

그럼 이 때 Array는 스택 영역에 값이 생기지만, 그냥 Heap영역에 대한 주소 reference가 들어있고, 새로운 Array를 할당하면서 Heap에서 또한 새로운 공간을 사용하게 되어 위와 같은 그림의 모습이 될 것입니다.

 

하지만 String의 경우 작은 문자열의 경우에는 stack에 inline으로 저장되고, 길이가 길어짐에 따라서 크기가 커지면 위의 방식대로 동작한다고 합니다.

 

https://sujinnaljin.medium.com/ios-swift%EC%9D%98-type%EA%B3%BC-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%A0%80%EC%9E%A5-%EA%B3%B5%EA%B0%84-25555c69ccff

 

[iOS] Swift의 Type과 메모리 저장 공간

Value Type이 Heap에 할당 될 때가 있다구여???

sujinnaljin.medium.com

https://medium.com/@jungkim/%EC%8A%A4%EC%9C%84%ED%94%84%ED%8A%B8-%ED%83%80%EC%9E%85%EB%B3%84-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%B6%84%EC%84%9D-%EC%8B%A4%ED%97%98-4d89e1436fee

 


Ref

댓글