티스토리 뷰

반응형

안녕하세요 :) Zedd입니다.

어쩌다 보니.. OptimizationTips에 들어가게 되었는데요..! 한번 공부하면 좋을 것 같아서 정리해보려고 합니다 XD


들어가보면 딱 보이는 듯이 "Writing High-Performance Swift Code"

가장 성능이 좋게 swift 코드를 작성하는 법? 같은 동작을 하는 Swift코드를 작성하더라도, 성능은 다를 수 있어요.

아무튼 어떻게 짜야 성능이 좋아질지 무려 애플님들이..주는..팁을 한번 공부해봅시다.



Writing High-Performance Swift Code



이 문서는 고성능 Swift작성을 위한 다양한 팁과 트릭을 모아놓은 것입니다. 

이 문서의 팁 중 일부는 Swift로 짜여진 프로그램의 품질을 향상시키고, 오류가 발생하기 쉬운 코드를 보다 쉽게 읽을 수 있도록 도와줍니다. final 클래스와 class-protocol을 명시적으로 표시하는 것이 한 예입니다. 


Enabling Optimizations

가장 먼저 해야 할 일은 최적화를 "활성화"하는 것입니다. 

Swift는 4가지 최적화 level을 제공합니다.


- Onone(없음) : 평범한 개발을 위한 것입니다. 최소한의 최적화를 수행하고, 모든 디버그 정보를 보존(preserve)합니다.


- O : 대부분의 production code를 의미합니다. 컴파일러는 aggressive(공격적인..?) 최적화를 수행하여 방출된(emitted) 코드의 타입과 양(amount)를 대폭 변경 할 수 있습니다. 디버그 정보가 발생하지만 손실됩니다.


- Ounchecked : 안전성(safety)를 성능으로 교환할 의향이 있는 특정 라이브러리, 앱을 위한 특수한 최적화 모드입니다. 컴파일러는 모든 overflow검사와 일부 implicit(암시적) 타입 검사를 제거합니다. 하지만 이는 감지되지 않은 메모리 문제와 정수 overflow를 초래할 수 있기 때문에 일반적으로 사용되지 않습니다. 정수 overflow 및 타입 캐스트와 관련하여 코드가 안전하다는 것을 신중하게 검토 경우에만 이 Ounchecked를 사용합니다.


- Osize : 컴파일러가 성능보다 코드 크기를 우선순위로 지정하는 특수 최적화 모드입니다.

Xcode UI에서 현재 최적화 수준을 수정 할 수 있습니다. 


@_@.....

Onone / O / Ounchecked / Osize 이렇게 4가지의 최적화 레벨이 있는데요, 마지막줄에서 말하죠.  "Xcode UI에서 현재 최적화 수준을 수정 할 수 있습니다. 

" < 그럼 어디서 하냐!??!?!?! 

프로젝트에 들어가서 > TARGETS > Build Settings > optimization Level 검색. 


하면 이렇게 2개가 나오는데, 아래 Swift Compiler - Code Generation부분에 우리가 방금 배운 것들이 있는..

여긴 따로 지정해준 적이 없는데..기본적으로 이렇게 되나봐요?..


궁금한건 왜 Ounchecked는 없ㅈ ㅣ...?


흠..잘 모르겠네요. 아무튼 여기서 지정 할 수 있습니다!!



Whole Module Optimizations

기본적으로, Swift는 각 "파일"을 개별적으로 컴파일 하는데요, 이를 통해 Xcode는 여러파일을 매우 빠르게 병렬로 컴파일 할 수 있어요.

하지만!!!!!! 각 파일을 개별적으로 컴파일하면, 특정 컴파일러 최적화가 수행되지 않아요. Swift는 전체 프로그램을 하나의 파일처럼 컴파일 할 수 있으며, 단일 컴파일 유닛인 것 처럼 프로그램을 최적화 할 수 있어요. 이 모드는 swiftc 커맨드 라인 플래그 -whole-module-optimization 을 사용하여 활성화 됩니다. 이 모드로 컴파일된 프로그램은 컴파일 하는데 오랜시간이 걸리지만, 더 빨리 실행될 수 있습니다.(이게 뭔소리..? 컴파일하는데 시간이 오래걸리는데 더 빨리 실행될 수 있ㄴ ㅏ..?)

이 모드는 Xcode Build Setting에 있는 Whole Module Optimization를 사용하여 활성화 할 수도 있습니다. 



프로젝트 > TARGETS > Build Settings > swift compiler검색 > Swift Compiler - Code Generation부분



이거 같아요1! 기본적으로 Whole Module인듯? 디버그는 Single File이지만.. 

아무튼 새로운 사실들을 많이 알아가네요.



Reducing Dynamic Dispatch

Swift기본적으로 Objective-C와 같이 매우 동적인 언어(dynamic language)입니다. Objective-C와 달리 Swift는 프로그래머에게 필요한경우, 이 동적인 효과를 제거하거나 줄임으로써 런타임 성능을 향상시킬 수 있는 기능을 제공합니다. 이 섹션에서는 이러한 작업을 수행하는데 사용 할 수 있는 언어 구문의 몇가지 예를 살펴볼겁니다. 


Dynamic Dispatch

클래스는 기본적으로 메소드 및 프로퍼티 접근에 동적 디스패치(dynamic dispatch)를 사용합니다. 따라서 다음 코드 스니펫에서 a.aPropertya.doSomething() 그리고 a.doSomethingElse()는 모두 동적 디스패치를 통해 호출됩니다. 


class A {


    var aProperty: [Int]


    func doSomething() { ... }


    dynamic doSomethingElse() { ... }


}


class B : A {


    override var aProperty {

        get { ... }

        set { ... }


    }


    override func doSomething() { ... }


}


func usingAnA(_ a: A) {


    a.doSomething()


    a.aProperty = ...

    

}


빨간색으로 표시한 애들이 동적 디스패치를 사용하는..!! 


Swift에서 동적 디스패치는 vtable을 통한 간접 호출로 기본 설정됩니다. "dynamic"키워드를 선언에 첨부하면, Swift는 Objective-C 메세지 전송을 통해 호출을 보냅니다. 

두 경우 모두 direct function call(직접 함수 호출..?)보다 느립니다. 그 이유는 간접 호출을 수행하는데 드는 오버헤드외에도 많은 컴파일러 최적화를 막기(prevent) 때문입니다.(왜냐면 컴파일러는 호출되는 함수가 뭔지 정확하게 알지 못하기 때문ㅎ)

성능이 중요한 코드에서는 종종 이러한 dynamic behavior을 제한하려고 합니다.


vtable: virtual method table: 가상 메소드 테이블은 타입 메소드의 주소를 포함하는 인스턴스에서 참조하는 타입 별 테이블입니다. 동적 디스패치는 먼저 객체에서 테이블을 찾은 다음, 테이블에서 메소드를 찾아보는 방식으로 진행됩니다.) 

동적 디스패치, dynamic에 관한 글은 Swift) dynamic이란? / Realm dynamic var?를 한번 읽어보시는 것도...ㅎ


Advice: Use 'final' when you know the declaration does not need to be overridden

final을 아직 모르신다면 Swift ) Inheritance(상속).

사실 별거 업승ㅁ ㅎ 클래스앞에 final이 붙으면 서브클래싱을 막는거고, 메소드나 프로퍼티 앞에ㅡ 붙으면 override가 안댐


final키워드는 선언을 재정의(override)할 수 없도록 클래스, 메소드 또는 프로퍼티 선언에 대한 제한입니다. 이것은 컴파일러가 간접호출대신, 직접 함수 호출을 내보낼 수 있음을 의미합니다.

예를들어, 다음과 같은 C.array1 와 D.array1은 직접 접근이 가능합니다. 반대로 D.array2는 vtable을 통해 호출됩니다. 



final class C {

    // No declarations in class 'C' can be overridden.

    var array1: [Int]

    func doSomething() { ... }

}


class D {

    final var array1: [Int] // 'array1' cannot be overridden by a computed property.

    var array2: [Int]      // 'array2' *can* be overridden by a computed property.

}


func usingC(_ c: C) {

    c.array1[i] = ... // Can directly access C.array without going through dynamic dispatch.

        c.doSomething() = ... // Can directly call C.doSomething without going through virtual dispatch.

}


func usingD(_ d: D) {

    d.array1[i] = ... // Can directly access D.array1 without going through dynamic dispatch.

        d.array2[i] = ... // Will access D.array2 through dynamic dispatch.

}


자, 하나하나 보시면 이해ㅑ가 가실거에요. C라는 클래스는 앞에 final이 붙었으므로 서브클래싱이 되지 않습니다.

클래스 D는 앞에 final이 붙지는 않았지만, array1앞에 final이 붙었네요. 

위에서 말했듯이, 클래스나 메소드, 프로퍼티앞에 final을 붙히는 것은 컴파일러가 간접호출대신, 직접 함수 호출을 내보낼 수 있음을 의미한다고 했죠? 

UsingC라는 함수를 보도록 하죠. C는 클래스 자체가 final로 선언되었기 때문에, C가 가지고 있는 array1과 doSomething은 동적디스패치를 통하지않고!!!!!!!!!!!!! 직접 접근/호출이 가능하죠. 아 근데 주석보면 array1에는 dynamic dispatch를 안한다고 되어있고, doSomething에는 virtual dispatch라고 되어있는데, vtable이 가상 "메소드" 테이블이라 그런건지...그런거겠죠? 아무튼 final이 붙으면 이렇게 오버헤드가 있는 동적 디스패치를 하지 않고 직접 접근이 가능해지죠 == 더 빨라지죠.


하지만 usingD 함수를 보겠습니다. D는 final class는 아니었고, D의 array1이라는 프로퍼티 하나만 final이었어요. 

그렇다면?!?! d.array1은 동적 디스패치없이 직접 접근이 가능해지죠. 하지만 array2는 final이 아니었기때문에 동적 디스패치를 사용하여 접근하게 됩니다. 


Advice: Use 'private' and 'fileprivate' when declaration does not need to be accessed outside of file

- 선언을 파일 외부에서 접근 할 필요가 없는 경우, 'private' 및 'fileprivate'를 사용하세요.

private과 fileprivate를 모르신다면...

access control(1)

access control(2)

를 참고하시면 될 것 같습니다. 


private또는 fileprivate키워드를 선언하면, 선언의 가시성(visibility)이 선언된 파일로 제한됩니다. 이렇게하면 컴파일러가 잠재적으로 중요한 다른 모든 선언들을 확인 할 수 있씁니다.ㄷ 따라서 이러한 선언이 없으면 컴파일러는 자동으로 final키워드를 추론하고, 메소드 및 필드 접근에 대한 간접 호출을 적절히 제거합니다. 예를들어, E, F가 동일한 파일에서 우선 순위 선언이 없다고 가정할 때, e.doSomething() 와 f.myPrivateVar에 직접 접근 할 수 있습니다. 


private class E {

    func doSomething() { ... }

}


class F {

    fileprivate var myPrivateVar : Int

}


func usingE(_ e: E) {

    e.doSomething() // There is no sub class in the file that declares this class.

    // The compiler can remove virtual calls to doSomething()

    // and directly call A's doSomething method.

}


func usingF(_ f: F) -> Int {

    return f.myPrivateVar

}


자, 저는 위에서 "따라서 이러한 선언이 없으면 컴파일러는 자동으로 final키워드를 추론하고, 메소드 및 필드 접근에 대한 간접 호출을 적절히 제거합니다. " < 갑자기 final이야기가 왜나와;;??? 위에서(final나왔을때) 이야기 해야하는데 지금 이야기 한건가? 오타인가..?라고 생각했는데 일단 그건 아니고 같이 한번 봅시다.


클래스 E는 fileprivate클래스이죠? E는 이 "파일"안에서만 접근이 가능해진다는 것입니다. 근데 이 파일안에는 E를 서브클래싱 하지 않았죠???????

E는 이 파일 안에서만 접근 할 수 있다 && 근데 이 파일 안에서 서브클래싱 하지 않았음 ==> 마치 final이 붙은 것 처럼 생각 할 수 있음. 

그렇죠!?!?? 컴파일러는 마치 E에 final이 붙었다~~라고 생각 할 수 있는 것이며. 위 final에서 했던것 처럼, E가 가지고 있는 doSomthing(). 즉 usingE함수에서 e.doSomething은 "직접"호출이 가능해지는 것입니다. 


F도 마찬가지죠. F는 internal(아무것도 안붙어있으면 기본적으로 internal) class이지만, myPrivateVar는 fileprivate프로퍼티입니다. 즉, F클래스는 같은 모듈 && 외부"파일"에서 접근은 가능하지만 fileprivate인 myPrivateVar는 이 파일 안에서만 접근이 가능합니다. 근데 이 파일 안에서 myPrivate를 딱히 오버라이드 하는 곳은 없네요. 그렇다면 역시 컴파일러는 마치 final이 붙은것 처럼 생각할 수 있다는 것이죠. 그래서 usingF에서 f.myPrivateVar도 직접 접근이 가능해지게 됩니다.


위 내용은 이게 정답이다!!!!!!!!!라는 것을 제가 확신할 수는 없는데, 문서가 "final를 추론할 수 있다"라는 것을 보고 제가 아마 이렇게 동작할거다..라고 쓴거에요, 혹시 틀린내용이 있다면..댓글이나 PC화면 오른쪽 하단에서 볼 수 있는 채널서비스를 이용해서 꼭 메세지 주시길 바랍니다 ㅠㅠㅠ..



Using Container Types Efficiently

Swift표준 라이브러리에서 제공하는 중요한 기능은, generic container Array및 Dictionary입니다. 이 섹션에서는 이러한 타입을 효율적으로 사용하는 방법에 대해서 설명할 거에요.


Advice: Use value types in Array

- Array에서는 값타입을 사용하세요.
Swift에서 타입은 값타입(구조체, 열거형, 튜플..) 및 참조타입(클래스)의 두가지 카테고리로 나눌 수 있습니다. 주요 차이점은 값타입은 NSArray내부에 포함할 수 없다는 것입니다. 
따라서 값타입을 사용할 때, optimizer는 NSArray를 백업할 가능성을 처리하는데 필요한 대부분의 오버헤드를 Array에서 제거할 수 있습니다. 
또한 참조타입과 달리, 값타입은 참조타입을 반복적으로 포함하는 경우에만 참조카운팅이 필요합니다. 참조타입 없이 값타입을 사용하면, Array내의 트래픽을 추가로 보존하지않도록 할 수 있습니다. 


// Don't use a class here.

struct PhonebookEntry {

    var name : String

    var number : [Int]

}

var a : [PhonebookEntry]


large value type(큰...값타입?)을 사용하는 것과 참조타입을 사용하는 것 사이에는 절충점이 있음을 명심하세요. 경우에따라 large value type을 복사하고 이동하는 오버헤드가 bridging 및 retain/release오버헤드를 제거하는 비용보다 클 수 있습니다. 


코드를 보면, 맨 마지막줄에 a라는 배열이 하나 있죠? 아니근데 왜이렇게 변수명을 짓는거야 나름 Swift OptimizationTips인데.. 암튼 a라는 변수는 PhonebookEntry타입의 배열입니다. Array!!!!!!! 이 Array에 참조타입을 사용하면 Array내에 트래픽을 추가로 가지고 있어야 한다는 말 같은데.. 그래서 이러한 경우에는 이 PhonebookEntry를 참조타입인 class로 만들지말고 값타입인 struct로 만들어라₩~~~같네요. 



Advice: Use ContiguousArray with reference types when NSArray bridging is unnecessary

- NSArray로의 브릿징이 필요없는 경우, 참조타입과 함께 ContiguousArray를 사용하세요.

아니 장난하나 말이 쉽지 ㅡㅡ 코딩하면서 어케 참조타입 배열은 안만드냐 ㅡㅡ 

이제 말해줍니다. 

참조타입의 배열이 필요하고, 배열을 NSArray에 연결 할 필요 없는 경우, Array대신 ContiguousArray를 사용하세요. 띠용...


class C { ... }

var a: ContiguousArray<C> = [C(...), C(...), ..., C(...)]


저는...ContiguousArray라는 것을 처음...들어..보지만......네..사용하도록 노력하겠읍니다..

이 ContiguousArray를 또 따로 공부 해봐야겠어용


Advice: Use inplace mutation instead of object-reassignment

- 객체 재할당 대신 inplace mutation를 사용하세요. 

Swift의 모든 표준 라이브러리 Container는 COW(copy-on-write)를 사용하여 to perform copies instead of explicit copies(번역하기 애매해서 그냥 원문..명시적 복사본대신 그냥 복사를 수행한다?..)를 하는 값타입입니다. 많은 경우, 이를 통해 컴파일러는 깊은 복사(deep copy)를 수행하는 대신 container를 유지함으로써 불필요한 복사본을 제거 할 수 있게 합니다.

 Container의 참조 카운트가 1보다 크고, Container가 변경되면, 기본 Container만 복사하면 됩니다. 예를들어, 다음과 같은 경우, d가 c에 할당될 때, 복사가 수행되지 않지만, d가 2를 추가하여 구조적으로 변이가 일어났을 때, d가 복사된 다음 2가 추가됩니다. 


이게 무슨 소리야;;할지도 모르지만 다음 예제를 보시면 이해가 가실거에요. 


var c: [Int] = [ ... ]

var d = c        // No copy will occur here.

d.append(2)      // A copy *does* occur here.


보통, 아니 보통이 아니라 일단 저는 var d = c라고 그러면 c를 복사해서 d라는 변수에 넣는다! 라는 거니까 여기서 복사가 일어날 줄 알았는데,

d가 구조적으로 변이, 즉 값이 추가된다거나 제거된다거나 했을때, 그때!!!! 비로소 복사가 일어난다는 것이죠. 


때때로 COW는 사용자가 주의하지 않으면, 예상치 못한 추가 복사본을 가져올 수 있습니다. 예를들어, 함수에서 객체 재할당(object-reassignment)을 통해 변형을 시도하는 것입니다. Swift에서 모든 매개변수는 +1한 상태로 전달됩니다. 즉, 매개변수는 callsite보다 callee의 마지막에 released됩니다. 



func append_one(_ a: [Int]) -> [Int] {

    a.append(1)

    return a

}


var a = [1, 2, 3]

a = append_one(a)


a에 아무것도 추가하지 않은 상태에서도 복사될 수 있으며, 할당으로 인해 append_one이후에 사용 할 수 없습니다.

실제로 위 코드는 컴파일에러를 내죠.

이는 inout매개변수를 사용하여 방지 할 수 있습니다. 


func append_one_in_place(_ a: inout [Int]) {

    a.append(1)

}


var a = [1, 2, 3]

append_one_in_place(&a)



Unchecked operations

Swift는 정수 산술을 수행할 때, overflow를 확인하여 정수 overflow버그를 제거합니다. 메모리 안전문제가 발생할 수 없다는 것을 알고 있는 코성능 코드에서는 이러한 검사가 적절하지 않습니다. 

그런문제가 발생하지 않을 코드를 또 검사하는것은 결국 오버헤드겠죠? 


Advice: Use unchecked integer arithmetic when you can prove that overflow cannot occur

- overflow가 발생 할 수 없음을 입증할 수 있는 경우, unchecked integer arithmetic를 사용하세요.

성능이 중요한 코드에서는 안전하다고 판단되면 overflow검사를 제거 할 수 있습니다. 



a : [Int]

b : [Int]

c : [Int]


// Precondition: for all a[i], b[i]: a[i] + b[i] does not overflow!

for i in 0 ... n {

    c[i] = a[i] &+ b[i]

}



&+를 살펴보니, 정수 타입의 고정 width를 overflow하는 모든 비트를 삭제한다고 나와있네요. 이 &+를 사용하면 컴파일러가 따로 overflow검사를 안해줘도 되는 듯? 

Generics

Swift는 Generic타입을 사용하여 매우 강력한 추상화 메커니즘을 제공합니다. Swift컴파일러는 임의의 T에 대해 MySwiftFunc<T> 를 수행 할 수 있는 하나의 concrete code 블록을 내보냅니다(emit) 생성된 코드는 함수 포인터 테이블과 T를 추가 매개변수로 포함하는 box를 사용합니다. MySwiftFunc<Int>MySwiftFunc<String> 사이의 동작 차이는 box에서 제공하는 다른 함수 포인터 및 크기를 추상화를 전달함으로써 설명됩니다. 



class MySwiftFunc<T> { }


MySwiftFunc<Int> X    // Will emit code that works with Int...

MySwiftFunc<String> Y // ... as well as String.


최적화가 활성화 되면, Swift컴파일러는 이러한 코드의 호출을 보고, 호출에 사용된 concrete((i.e. non-generic type) )를 확인하려고 시도합니다. 일반 함수의 정의가 최적화 프로그램에 표시되고, concrete type이 뭔지 알게되면, Swift컴파일러는 특정 타입에 특화된 일반 함수 버전을 방출합니다. specialization이라고 하는 이 프로세스로, Generic과 관련된 오버헤드를 제거 할 수 있습니다. 


class MyStack<T> {

    func push(_ element: T) { ... }

    func pop() -> T { ... }

}


func myAlgorithm(_ a: [T], length: Int) { ... }


// The compiler can specialize code of MyStack<Int>

var stackOfInts: MyStack<Int>

// Use stack of ints.

for i in ... {

    stack.push(...)

    stack.pop(...)

}


var arrayOfInts: [Int]

// The compiler can emit a specialized version of 'myAlgorithm' targeted for

// [Int]' types.

myAlgorithm(arrayOfInts, arrayOfInts.length)



생략을 조아하시는 애플님들 ㅎㅎ


위에서 구구절절 Generic...어쩌구저쩌구 했지만  "The compiler can specialize code of MyStack<Int>"이 걸로 이해가 가시나요? 컴파일러가 구체적인 타입을 알게되면 특정 타입에 특화된 일반 함수 버전을 내보낸다 그랬죠? 이경우에는 Int에 특화된 함수를 내보내겠네요. 즉, 위에서는 myAlgorithm이 되겠네요. 


근데 이게 왜 팁인지 모르겠음...아니 Generic쓰려면 이렇게 쓰는거 아닌가..? 마치 이건 Swift문서에 있어야 할 것 같은데..실제로 Generic예제는 위 코드와 마찬가지로 Stack이었지만


Advice: Put generic declarations in the same module where they are used

- Generic선언을 그 선언들이 사용되는 동일한 모듈 안에 넣으세요. 
optimizer는 generic 선언의 정의가 현재 모듈에서 볼 수 있는 경우에만 specialization을 수행 할 수 있습니다. 이것(specialization)은 -whole-module-optimization플래그가 사용되지 않는 한 선언이 generic호출과 동일한 파일에 있는 경우에만 발생 할 수 있습니다. 
Note: 표준 라이브러리는 special case입니다. 표준라이브러리의 정의는 모든 모듈에서 볼 수 있으며, specialization가 가능합니다.

Swift에서 값은 고유한 데이터 사본을 유지합니다. 값이 독립적인 상태를 유지하도록 하는 것과 같이, 값타입을 사용하면 몇가지 장점이 있습니다. 값을 복사하면(할당, 초기화 및 argument전달의 영향) 프로그램은 값의 새 사본을 만듭니다. 일부 큰 값의 경우, 이러한 사본은 시간이 오래 걸릴 수 있으며 프로그램의 성능을 저하시킬 수 있습니다.


"value"노드를 사용하는 트리를 정의하는 아래 예제를 한번 보겠습니다.

트리노드에는 프로토콜을 사용하는 다른 노드가 있습니다. 컴퓨터 그래픽의 경우, 씬(scene)은 다른 엔티티 및 값으로 나타낼 수 있는 변환(transformations)으로 구성되는 경우가 많으므로 이 예는 다소 현실적입니다. 



protocol P {}

struct Node : P {

    var left, right : P?

}


struct Tree {

    var node : P?

    init() { ... }

}


트리를 복사(argument로 전달, 초기화 또는 할당)하면 전체 트리를 복사해야합니다. 우리 트리의 경우,  malloc / free에 대한 많은 호출과 중요한 참조 계산 오버 헤드가 필요한 값 비싼 연산(expensive operation)입니다.

그러나 우리는, 값의 의미로 남아있는 한 그 값이 메모리에 복사되는지 여부에 별로 신경을 쓰지 않죠. 

COW sematinc에 Array를 사용하는데는 명백한 두가지 단점이 있습니다. 


1. value wrapper의 컨텍스트에서 의미가 없는 "append", "count"와 같은 메소드를 Array에서 노출(exposes)한다는 것입니다. 이러한 메소드는 reference wrapper의 사용을 어색하게 만들 수 있습니다. 

사용되지 않는 API를 숨길 wrapper 구조체를 작성하면, 이 문제를 해결할 수 있으며, optimizer가 오버헤드를 제거할 것입니다. 하지만, 이 wrapper는 다음 두번쨰 문제를 해결하지 못합니다.


2. 프로그램의 안전성과 Objective-C와의 상호작용을 보장하기 위한 코드를 Array가 가지고있다는 것입니다. Swift는 인덱싱 된 접근이 배열 범위 내에 있는지, 배열 저장 영역(capacity를 말하는 것 같음)을 확장해야하는지 여부를 값을 저장할 때 검사합니다. 이러한 런타임 검사로 작업 속도가 느려질 수 있습니다.


Array를 사용하는 대신, Array를 value wrapper로 바꾸기 위해, COW 전용 데이터 구조를 구현 할 수 있습니다. 



final class Ref<T> {

    var val : T

    init(_ v : T) { val = v }

}


struct Box<T> {

    var ref : Ref<T>

    init(_ x : T) { ref = Ref(x) }


    var value: T {

        get { return ref.val }

        set {

            if (!isKnownUniquelyReferenced(&ref)) {

                ref = Ref(newValue)

                return

            }

            ref.val = newValue

        }

    }

}


Box타입은 Array를 대체 할 수 있습니다. 


솔직히 이부분은 정말 이해가 완전히 됐다고 말을 못하겠네요...!!


제가 이해한 바로는 배열로 copy-on-write를 할 시 두가지 단점이 있음.

1. count, append 같은 메소드가 노출됨 (근데 이게 노출되는게 왜..? array는 value type인데, append랑 count가 노출되는게 왜 문제인지 잘 감이 안오는 어차피 매번 복사하기 때문에(append할 때) 굳이 노출되지 않아도 된다는 건가?)

2. Array에서는 런타임에 Objc와의 상호운용성을 위해 이것저것 검사함 -> 속도에 영향.


그러니까 그 Array를 대체하고, append랑 count도 안나오고, Array가 아니니까 컴파일러가 런타임에 이런저런 검사도 안하는!!!!!Box를 만들어따!!!!!!!!!!

라는 것 같읍니다...


Unsafe code

 Swift의 클래스는 항상 참조카운트가 됩니다. Swift컴파일러는 객체에 접근 할 때 마다, 참조 카운트(reference count)를 증가시키는 코드를 삽입합니다.

예를들어, 클래스를 사용하여 구현된 linked list를 scanning하는 문제를 생각해봅시다. list scanning은 한 노드에서 다음 노드로 참조를 이동하여 수행합니다. 

elem = elem.next이렇게 말이죠.

참조를 이동시킬 때 마다 Swift는 다음 객체의 참조 카운트를 증가시키고, 이전 객체의 참조 카운트를 감소시킵니다. 이러한 참조 카운팅 연산은 Swift의 클래스를 사용할 때 비용이 많이 들고, 피할 수 없는(unavoidable) 것입니다. 



final class Node {

    var next: Node?

    var data: Int

    ...

}


Advice: Use unmanaged references to avoid reference counting overhead

- 참조 카운팅 오버헤드를 피하려면, unmanaged references를 사용하세요.
NoteUnmanaged<T>._withUnsafeGuaranteedRef 는 공개API가 아니므로, 향후에 사라질 것입니다. 따라서 앞으로 변경 할 수 없는 코드에는 사용하지 마세요.

성능이 중요한 코드에서는 unmanaged references를 사용하도록 선택 할 수 있습니다. Unmanaged<T> 를 사용하여 개발자가 특정 참조에 대한 자동 참조 계산을 사용하지 않도록 설정 할 수 있습니다.
이 작업을 수행하려면, 인스턴스를 계속 활성상태로 유지하는 Unmanaged 구조체 인스턴스 사용기간동안, Unmanaged구조체 인스턴스가 유지하는 인스턴스에 대한 다른 참조가 있는지 확인해야합니다. (뭔말인지 잘 이해가 안가실듯....천천히 읽어보세용!) 

(자세한 내용은 Unmanaged.swift 참조)


클래스면 무조건 참조 카운팅을 하는 줄 알았는데, 이걸 안할 수있는 방법이 있었다니..하지만 ㅎ 앞으로 사라질거라니까 적극적으로 쓰지는 못하겠네요Unmanaged 가 구조체라서 참조카운팅을 안하게 되는건가? 이 안에서 pointer를 이용해서 클래스와 같이 참조타입처럼 행동하는?? 그런 느낌이네요. 



Protocols

Advice: Mark protocols that are only satisfied by classes as class-protocols

- 해당 프로토콜을 클래스만 사용한다면, 클래스 프로토콜임을 명시하세요.

Swift는 프로토콜 채택을 클래스만 할 수 있도록 제한을 걸 수 있습니다.

프로토콜을 클래스 전용으로 표시하는 한가지 이점은, 컴파일러가 클래스만 이 프로토콜을 충족한다는 지식을 기반으로 프로그램을 최적화 할 수 있다는 것입니다.

예를들어, ARC메모리 관리 시스템은 클래스를 다루고 있음을 알고 있으면, 쉽게 유지 할 수 있습니다. 이 지식이 없으면, 컴파일러는 구조체가 프로토콜을 채택 할 수 있다고 가정해야하며, 비용이 많이 드는  non-trivial structures를 retain/release할 준비를 하고 있어야 합니다.


프로토콜의 채택을 클래스로 제한하는 것이 합리적이라면, 프로토콜을 클래스 전용으로 표시하여 더 나은 런타임 성능을 얻으세요.


protocol Pingable : AnyObject { func ping() -> Int }


ㅎㅎㅎㅎㅎㄴ,ㅇ항ㅎㄴㅎㅎ핳핳ㅎㅎㅎㅎㅎ

사싫ㅎㅎㅎㅎㅎㅎㅎㅎ위 코드의 AnyObject 부분이 "class"라고 되어있었어요. (class는 AnyObject의 typealias)

사실 Swift공식 문서에서도 그렇고...클래스 전용 프로토콜에는 AnyObject를 명시하게 되어있거든요!!! 사실 똑같지만!!!!!! Swift ) Class only Protocol. class? AnyObject?참고


그래서 거슬려서 PR를 날렸습니다ㅏㅏㅏㅏㅏ





핳ㅁ나라핳ㅎㅎ하ㅏㅎㅎㅎㅎㅎㅎㅎㅎ핳ㅎ하핳ㅎㅆㅅ하핳ㅎ



근뎈ㅋㅋㅋㅋ아 휴ㅠㅠㅠㅠㅠㅠㅠㅠㅠ근데 포크하고 클론하고나서 git config name이랑 email을 안해줬더니 ㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠ제 본계정(?)이 아니라 그냥 Zedd로 나와서 contributor로는 못들어간 것 같네요 ㅇ,ㅡ아앙유ㅠㅠㅠㅠ


아무튼!!!!!!!!!!!!!!!!!!!!!!!!!진짜 별거 아니지만 자랑자랑

핳ㅎㅎㅎㅎ진짜 별거아닌데 기분이 너무 좋았다늖ㅎㅎㅎㅎㅎㅎㅎㅎㅎ

제 본계정이 contributor로 들어갔다면 더 좋았겠지만요,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,

진짜 사소한것도 다 PR날려도 됩니닷 뭐 오타라던가...그런것들!



암튼 그래섷ㅎㅎㅎㅎ결론은 ㅎㅎㅎㅎㅎㅎㅎㅎ

만약 해당 프로토콜을 클래스만 채택한다면, AnyObject를 프로토콜에 명시하여서 더 나은 런타임 성능을 얻읍시다!!!!


아무튼 이렇게 OptimizationTips..문서를 다 보았어요!!

새롭게 안 사실들이 은근히 많아서 기분이 좋습니다 :) 

도움이 되었길 바라며 혹시 틀린부분이 있다면 언제든지!! 댓글남겨주세요XD..


반응형

'iOS' 카테고리의 다른 글

iOS ) UIMenuController  (6) 2018.10.13
iOS ) Typographical Concepts  (2) 2018.10.09
iOS 12 달라진점!!  (2) 2018.09.18
iOS12 ) Notification  (2) 2018.09.13
iOS ) Decodable  (2) 2018.08.29