https://blog.datepop.co.kr/ios-interview-2024-1/
위의 글을 보고 해당 주제에 대해 작성하는 글임을 밝힘
아래코드는 해당 블로그에 작성된 코드이고, 약간 수정해서 공부해봄
구조체와 클래스의 차이가 뭐에요? 라고 물어보면 한가지만 정확하게 말할 수 있음
구조체는 값타입이고 스택에 저장됨. 클래스는 참조타입이고 힙에 저장됨.
그러면 또 이런 질문을 해보겠음. 그럼 클래스 안에 구조체가 있으면? 구조체 안에 클래스가 있으면 그건 뭔가요?
일단 1탄에서는 클래스 - 클래스, 구조체 - 클래스에 대해서 다뤄보도록 하겠음
final class EmptyClass {}
struct HugeStruct {
var emptyClass = EmptyClass()
var emptyClass2 = EmptyClass()
var emptyClass3 = EmptyClass()
var emptyClass4 = EmptyClass()
var emptyClass5 = EmptyClass()
var emptyClass6 = EmptyClass()
var emptyClass7 = EmptyClass()
var emptyClass8 = EmptyClass()
var emptyClass9 = EmptyClass()
var emptyClass10 = EmptyClass()
}
class HugeClass {
var emptyClass = EmptyClass()
var emptyClass2 = EmptyClass()
var emptyClass3 = EmptyClass()
var emptyClass4 = EmptyClass()
var emptyClass5 = EmptyClass()
var emptyClass6 = EmptyClass()
var emptyClass7 = EmptyClass()
var emptyClass8 = EmptyClass()
var emptyClass9 = EmptyClass()
var emptyClass10 = EmptyClass()
}
실행시간 측정
func createBunchOfReferencesOfClass() {
let clock = ContinuousClock()
let time = clock.measure {
var array = [HugeClass]()
let object = HugeClass()
for _ in 0..<100000000 {
array.append(object)
}
}
print(#function, time)
}
func createBunchOfCopiesOfStruct() {
let clock = ContinuousClock()
let time = clock.measure {
var array = [HugeStruct]()
let object = HugeStruct()
for _ in 0..<100000000 {
array.append(object)
}
}
print(#function, time)
}
createBunchOfReferencesOfClass() 20.57859175 seconds
createBunchOfCopiesOfStruct() 32.240995708999996 seconds
- 클래스 안에 클래스 구조: 20초
- 구조체 안에 클래스 구조: 32초
클래스 안에 클래스가 들어있는 구조가 더 빠르게 실행되는 것을 확인할 수 있음
근데 좀 이상하다는 생각이 들었음. 클래스와 구조체의 할당되는 영역에 대해서 확인해보면
클래스
- 힙을 사용하고, 힙은 동적할당을 통해 공간을 제공함. 이 때 힙에서 사용하지 않은 공간을 찾고 삽입하는 과정에서 시간이 더 걸림
구조체
- 스택을 사용. LIFO의 단순한 구조. 정적 할당이며 스택 포인터로 메모리의 할당과 해제가 이루어짐. O(1)의 시간복잡도가 걸림
통상적으로 클래스가 힙을 사용하고, 비용이 비싸면서 속도가 더 느리다고 알고 있음.
그러면 위의 결과는 잘못된게 아닌가 생각할 수 있음. 근데 구조체를 사용하면 과연 힙 할당이 안 일어날지 생각해봐야함.
구조체 안에 참조타입을 프로퍼티로 가지고 있다면 Reference Counting이 발생함.
이 레퍼런스들은 구조체에 있는 레퍼랜스 개수에 비례하는데, 하나보다 많은 레퍼런스를 가지게 된다고 하면?
동일 상황일 경우 클래스보다 레퍼런스 카운팅 오버헤드가 더 많이 걸리게 되기 때문에 시간이 더 걸리는 것임
WWDC에서 소개한 사진을 보면 참조타입에 대한 레퍼런스 카운트가 구조체 생성시마다 늘어나는 것을 볼 수 있음
여기서 몰랐던 사실 한가지는 String도 참조카운팅이 발생한다는 것인데 실제 문자 내용을 힙에 간접 저장한다고 함
이 사실을 위의 구조체 안에 클래스가 있는 코드에 대입해보면
현재 구조체 개수가 1억개, 1억개의 구조체 * 10개의 레퍼런스 카운트 => 10억개의 레퍼런스 카운트
그러면 클래스 안에 구조체가 있는 코드에 대입해보면
현재 클래스 개수가 1억개, 1억개의 클래스 * 1개의 레퍼런스 카운트 => 1억개의 레퍼런스 카운트
그렇기 때문에 반드시 구조체가 빠를거라는 생각은 옳지 않다는 것이고,
구조체를 사용할 때 참조 타입 프로퍼티를 최소한으로 줄이는 개선이 필요하다고 함
두번째는 레퍼런스 카운트에 대한 코드임
final class EmptyClass {}
final class ClassOfClasses {
let emptyClass = EmptyClass()
let emptyClass2 = EmptyClass()
let emptyClass3 = EmptyClass()
}
let classOfClasses = ClassOfClasses()
let reference = classOfClasses
let reference2 = classOfClasses
let reference3 = classOfClasses
print(CFGetRetainCount(classOfClasses))
print(CFGetRetainCount(classOfClasses.emptyClass))
print(CFGetRetainCount(classOfClasses.emptyClass2))
print(CFGetRetainCount(classOfClasses.emptyClass3))
뭐가 출력될지 생각해봄
1. 인스턴스 생성으로 인해 classOfClasses의 rc 1 증가
2. 다른 변수에 인스턴스가 대입으로 rc 3 증가(reference1~3)
3. CFGetRetainCount 함수를 사용함으로써 1 증가
총 5개
그러면 classOfClasses의 RC는 5인데, 그럼 그 안의 클래스들은 어떨까?
5
2
2
2
결과가 2로 나오는 것을 알 수 있음. 프린트되면서 올라간 거 치면 1이란 이야기임
여기서 알 수 있는 건 감싸고 있는 클래스의 RC가 올라가도, 프로퍼티의 RC는 별도로 올라간다는 것임
emptyClass프로퍼티의 참조카운트를 올리고, 프린트를 다시 찍어봄
let reference4 = classOfClasses.emptyClass
print(CFGetRetainCount(classOfClasses))
print(CFGetRetainCount(classOfClasses.emptyClass))
이제 어떻게 나올까보면 기존 classOfClasses의 참조카운트는 여전히 5이지만
emptyClass의 참조카운트는 3으로 증가한 것을 볼 수 있음
참고
https://blog.datepop.co.kr/ios-interview-2024-1/
https://hasensprung.tistory.com/181
https://corykim0829.github.io/swift/Understanding-Swift-Performance/#
'iOS > iOS 지식' 카테고리의 다른 글
Swift Concurrency(1) - Async와 Await (2) | 2024.11.10 |
---|---|
[Swift] Protocol(1) - (some과 any는 왜 필요할까?) (0) | 2024.10.07 |
[iOS] ViewController 생명주기(2) - 실험 (2) | 2024.10.04 |
[iOS] GCD Priority Inversion (우선 순위의 뒤바뀜) (0) | 2024.08.03 |
[iOS] 네트워크 연결 감지 해보기 (0) | 2024.08.03 |