- 테스트에 사용된 Class 와 Struct 구현은 맨 밑에 있습니다. (+ 직접 테스트 해볼 수 있는 .swift 파일)
- 실제 상황에서 측정한 것이 아니므로 정확한 측정이 될 수 없겠지만 최대한 변수 요소가 제한된 상황에서 테스트를 수행했다고 생각합니다.
Allocation
// - Simple ------------------------------------------------------------------------
// Average: 0.275 sec
func test_measureAllocationPerformanceForSimpleClass() {
measure {
(1...REPEAT_COUNT).forEach { i in
let ref = SimpleClass(id: i)
}
}
}
// Average: 0.232 sec
func test_measureAllocationPerformanceForSimpleStruct() {
measure {
(1...REPEAT_COUNT).forEach { i in
let ref = SimpleStruct(id: i)
}
}
}
// - Normal ------------------------------------------------------------------------
// Average: 0.760 sec
func test_measureAllocationPerformanceForNormalClass() {
let sampleData = "SampleDataString".data(using: .utf8)!
measure {
(1...REPEAT_COUNT).forEach { i in
let ref = NormalClass(id: UUID(), data: sampleData, name: "\(i)", alias: "\(i)", userInfo: [:])
}
}
}
// Average: 0.654 sec
func test_measureAllocationPerformanceForNormalStruct() {
let sampleData = "SampleDataString".data(using: .utf8)!
measure {
(1...REPEAT_COUNT).forEach { i in
let ref = NormalStruct(id: UUID(), data: sampleData, name: "\(i)", alias: "\(i)", userInfo: [:])
}
}
}
// - Heavy -------------------------------------------------------------------------
// Average: 0.757 sec
func test_measureAllocationPerformanceForHeavyClass() {
let largeData = Data(count: 1024 * 1024 * 1024 * 1024)
measure {
(1...REPEAT_COUNT).forEach { i in
let ref = HeavyClass(id: UUID(), data: largeData, name: "\(i)", alias: "\(i)", userInfo: [:])
}
}
}
// Average: 0.663 sec
func test_measureAllocationPerformanceForHeavyStruct() {
let largeData = Data(count: 1024 * 1024 * 1024 * 1024)
measure {
(1...REPEAT_COUNT).forEach { i in
let ref = HeavyStruct(id: UUID(), data: largeData, name: "\(i)", alias: "\(i)", userInfo: [:])
}
}
}
// - Heaviest ----------------------------------------------------------------------
// Average: 0.528 sec
func test_measureAllocationPerformanceForHeaviestClass() {
let p1: Point = Point(x: 0, y: 0)
let p2: Point = Point(x: 0, y: 0)
...
let p19: Point = Point(x: 0, y: 0)
let p20: Point = Point(x: 0, y: 0)
let size = MemoryLayout.size(ofValue: HeaviestClass(p1: p1, p2: p2, p3: p3, p4: p4, p5: p5, p6: p6, p7: p7, p8: p8, p9: p9, p10: p10, p11: p11, p12: p12, p13: p13, p14: p14, p15: p15, p16: p16, p17: p17, p18: p18, p19: p19, p20: p20))
print(size) // 8
measure {
(1...REPEAT_COUNT).forEach { i in
let ref = HeaviestClass(p1: p1, p2: p2, p3: p3, p4: p4, p5: p5, p6: p6, p7: p7, p8: p8, p9: p9, p10: p10, p11: p11, p12: p12, p13: p13, p14: p14, p15: p15, p16: p16, p17: p17, p18: p18, p19: p19, p20: p20)
}
}
}
// Average: 0.317 sec
func test_measureAllocationPerformanceForHeaviestStruct() {
let p1: Point = Point(x: 0, y: 0)
let p2: Point = Point(x: 0, y: 0)
...
let p19: Point = Point(x: 0, y: 0)
let p20: Point = Point(x: 0, y: 0)
let size = MemoryLayout.size(ofValue: HeaviestStruct(p1: p1, p2: p2, p3: p3, p4: p4, p5: p5, p6: p6, p7: p7, p8: p8, p9: p9, p10: p10, p11: p11, p12: p12, p13: p13, p14: p14, p15: p15, p16: p16, p17: p17, p18: p18, p19: p19, p20: p20))
print(size) // 160
measure {
(1...REPEAT_COUNT).forEach { i in
let ref = HeaviestStruct(p1: p1, p2: p2, p3: p3, p4: p4, p5: p5, p6: p6, p7: p7, p8: p8, p9: p9, p10: p10, p11: p11, p12: p12, p13: p13, p14: p14, p15: p15, p16: p16, p17: p17, p18: p18, p19: p19, p20: p20)
}
}
}
// Average: 0.236 sec
func test_measureAllocationPerformanceForNonReferenceHeaviestStruct() {
let size = MemoryLayout.size(ofValue: NonReferenceHeaviestStruct(p1: 0, p2: 0, p3: 0, p4: 0, p5: 0, p6: 0, p7: 0, p8: 0, p9: 0, p10: 0, p11: 0, p12: 0, p13: 0, p14: 0, p15: 0, p16: 0, p17: 0, p18: 0, p19: 0, p20: 0))
print(size) // 160
measure {
(1...REPEAT_COUNT).forEach { i in
let ref = NonReferenceHeaviestStruct(p1: 0, p2: 0, p3: 0, p4: 0, p5: 0, p6: 0, p7: 0, p8: 0, p9: 0, p10: 0, p11: 0, p12: 0, p13: 0, p14: 0, p15: 0, p16: 0, p17: 0, p18: 0, p19: 0, p20: 0)
}
}
}
분석 결과:
- 정말 성능에 차이가 날까 궁금했는데 실제로 실행 시간에 차이가 나서 놀랐다.
- 각 테스트 케이스마다 Simple Class/Struct 의 경우 약 16%, Normal/Heavy Class/Struct 의 경우 약 14%, Heaviest Class/Struct 의 경우 약 40% 정도의 성능 향상이 관측되었다.(40% 정도의 성능 향상이 관측된 것은 Reference Counting 등의 외부 요인이 있는 것으로 보임. 뒤에 Reference Counting 에 대한 Tests 에서도 비슷하게 관측됨.)
- Normal 과 Heavy 를 비교해서 봤을 때 스택/힙에 할당하는 메모리의 크기와 실행 속도에는 큰 관계가 없는것 같다.
Method Dispatch
// Average: 0.764 sec
func test_measureMethodDispatchPerformanceForNonFinalClass() {
var nonFinalClassList: [SimpleClass] = []
(1...100).forEach { _ in
nonFinalClassList.append(Simple2DerivedClass(id: 0))
}
(101...200).forEach { _ in
nonFinalClassList.append(Simple4DerivedClass(id: 0))
}
(201...REPEAT_COUNT).forEach { _ in
nonFinalClassList.append(Simple5DerivedClass(id: 0))
}
measure {
nonFinalClassList.forEach { nonFinalClass in
let ref = nonFinalClass.getID()
}
}
}
// Average: 0.647 sec
func test_measureMethodDispatchPerformanceForFinalClass() {
var finalClassList: [SimpleFinalClass] = []
(1...REPEAT_COUNT).forEach { _ in
finalClassList.append(SimpleFinalClass(id: 0))
}
measure {
finalClassList.forEach { finalClass in
let ref = finalClass.getID()
}
}
}
// Average: 0.599 sec
func test_measureMethodDispatchPerformanceForStruct() {
var structList: [SimpleStruct] = []
(1...REPEAT_COUNT).forEach { _ in
structList.append(SimpleStruct(id: 0))
}
measure {
structList.forEach { `struct` in
let ref = `struct`.getID()
}
}
}
분석 결과:
- Dispatch 방식에 따라선 성능차이가 거의 없다고 어떤 글에서 본 적이 있는데 직접 테스트한 결과에서는 의미있는 차이가 보였다.
- `final class` 와 `struct` 는 Method dispatch 방식에 따른 차이가 없을거라 예상했지만 아주 근소하게 차이가 발견 되었다. 아마 실제 환경에서는 컴파일러 최적화에 의해 차이가 없을 수도 있다고 생각된다.
Reference Counting or Copy
// - Simple ------------------------------------------------------------------------
// Average: 0.284 sec
func test_measureReferenceCountingOrCopyPerformanceForSimpleClass() {
let simpleClass = SimpleClass(id: 0)
let size = MemoryLayout.size(ofValue: simpleClass)
print(size) // 8
measure {
(1...REPEAT_COUNT).forEach { _ in
let ref = simpleClass
}
}
}
// Average: 0.275 sec
func test_measureReferenceCountingOrCopyPerformanceForSimpleStruct() {
let simpleStruct = SimpleStruct(id: 0)
let size = MemoryLayout.size(ofValue: simpleStruct)
print(size) // 8
measure {
(1...REPEAT_COUNT).forEach { _ in
let ref = simpleStruct
}
}
}
// - Normal ------------------------------------------------------------------------
// Average: 0.280 sec
func test_measureReferenceCountingOrCopyPerformanceForNormalClass() {
let normalClass = NormalClass(id: UUID(), data: Data(), name: "name", alias: "alias", userInfo: [:])
let size = MemoryLayout.size(ofValue: normalClass)
print(size) // 8
measure {
(1...REPEAT_COUNT).forEach { _ in
let ref = normalClass
}
}
}
// Average: 0.315 sec
func test_measureReferenceCountingOrCopyPerformanceForNormalStruct() {
let normalStruct = NormalStruct(id: UUID(), data: Data(), name: "name", alias: "alias", userInfo: [:])
let size = MemoryLayout.size(ofValue: normalStruct)
print(size) // 88
measure {
(1...REPEAT_COUNT).forEach { _ in
let ref = normalStruct
}
}
}
// - Heaviest ----------------------------------------------------------------------
// Average: 0.281 sec
func test_measureReferenceCountingOrCopyPerformanceForHeaviestClass() {
let p1: Point = Point(x: 0, y: 0)
let p2: Point = Point(x: 0, y: 0)
...
let p19: Point = Point(x: 0, y: 0)
let p20: Point = Point(x: 0, y: 0)
let heaviestClass = HeaviestClass(p1: p1, p2: p2, p3: p3, p4: p4, p5: p5, p6: p6, p7: p7, p8: p8, p9: p9, p10: p10, p11: p11, p12: p12, p13: p13, p14: p14, p15: p15, p16: p16, p17: p17, p18: p18, p19: p19, p20: p20)
let size = MemoryLayout.size(ofValue: heaviestClass)
print(size) // 8
measure {
(1...REPEAT_COUNT).forEach { _ in
let ref = heaviestClass
}
}
}
// Average: 0.365 sec
func test_measureReferenceCountingOrCopyPerformanceForHeaviestStruct() {
let p1: Point = Point(x: 0, y: 0)
let p2: Point = Point(x: 0, y: 0)
...
let p19: Point = Point(x: 0, y: 0)
let p20: Point = Point(x: 0, y: 0)
let heaviestStruct = HeaviestStruct(p1: p1, p2: p2, p3: p3, p4: p4, p5: p5, p6: p6, p7: p7, p8: p8, p9: p9, p10: p10, p11: p11, p12: p12, p13: p13, p14: p14, p15: p15, p16: p16, p17: p17, p18: p18, p19: p19, p20: p20)
let size = MemoryLayout.size(ofValue: heaviestStruct)
print(size) // 160
measure {
(1...REPEAT_COUNT).forEach { _ in
let ref = heaviestStruct
}
}
}
// Average: 0.274 sec
func test_measureReferenceCountingOrCopyPerformanceForNonReferenceHeaviestStruct() {
let nonReferenceStruct = NonReferenceHeaviestStruct(p1: 0, p2: 0, p3: 0, p4: 0, p5: 0, p6: 0, p7: 0, p8: 0, p9: 0, p10: 0, p11: 0, p12: 0, p13: 0, p14: 0, p15: 0, p16: 0, p17: 0, p18: 0, p19: 0, p20: 0)
let size = MemoryLayout.size(ofValue: nonReferenceStruct)
print(size) // 160
measure {
(1...REPEAT_COUNT).forEach { _ in
let ref = nonReferenceStruct
}
}
}
분석 결과:
일단 Simple Class/Struct 케이스를 비교하자면, 여러번의 테스트를 통해 평균값을 비교했음에도 항상 아주아주 근소하게 Struct 쪽이 더 빨랐다.
이는 Class 에 Reference Counting 의 증감을 유발하는 프로퍼티가 없고(Int) 스택에 할당될 메모리 크기(8 byte)가 같음에도 Sturct 는 순수하게 값을 복사해서 스택에 저장하는데 비해, Class 는 Struct 와 같은 메모리 크기의 주소값을 스택에 저장하면서도 `SimpleClass` 참조에 대한 Reference Counting 을 증감시켜야 하므로 근소한 차이가 보인것 같다.
`NonReferenceStruct` 를 제외한 다른 Struct 에 대한 테스트 케이스들을 비교해 보면 참조 프로퍼티를 많이 가지고 있을 수록 비례하여 소요 시간이 증가하는걸 확인할 수 있다. `NonReferenceStruct` 는 구조체 자체가 차지하는 메모리 크기가 큼에도 참조 프로퍼티를 가지고 있지 않으므로 `SimpleStruct` 테스트 케이스와 소요시간이 같은 결과가 나왔다.
*****최종 결론*****
Class 와 Struct 비교에 관한 글 & WWDC 를 보고도 언제/어떤 상황에 Class 혹은 Struct 를 사용하는게 이득인지 명확하지 않았는데 이번 실험을 통해 좀 더 근거에 기반한 판단을 내릴 수 있게 되었다.
의외였던 사실은 할당하려는 스택/Heap 크기에 따라 성능이 차이가 날 줄 알았는데 크기에 따른 성능 차이는 없다는 것이었다.
- 스택 크기에 대한 제한이 없고 모든 프로퍼티가 Reference Counting 오버헤드를 유발하지 않는 경우 --> struct
- 스택 크기에 대한 제한이 있고 모든 프로퍼티가 Reference Counting 오버헤드를 유발하지 않는 경우 --> struct -> final class
- 상속이 필요한 경우 --> (only)class
- 저장 프로퍼티가 2개 이상 Reference Counting 오버헤드를 유발하며 복사가 잦은 경우 --> final class
- 저장 프로퍼티가 2개 이상 Reference Counting 오버헤드를 유발하며 복사가 드문 경우 --> struct -> final class
- ...
... 쓰다 보니 모든 케이스에 대한 정확한 사용 사례를 정의할 수 없을 것 같다. 각 상황에 맞는 선택을 하도록 하자.
* 테스트에 사용된 Class 와 Struct
// MARK: Class
class SimpleClass {
var id: Int
init(id: Int) {
self.id = id
}
func getID() -> Int {
return id
}
}
class Simple1DerivedClass: SimpleClass {
override func getID() -> Int {
return id
}
}
class Simple2DerivedClass: Simple1DerivedClass {
override func getID() -> Int {
return id
}
}
class Simple3DerivedClass: Simple2DerivedClass {
override func getID() -> Int {
return id
}
}
class Simple4DerivedClass: Simple3DerivedClass {
override func getID() -> Int {
return id
}
}
class Simple5DerivedClass: Simple4DerivedClass {
override func getID() -> Int {
return id
}
}
class Simple6DerivedClass: Simple5DerivedClass {
override func getID() -> Int {
return id
}
}
final class SimpleFinalClass {
let id: Int
init(id: Int) {
self.id = id
}
func getID() -> Int {
return id
}
}
class HeavyClass {
let id: UUID
var data: Data
var name: String
var alias: String?
var thumbnailData: Data?
var userInfo: [String: Any]
init(id: UUID, data: Data, name: String, alias: String? = nil, thumbnailData: Data? = nil, userInfo: [String : Any]) {
self.id = id
self.data = data
self.name = name
self.alias = alias
self.thumbnailData = thumbnailData
self.userInfo = userInfo
}
}
typealias NormalClass = HeavyClass
class HeaviestClass {
var p1: Point
var p2: Point
var p3: Point
var p4: Point
var p5: Point
var p6: Point
var p7: Point
var p8: Point
var p9: Point
var p10: Point
var p11: Point
var p12: Point
var p13: Point
var p14: Point
var p15: Point
var p16: Point
var p17: Point
var p18: Point
var p19: Point
var p20: Point
init(p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point, p7: Point, p8: Point, p9: Point, p10: Point, p11: Point, p12: Point, p13: Point, p14: Point, p15: Point, p16: Point, p17: Point, p18: Point, p19: Point, p20: Point) {
self.p1 = p1
self.p2 = p2
self.p3 = p3
self.p4 = p4
self.p5 = p5
self.p6 = p6
self.p7 = p7
self.p8 = p8
self.p9 = p9
self.p10 = p10
self.p11 = p11
self.p12 = p12
self.p13 = p13
self.p14 = p14
self.p15 = p15
self.p16 = p16
self.p17 = p17
self.p18 = p18
self.p19 = p19
self.p20 = p20
}
}
// MARK: Struct
struct SimpleStruct {
let id: Int
func getID() -> Int {
return id
}
}
struct HeavyStruct {
let id: UUID
var data: Data
var name: String
var alias: String?
var thumbnailData: Data?
var userInfo: [String: Any]
}
typealias NormalStruct = HeavyStruct
struct HeaviestStruct {
var p1: Point
var p2: Point
var p3: Point
var p4: Point
var p5: Point
var p6: Point
var p7: Point
var p8: Point
var p9: Point
var p10: Point
var p11: Point
var p12: Point
var p13: Point
var p14: Point
var p15: Point
var p16: Point
var p17: Point
var p18: Point
var p19: Point
var p20: Point
}
struct NonReferenceHeaviestStruct {
var p1: Int
var p2: Int
var p3: Int
var p4: Int
var p5: Int
var p6: Int
var p7: Int
var p8: Int
var p9: Int
var p10: Int
var p11: Int
var p12: Int
var p13: Int
var p14: Int
var p15: Int
var p16: Int
var p17: Int
var p18: Int
var p19: Int
var p20: Int
}
// MARK: Helpers
class Point {
var x: CGFloat
var y: CGFloat
init(x: CGFloat, y: CGFloat) {
self.x = x
self.y = y
}
}
'Computer' 카테고리의 다른 글
What is a Use Case? (0) | 2024.03.18 |
---|---|
[RealmSwift] Index 사용에 대한 성능 테스트 기록 (0) | 2024.02.20 |
RxSwift 구현에 대한 이해 기초 (1) (1) | 2024.02.15 |
[RxSwift] `UITextField.rx.text` 프로퍼티의 '예상치 못한 요소 방출' 이슈 (0) | 2024.02.14 |
RxSwift.Single 구현에서의 `stopped` 변수의 의미 (1) | 2024.02.13 |