Swift) 성능 최적화

ScutiUY·2022년 8월 30일
1

평소에 하던 생각이 있다.

'모바일에는 한계가 있다'

하드웨어의 스펙은 나날이 발전한다.
요즘의 아이폰 스펙은 어지간한 pc 못지않다.

게다가 대부분의 어플리케이션은 인터넷을 사용하는 네트워크 통신을 필요로하는 서비스이다.
이때 서비스의 performance를 올릴수 있는 방법은 벡엔드 단에서의 데이터처리이다.

네트워크 기반 서비스는 아무리 로컬에서 구현된 로직이 느려도 네트워크 환경이 좋다면 빠르게 느껴진다.

그럼 정말 클라이언트(앱)에서 할 수 있는게 없을까?
우리가 어떤 앱을 사용할때 이 앱 실행 속도 빠르다 라는 경험을 한 경우가 있을것이다.

그럼 이는 단순히 네트워크 속도가 빨라서일까?

이미지를 처리 하는 어플리케이션이 있다고 가정하자.

사이즈가 매우 커서 네트워크 환경이 아무리 좋아도 받아오는 속도가 느리다.

이때 우리가 최적화 할 수있는 것은 아이폰 사이즈에 맞는 사진을 받아오거나, 적당한 사이즈의 이미지를 받아와서 app단에서 compression을 해주는 것이다.

그럼 네트워크가 느린데 앱의 성능까지 최악인 어플이 있다면?
iOS 개발자는 iOS 개발자가 할 수 있는 최선을 다해야한다.

이러한 로직 혹은 알고리즘 관점의 성능 최적화 말고 swift로 할 수 있는 성능 최적화를 생각해보자.

  • Memory Allocation: Stack이냐 heap이냐 - Class vs Struct
  • Reference Count: No or Yes - heap을 사용하는가?
  • Method Dispatch: Static or Dynamic - Access control

Memory Allocation 메모리 할당

Stack vc Heap

우리가 작성한 코드는 어떤식으로 실행 될까?

기본적인 것이다.

빌드 과정에서 전처리를 한다. 물론 swift는 전처리가 없지만 llbuild를 통해 참조 관계를 해결한다.

그 다음 컴파일러를 통해 작성한 코드를 기계어로 바꿔준다.

이 과정에서 즉, 컴파일 단계에서 우린 어떤식으로 최적화를 해야할지 봐야한다.

우리가 작성한 코드는 OS에서 컴파일을 하며 프로세스가 실행 되면서 메모리 공간을 할당 해준다.

메모리 공간

우리가 볼 영역은 Heap과 Stack 영역이다.

Heap과 Stack은 기본적으로 메모리를 저장하는 구조이다.

우리가 작성한 코드를 메모리 영역에 저장하는것에 있어서 더 빠른 메모리 구조를 선택한다? 그럼 그것이 최적화이다.

Stack

Stack은 선형의 단순한 구조이면 시간 복잡도가 O(1)이다. 즉, 할당과 해제가 빠르고 효율적이다.

하지만 컴파일 과정에서 메모리 크기를 미리 알수 있어야 한다. 메모리 크기를 알아야 다음에 스택에 올라갈 메모리를 알 수 있다.

Heap

Heap 영역에는 임의의 주소값에 해당 메모리를 할당한다.

임의의 값에 해당하기 때문에 주소를 찾아가는 것에 대한 오버헤드가 크다.

또한 메모리가 올라 갔는지 아닌지 계속 추적하는 Reference Counting을 해야한다.

하지만 Heap은 런타임에 크기가 변할 수 있고, 이는 객체지향에 알맞다. 또한, 참조이기 때문에 여러 곳에서 접근이 가능하다.

class? struct?

class는 Heap 영역에
struct, enum, tuple 그리고 기본 타입들은 stack
이런식의 단순한 이분법적인 지식이였다.

Swift에선 어떤 것을 기준으로 메모리 저장 영역을 나눌까?

Swift는 Semantics를 기준으로 저장 영역을 나눈다.
semantics는 어떤 타입인지, 내부적으로 어떻게 저장 되는지를 나누는 기준이다.

class와 function, closure는 Reference Semantics이다.
struct, enum, tuple 등과 Swift 기본 타입들은 Value Semantics이다.

이 둘의 가장 큰 차이점은 저장 영역의 차이이고, 이 부분에서 할당/해제/접근 속도의 차이 메모리 공간의 차이가 나타난다.

Value 타입이지만 Heap에 할당 되는 경우 : heap allocation

위에서 언급했듯 class, function과 closure 등을 제외한 swift의 기본 타입(String, Int, Float..)들은 값 타입이다.

당연히 값 타입이라고 생각했던 것들이 heap 영역에 저장되는 경우가 있다.

바로 Collection이다.

Collection Type에는 Array, Set, Dictionary 등이 있다.
공식 문서를 보면 String 또한 Char의 collection이므로 Collection에 포함된다.

이들은 크기가 가변적이므로 stack 영역에 정확히 메모리를 할당 시킬수 없다.
또한 런타임에 크기가 변할 수 있기 때문에 heap 영역에 저장한다.

Reference Count 참조 카운트

위에 언급 했듯 reference 타입은 reference counting을 발생시킨다고 했다.
reference count는 말 그대로 참조 된 수이다.

class의 인스턴스가 생성되는 것은 물론이고, 이것이 어딘가에 다시 할당(복사) 된다면 reference count가 올라간다.

그럼 reference counting은 왜 안 좋을까?

당연하다 class를 할당 하는것 만으로 reference count를 하고 해제 할때도 reference count를 감소 시켜야 한다.

또한 하나의 인스턴스에 여러 참조가 되어 있다면 해당 인스턴스가 해제 될때 참조 된 수만큼 reference count를 감소시켜야 한다.

이는 overhead를 유발하고 성능에 악영향을 미친다.

만약 reference type을 value type으로 사용할 수 있다면 성능에 도움이 된다.

class의 reference count

class Person {
	var name: String
    var age: Int
    
    init(name: String, age: Int) {
    	self.name = name
        self.age = age
    }
}

p1 = Person(name: "a", age: 20)
p2 = p1

p1.name = "b"

class로 구현된 사람 객체이다.
이름과 나이를 프로퍼티로 갖는다.

class는 무조건 heap 영역만 쓰는것이 아니라 heap영역의 메모리 주소값을 stack영역에 저장한 후 그 주소값을 가지고 heap 메모리를 찾아가서 reference로 사용한다.

class Point {
	var x: Double
    var y: Doble
    
    init(x: Double, y: Double) {
    	self.x = x
        self.y = y
    }
}

p1 = Point(x: 10.0, y: 20.0) // Person에 대한 reference로 count 1 증가
p2 = p1 // p1이 가리키는 heap 영역의 주소값 대한 reference count 1 증가

p1.name = "b" // p1 p2가 동시에 가리키는 있는 heap에 접근

새로 생성된 Person이라는 class는 p1에 할당 되면서 해당 인스턴스에 대한 reference count가 증가한다.
p2에 p1을 할당 하면서 p1이 가리키는 heap의 주소값을 p2도 가리키며 reference count가 1 증가한다.
즉, 해당 영역을 가리키는 주소값의 reference count 2이다.

메모리를 해제 할 때 또한 해당 주소값에 대한 reference count가 독립적으로 감소하며, 총 두번의 reference count 감소가 이루어진다.

Struct의 reference count

그럼 struct를 쓰면 무조건 reference count에 대하여 안전할까?

답은 그렇지 않다이다.

struct Person {
	var name: String
    var address: String
    
    init(name: String, address: String) {
    	self.name = name
        self.address = address
    }
}

메모리 할당 부분에서 언급 했드시 String은 Heap 영역에 할당된다.
따라서 구조체라도 Heap영역에 할당 되는 프로퍼티를 가지고 있다면 스택 영역에 할당 된 struct 구조체의 프로퍼티가 가리키는 영역은 heap 영역이다.

구조체는 기본적으로 레퍼런스를 사용하지 않지만, 구조체가 레퍼런스를 가지게 되면 reference counting으로 오버헤드(overhead)를 처리하는 비용이 들게 된다.

Method Dispatch

우리가 코드가 어떤 순서로 실행되는지 실행 되는 단계에 대해서 언급했다.

프로그램을 실행하는데 있어서 성능에 영향을 미치는 중요한 요소중 하나가 오버 헤드이다.

오버헤드란?
오버헤드(overhead)는 어떤 처리를 하기 위해 들어가는 간접적인 처리 시간 · 메모리 등을 말한다.
이번에 말하는 오버헤드는 실제 실행되는 메서드를 코드를 훑으며 찾는 과정이다.
즉, 오버헤드가 발생하면 비용이 발생한다.

swift는 프로그램을 실행하며 어떤 메소드를 호출 시킬지 파악 할 수 있다.

이 메소드가 어떤 Dispatch를 사용하느냐에 따라서 성능을 개선할 수 있다.

언제 어떤 메소드를 실행 시킬지를 결정하는 것이 Method Dispatch이다.

메소드가 실행되는 시점에 따라 Static Dispatch와 Dynamic Dispatch로 나눈다.

Static Dispatch

Static Dispatch는 컴파일 단계에서 컴파일러가 메소드의 위치(실제 코드에서의 위치)를 알고 있으므로 런타임에 따로 찾을 필요 없이 바로 실행 할 수 있다.

즉, 바로 결과 값을 반환 할 수 있다는 것이다.
이를 메소드 인라이닝 이라고 한다.

따라서 오버헤드가 발생하지 않는다.

Dynamic Dispatch

Dynamic Dispatch는 class에서 발생한다.

왜 class에서 발생하는가?

Static Dispatch는 컴파일 단계에서 어떤 메소드가 실행 될지 알고 있다고 했다.
실행되는 메소드가 고유하기 때문이다.

class는 다형성을 가지고있다.

때문에 동일한 메소드가 있다면 컴파일 단계에서 어떤 메소드를 실행시킬지 모르며 런타임에 물어 물어 찾아가야한다.

먄약 부모 class 상속을 할수 있는 자식 클래스가 있다고 가정하자.
class의 메소드를 오버라이딩 한다면 해당 메소드의 타입은 모두 같다.
따라서 컴파일 시점에 어떤 클래스의 메소드인지 알 수 없기 때문에 런타임에 찾아야 한다.

그럼 어떻게 어떤 메소드를 실행하는지 컴파일러가 알 수 있을까?

컴파일러는 해당 클래스마다 vTable이란 table을 만든다.
table은 클래스가 가지고 있는 메소드의 주소값을 가지고 있는 table이다.
런타임 시점에 table을 사용하여 어떤 메소드가 불릴지 결정한다.

잠깐..? class가 상속을 안하면 고유한 메소드만 존재하는게 아닌가?

class는 언제나 다형성의 가능성을 열어둔다.
때문에 항상 Dynamic Dispatch를 사용한다.

런타임 시점에서 vTable을 확인하고 그 주소값을 통해 실행될 메소드를 찾아가기 때문에 오버헤드가 발생한다.

Dynamic Dispatch 방지 - 접근 제한자

class는 상속의 가능성이 있기 때문에 언제나 dynamic dispatch를 사용한다고 했다.
그럼 상속의 가능성을 없앤다면?

  • 접근 제한자: final, private

class의 메소드나 프로퍼티가 상속되어 오버라이딩 될 가능성을 없애 버린다면 컴파일러는 해당 코드를 언제 실행할지 예측 할수 있다.

따라서 접근 제한자를 활용하면 class라도 Static Dispatch를 사용할 수 있다!!

class는 heap 영역에 할당되고 reference counting을 하지만 최소한 Static Dispatch를 사용 할 순 있다.

결론

Reference 타입이 무조건 나쁜건 아니다.
하지만 Heap 영역에 할당 하는 것은 많은 자원을 소모하기 때문에 할 수 있다면 Stack 영역에 저장 할 수 있다면 사용하는게 좋다.
Struct 또한 내부적으로 reference type을 쓴다면 오히려 성능적으로 안좋기 때문이다.

항상 private과 final을 생각하자.


참조

https://www.vadimbulavin.com/value-types-and-reference-types-in-swift/
https://developer.apple.com/videos/play/wwdc2016/416/

0개의 댓글