본문 바로가기

면접질문정리/Swift문법

class의 성능을 향상 시킬수 있는 방법들을 나열해보시오.

- Reference 

https://corykim0829.github.io/swift/Understanding-Swift-Performance/#

 

[Swift] 스위프트 성능 이해하기 (1) - struct와 class의 성능 차이

struct와 class의 성능에 대해 자세히 알아보자

corykim0829.github.io

 

# ARM 앎을 기억하자.

- Allocation - 인스턴스생성 위치 , Stack or Heap

- Reference Counting - 인스턴스를 통해 레카 발생 수

- Method Dispatch - 인스턴스로 메서드를 호출했을때 메소드 디스패치가 정적인지 동적인지

 

위 세가지를 중점으로 파악해보자

 

1. Heap allocation 피하기

 

2. Reference Counting OverHead 최소한으로 줄이기

 

3. 메소드 인라이닝 - 메소드를 호출할 때 해당 메소드로 이동하지않고 메소드의 결과값을 바로 반환하여 성능을 향상시키는 것

 

 

# Stack

- LIFO로 단순한 구조로 메모리 할당과 해제가 편리하다. 

- Stack Pointer를 사용하여 할당.해제를 처리한다.

- 단순한 구조를 가진만큼 시간복잡도는 O(1)로 속도가 매우 빠르다.

 

# Heap

- Stack보다 더 복잡하다. 

- 더 dynamic한 할당방법을 사용하는데 Heap영역에서 사용하지 않은 블록을 찾아 메모리를 할당처리한다.

- 할당을 해제하기 위해서는 해당 메모리를 적절한 위치로 다시 삽입한다.(뭔개소리야?)

- thread에 메모리를 할당할 수 있기에 locking 또는 기타 동기화 메커니즘을 사용하여 무결성을 보호해야함

 

=> stack heap보다 비용이 적게들어가며 속도가 빠른 할당방법이다.

 

# 그렇다면 Stack Heap 저장되는 기준은 무엇인가?

semantics로 결정되는데 여기서 value semanticsreference semantics로 나뉜다.

참고 - semantics은 어떤 타입, 기호가 내부적으로 어떤 의미인지를 뜻한다.

 

Value Semantics: Struct

Value semantics 타입들의 인스턴스는 stack에 할당됩니다.

struct가 대표적인 value semantics를 따르고 있으며,

struct 외에도 enum, tuple 그리고 기본 타입들이 있습니다.

 

Reference Semantics: Class

Reference semantics는 stack에는 주소값을 할당하고, 실질적인 데이터는 heap에 할당한다.(주소값은 heap에 데이터를 가르킴)

대표적으로 class가 있으며 function또한 reference semantics이다.

class는 identity가 하나이다.

 

 

=> class 사용하게 되면, Heap allocation 사용하기 때문에 struct보다  많은 비용이 필요하다. 그렇기 때문에 class 특성이 필요하지 않으면 struct 사용하는게 좋다.

 

 

# 예시 1

Struct에서도 Referenct Counting이 발생할 수 있다???

struct label {
    var text: String
    var font: UIFont
    func draw() {}
}

# 문제점 파악하기

  • String은 다른값이 들어갈 수 있는 위험이 있다(모든 값을 허용하는 포괄적인 타입)
  • String은 value type이지만, heap에 character타입으로 문자들을 간접적으로 저장되기 때문에 String을 사용하게되면 heap allocation이 발생합니다. => Stack에는 주소값이 저장되고 heap에 실질적인 값이 저장된다

이와같이 String 안전성이 떨어지고 Heap allocation 발생시킨다.

 

# 성능 올리기: Heap allocation피하기

- struct를 하나더만들어 캡슐화한다. 

래퍼런스 카운팅이란 인스턴스가 class의 타입을 참조하는 것을 의미함. 인스턴스가 참조하는 즉시 카운팅은 1이된다.

 

# struct에서는 레퍼런스 카운팅이 발생하지 않을 거라고 생각하기 쉬운데 그렇지않다. struct내 프로퍼티가 가지는 타입이 class로 구성되어있는경우 레퍼런스 카운팅이 발생한다. 구조체의 레퍼런스를 사용하지않지만, 구조체가 레퍼런스를 가지게되면 reference counting으로 오버헤드를 처리하는 비용이 들게된다.

 

오버헤드(overhead)는 어떤 처리를 하기 위해 들어가는 간접적인 처리 시간·메모리 등을 말합니다.

 

여기서 구조체의 reference counting 오버헤드는 구조체에 있는 레퍼런스 개수에 비례하게 된다. 그래서 만약 구조체에서 하나보다 더많은 레퍼런스를 가지게되면 reference counting 오버헤드가 클래스보다 많이 발생하게된다….!!!!!!

 

 

# 예시 2

# 문제점 파악하기

 

URL은 구조체이지만, String을 생성자로 받아들어지기 때문에 레퍼런스가 발생하고 나머지 프로퍼티는 String이므로 모두 레퍼런스가 발생한다. 3개의 프로퍼티 모두 reference counting오버헤드 를 발생시킨다. 따라서 Attachment 구조체의 인스턴스의 메모리는 다음과 같이 나타난다.

 

Attachment 인스턴스 하나를 생성하는 것만으로도 3개의 레퍼런스가 발생한다. 오버헤드가 많이 발생하게 된다.

 

# 성능 올리기: Reference counting Overhead최소한으로 줄이기

URL은 대체할 수 있는 타입이 없기때문에 그대로 사용해야한다.  uuid는 UUID라는 struct타입으로 대체가 가능하고 mimeType은 enum을 만들어 대체가 가능하다.

고로 2개의 RCH(Reference Counting Head) 줄일 있다. 자칫 RCH를 간과하고 구조체를 사용하게되면 오히려 class보다 성능이 안좋아질 수 있기에 유의하자

 

 

아래는 2개의 RC를 줄인 후의 메모리 할당 상태이다.

 

# Method Dispatch

이는 프로그램이 어떤 메소드를 호출할 것인지 결정하여 그 메소드를 호출하는 과정을 뜻한다.

어떤 메소드인지 결정되는 시점에 따라 static method dispatchdynamic method dispatch로 나뉜다.

 

Static Method Dispatch

Static method dispatch는 컴파일 시점에 컴파일러가 메소드의 실제 코드 위치를 파악할 수 있어 런타임에 찾는 과정 없이 바로 해당 코드를 실행하는 것을 의미함.

구현된 코드들이 어디서 실행되는지 알 수 있기 때문에 메소드 인라이닝 과 같은 코드 최적화를 적극적으로 시행한다.

 

메소드 인라이닝 - 메소드를 호출할 때 해당 메소드로 이동하지않고 메소드의 결과값을 바로 반환하여 성능을 향상시키는것이다.

Dynamic Method Dispatch

Dynamic method dispatch는 컴파일 타임에 어떤 메소드를 호출하는 지 판단할 수 없어 런타임에 table에 구현을 참조하여 해당 메소드에 대한 정보를 가져와서 코드를 실행시키는 것을 의미함.

사실 dynamic dispatch는 static dispatch보다 그렇게 많은 비용을 필요로 하지는 않는다. 레퍼런스 카운팅 Heap 할당과 같은 쓰레드 동기 오버헤드가 없다. 하지만 컴파일러는 static dispatch에서는 최적화 작업이 가능하지만 Dynamic dispatch에서는 컴파일러가 추론할 수 없다.

 

쉽게 말해

Static Method Dispatch(정적 메소드 디스패치)는 컴파일러가 메소드를 미리읽어 파악해놓고 실행할때 결괏값을 바로 출력하는 방법

-> 실행시 해당 메소드로 이동하지않고 결괏값을 바로 반환해 성능을 향상 할 수 있는 방법임

 

Dynamic Method Dispatch(동적 메소드 디스패치)는 컴파일 타임에 어떤 메소드를 호출할지 판단할 수 없어 런타임에 table에 구현을 참조하여 해당 메소드에 대한 정보를 가져와 코드를 실행시키는 방법

Static Method Dispatch 예시)

동적인 디스패치는 컴파일이 읽을 수 있다.

 

 

Dynamic Method Dispatch 예시)

 

Drawble, Point, Line모두 클래스라서 배열을 만들 시 각 원소들은 모두 같은 사이즈로 저장된다.

왜냐하면 레퍼런스는 주소값으로 배열에 저장하기 때문이다.

 

근데 for문을 사용하여 배열을 순회하여 draw()메소들 호출할건데 컴파일러가 컴파일 타임에 메소드를 정확한 구현부로 대체할 수 있을까요 ? 메소드 인라이닝 같이 해당 메소드의 body구현부를 대체하는것은 불가능해 보인다. 

 

d.draw()의 d는 Point가 될지, Line될지 알 수 가 없기때문이다.

 

 

그래서 이를 해결하기 위해 컴파일러는 클래스에 필드를 하나더 추가한다. 이필드는 클래스의 타입정보에 대한 포인터이며 타입정보는 static memory에 저장되어 있다.

 

이제 draw메소드를 호출할때 컴파일러는 우리를 대신해 static memory에 있는 타입정보를 보고 실행되어야 할 구현부를 가리키는 포인터가 있는 virtial method table를 통해 메소드를 호출한다. 이 table은 V-table이 라고도 불린다.

 

그래서 컴파일러가 어떻게 메소드를 호출하는지 확인해보면 V-table을 통해 실행에 올바른 draw()구현 부를 찾는것을 볼 수 있다.

 

 

마무리

class의 성능

클래스는 기본적으로 heap allocation을 사용하고 이로 인해 reference couting이 발생한다. 또한 dynamic dispatch로 메소드를 호출한다. 하나의 메소드만 봤을때 Dynamic dispatch는 static dispatch와 큰 차이가 없지만, 메소드 체인과 같은 여러메소드가 얽혀있느 상황에서는 메소드 인라이닝과 같은 최적화가 어렵다.

final Class의 성능

struct의 성능

 

원본

https://developer.apple.com/videos/play/wwdc2016/416/

 

Understanding Swift Performance - WWDC16 - Videos - Apple Developer

In this advanced session, find out how structs, classes, protocols, and generics are implemented in Swift. Learn about their relative...

developer.apple.com

 

거의 코리킴님의 블로그를 기반으로 작성한것인데 원본은 WWDC인 위사이트에서 참고할 수 있따. 시간날때 참고해보도록 하자.