매주 진행하는 면접스터디에서 아래의 질문들에 대한 정리를 모은 글입니다.
https://github.com/JeaSungLEE/iOSInterviewquestions
[Swift] 스위프트 성능 이해하기 (1) - struct와 class의 성능 차이
Understanding Swift Performance - WWDC16
Swift는 자동으로 메모리 할당과 해제를 처리한다. 메모리 할당과 해제는 Stack 또는 Heap에서 처리된다.
Stack은 LIFO(Last In First Out)의 단순한 구조로 메모리 할당과 해제가 편리하다. 시간복잡도는 O(1)로 속도가 매우 빠르다. Stack Pointer를 사용하여 할당과 해제를 처리한다.
Heap은 Stack보다 좀 더 복잡하다. 다이나믹한 할당 방법을 사용하는데, Heap 영역에서 사용하지 않은 블록을 찾아 메모리 할당을 처리한다. 할당을 해제하기 위해, 해당 메모리를 적절한 위치로 다시 삽입한다. 여러 스레드가 동시에 Heap에 접근할 수 있기에 Locking 또는 다른 동기화 메커니즘으로 무결성을 보호해야 한다.
데이터 무결성이란 데이터의 정확성과 일관성이 보장된 상태를 의밓ㄴ다. 정확성이란 중복이나 누락이 없는 상태를 뜻하며, 일관성이란 원인과 결과가 연속적으로 보장돼 변하지 않는 상태를 의미한다.
메모리 할당 시 Stack 혹은 Heap에 저장될 때 시멘틱으로 결정된다. 시멘틱이란 어떤 타입, 기호가 내부적으로 어떤 의미인지를 뜻한다. value semantics와 reference semantices로 구별된다.
딕셔너리에 String 타입의 키를 사용하는 것은 성능에 좋지 않다. String 타입은 값타입이지만, heap에 Character 타입으로 문자들을 간접적으로 저장하기 때문이다. 사용하게 되면 heap allocation이 발생한다.
struct Attributes: Hashable {
var color: Color
var orientation: Orientation
var tail: Tail
}
let key = Attributes(color: color, orientation: orientation, tail: tail)
해결하는 방법으로는, struct을 만들어서 key로 사용하는 것이다. 이때 Hashable이라는 프로토콜을 채택해야 하는데, 커스텀 객체를 collection에 사용하기 위해 필요하다.
struct label {
var text: String
var font: UIFont
func draw() {}
}
String 타입은 Character들을 힙에 저장하며, UIFont 또한 클래스로 만들어진 객체이므로 래퍼런스 카운트가 필요하다.
uuid를 String 타입이 아닌 UUID로 변경하여 래퍼런스 줄인다. UUID는 struct 타입이다. String 대신 enum을 사용하여 해결할 수도 있다.
Method Dispatch는 프로그램이 어떤 메소드를 호출할 것인지 결정하여 그 메소드를 호출하는 과정을 뜻한다. 어떤 메소드인지 결정되는 시점에 따라 static과 dynamic으로 나뉜다.
컴파일 시점에 컴파일러가 메소드의 실제 코드 위치를 파악할 수 있어, 런타임에 찾는 과정 없이 바로 코드를 실행한다. 구현된 코드들이 어디서 실행되는지 알 수 있기에 메소드 인라이닝과 같은 코드 최적화를 시행한다. 메소드 인라이닝(Method Inlining)이란 메소드를 호출할 때 해당 메소드로 이동하지 않고 메소드의 결과값을 바로 반환하여 성능을 향상시킨다.
컴파일 시점에 어떤 메소드를 호출하는 지 알 수 없어, 런타임에 table을 참조하여 해당 메소드에 대한 정보를 가져와서 코드를 실행한다. Static Method Dispatch보다 많은 비용을 필요로 하지 않고, 래퍼런스 카운팅, 힙 할당과 같은 쓰레드 동기 오버헤드가 없다. 하지만 컴파일러는 Static은 최적화 작업이 가능하지만, Dynamic Dispatch에는 추론할 수 없다.
이유는 다형성 때문이다. 다형성(Polymorphism)이란 하나의 객체가 여러 타입을 가질 수 있는 것을 의미한다.
위의 사진을 보면, Point, Line 클래스가 Drawable를 상속하고 있다. 이 상황에서 d.draw의 d가 Point인지, Line인지 알기가 힘들다. 이를 해결하기 위해 컴파일러는 클래스에 타입 정보에 대한 포인터를 저장하는 virtual method table를 추가한다. static memory에 저장하며, 이 테이블을 통해 메소드를 호출한다.
서브 클래스를 만들지 않는다면, final을 클래스 앞에 선언한다. 그러면 컴파일러가 static하게 dispatch할 수 있다. 또한 서브클래스를 만들지 않는다라는 의도도 보여줄 수 있다.