본문 바로가기

Computer

Class vs Struct 성능 비교 테스트

- 테스트에 사용된 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 크기에 따라 성능이 차이가 날 줄 알았는데 크기에 따른 성능 차이는 없다는 것이었다.

  1. 스택 크기에 대한 제한이 없고 모든 프로퍼티가 Reference Counting 오버헤드를 유발하지 않는 경우 --> struct
  2. 스택 크기에 대한 제한이 있고 모든 프로퍼티가 Reference Counting 오버헤드를 유발하지 않는 경우 --> struct -> final class
  3. 상속이 필요한 경우 --> (only)class
  4. 저장 프로퍼티가 2개 이상 Reference Counting 오버헤드를 유발하며 복사가 잦은 경우 --> final class
  5. 저장 프로퍼티가 2개 이상 Reference Counting 오버헤드를 유발하며 복사가 드문 경우 --> struct -> final class
  6. ...

... 쓰다 보니 모든 케이스에 대한 정확한 사용 사례를 정의할 수 없을 것 같다. 각 상황에 맞는 선택을 하도록 하자.

 

 

 

 

* 테스트에 사용된  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
    }
}

 

ClassAndStructTests.swift
0.02MB