- Swift Performance-wwdc242024년 06월 17일
- 2료일
- 작성자
- 2024.06.17.:17
https://www.youtube.com/watch?v=nb3bRQa0iGQ&t=3s
1. 여기서 말하는 성능이란?
: 애플리케이션이 얼마나 빠르고 효율적으로 동작하는지를 나타내는 척도.
- 지연: 작업이 시작되고 완료될때 까지 걸리는 시간.
- 에너지 소모: 앱이 베터리 얼마나 사용하는지
- 메모리 사용: 앱이 얼마나 많은 메모를 점유하고 어떻게 관리하는지.
2. 낮은 수준의 성능을 볼 때 고려해야할 원칙들
bottom-up으로 생각을 해보자
- 최적화되지 않은 함수 호출들: 4가지의 cost가 있다고 한다. 이 중 3가지는 우리가 하는것.
1. 먼저 호출에 의한 인수를 설정(arguments)
가장 낮은 수준인 1번은 우리가 함수를 호출할때 인수를 넣어야한다. 하지만 최신 프로세서에선 레지스터를 활용해 이 비용이 거의 없다고 한다.
2. 호출하는 함수의 주소 확인
: 호출할 함수가 메모리 어디에 있는지 찾는 비용. 컴파일러가 미리 알면 빠르지만, 모르면 런타임에 확인해야해서 느릴 수. ㅣㅆ다.
3. 함수의 로컬 상태를 위한 공간 할당
: 함수 실행되면서 내부에 있는 로컬변수 뭐 let temp = request이런식으로 저장할 메모리 공간을 만드는 비용.
4. 이건 우리가 하지않는것인데 함수 호출자와 호출하는곳에서 최적화를 방해할 수 있다. Optimization restrictions
3번과 4번은 같은 모두 같은 문제로 귀결된다.
바로 컴파일시간에 우리가 호출하는 함수를 정확히 알 수 있는지다.
Swift는 강력한 optimizer가 있다. 그래서 컴파일러가 많은 costs들을 제거해주기에 이외의 요소들도 있지만 고려안해도된다.
여기서 static dispatch 와 dynamic Dispatch를 비교할 수 잇다.
Static Dispatch는 프로세서레벨에서 좀 더 빠르고 컴파일러가 함수 정의를 볼 수 있기에 많은 최적화가 있다.Dynamic Dispatch는 다형성과 추상화를 위한 도구인데 Swift는 특정 종류만 Dynamic Dispatch를 사용 바로
- 불투명한 함수를 부를때
- 클래스메서드를 오버라이딩 가능할때
- 프로토콜 요구사항을 부를때
- Objective-C나 C++함수부를때
이외에는 전부 Static Dispatch라고 한다.
프로토콜에서 메서드의 위치에 따른 Dispatch
여기서 보면 DataModel 프로토콜 유형 값을 업데이트하는 함수의 호출이 있다. 하지만 이게 static인지 dynamic인지는 아직 모른다. 그 이유는 메소드가 선언된 위치에 따라 달라지기 때문이다.
- 프로토콜의 메인 body에서 선언된 경우, 이 함수는 프로토콜 요구사항이된다. 즉 이 프로토콜을 상속할때 무조건 이 함수를 써야한다는 느낌? 그래서 이에 대한 호출은 dynamic Dispatch를 사용한다.
- 프로토콜의 extension에서 선언된 경우, static Dispatch를 사용한다.
왜? extension에서 메서드를 추가한경우에는 서브 클래스에서 오버라이딩이 불가능하기에!!!! 하지만 여기서 주의해야할 것이 있다.
protocol IsStaticOrDynamic { } extension IsStaticOrDynamic { func isStatic() { print("static dispatch") } }
이렇게 프로토콜에 정의되어 있지 않은 것들을 extension에서 추가한 경우 오버라이딩이 불가능하기에 static이지만
protocol IsStaticOrDynamic { func isDynamic() } extension IsStaticOrDynamic { func isStatic() { print("static dispatch") } func isDynamic() { print("DYnmaic DIspatch") } }
이렇게 protocol에 선언되어있는것을 extension을 통해 default메서드 구현해놓은 경우에는 어떤게 불려야할지 모르기에 dynamic Dispatch이다.
함수호출의 마지막 비용은 로컬 변수와 상태를 저장할 메모리를 할당하는 것이다. 하지만 여기서부터는 좀 어려워서 이해가 어려웠따....
함수가 호출되면 해당 함수는 실행을 위해 메모리가 필요하다.
위의 updateAll메소드는 동기함수다. 해당 메모리를 C stack에 할당한다고 한다. Cstack의 할당공간은 바로 stack Pointer에서 빼기만 하면 된다고 한다. 이 뺀 공간을 CallFrame이라고 부른다고 한다.
저거를 컴파일 하면 함수의 시작과 끝에서 스택포인터를 조작하는 어셈블리 코드를 얻는다.
함수에 들어가면, 스택포인터가 Cstack을 가리키고 있다. 그 후 어셈블리 코드의 동작을 해야한다.
sub 즉 스택포인터에서 208만큼 뺀다. 그리고 여기에 208바이트의 공간을 할당한다. 이공간을 CallFrame. 함수의 로컬변수및 상태를 여기에 저장한다.
리턴 직전에는 우리는 다시 208바이트를 추가하여 이전에 할당한 위치에서 메모리를 할당 해제한다.
CallFrame이 C구조체와 같이 이루어져있다고 생각할 수 있다. 이상적으로는, 함수의 모든 로컬 상태가 CallFrame의 필드가 된다. CallFrame안에 넣는것이 이상적이다라고 말한 이유는, 컴파일러가 항상 함수 시작시 뺄셈을 내보낼 것이기 때문이다. return 주소같은 중요한 것들을 저장할 공간이 필요하다. 큰 상수를 빼는 것조차도 시간이 얼마 안걸리기에 함수 메모리가 필요한 경우 CELLFRAME의 일부로 할당하는 것이 해제되는 것과 비슷하다.
-> 아아..이해완료 그니까 전역함수를 만들면 일일이 CallFrame에 할당해줘야하는데, 위와같이 Struct안에 넣으면 메모리 한번에 할당하고, 종료할때 한번에 해제하기 때문!!(이지않을까?)
예를 들어, 오른쪽 struct안에 넣은경우 1. CallFrame할당->2. models, source, model, iterator 로컬 변수 CallFrame에 저장 -> 3. 종료시 한번에 해제
- 메모리 할당
메모리는 총 3가지가 있다.
- Global
- 프로그램이 로드될때 할당하고 초기화한다.
- 글로벌 메모리의 큰 단점은 프로그램이 도중에 계속에서 살아있는 고정된 메모리의 특정 패턴에만 사용가능하다. (가변X)
- Stack
- 메모리의 범위가 지정되어야한다.
- Heap(힙에관한글은 진짜 바로직전글)
- 임의의 시간에 할당하고 임의의 시간에 할당해제.
- class나 actor인스턴스.
- 범위의 크기를 모를때도 ex) string, collectiontype 등
이 모든것은 Ram에 있지만 다른 패턴으로 각각을 할당하고 사용한다.
- 데이터가 표현되는 방법으로 인한 메모리 낭비(Memory layout)
메모리에 어떻게 저장되어있니? 어...값타입으로!
어 맞지 근데 여기서는 "representation"으로 설명한다. 이는 값이 메모리에 어떻게 보이는지를 말한다.
array변수는 버퍼개체에 대한 참조를 유지하는 메모리의 이름이다.
"inline representation"은 포인터 따라가지 않는 representation이라고 하자. 저 위에서 인라인 representation은 단일 버퍼참조이며 버퍼가 실제로 포함된 내용들을 무시한다. 측정을 해보면 64비트, 즉 8바이트다.
친숙하다 CallFrame. 로컬 범위에 포함된 배열값을 가지게 되는데, 이는 CallFrame에 인라인 표현에 배치한다. 실제 해당 포인터를 저장한다.
- 값타입을 복사하고 파괴.
이전에 array변수의 인라인 표현이 버퍼객체에 대한 참조라고 햇다.
consume: array변수를 초기화할때 초기값의 소유권을 변수로 이전해야한다.
array2에 넣어주면 값은 배열이므로 복사한다는 것은 해당 버퍼를 유지한다는 뜻. 만약 컴파일러가 여기서 array변수가 마지막으로 쓰이고 안쓰인다는 것을 알아? 복사본없이 여기에 값을 전송할 수 있어야 함.
var array2 = consume array라고 쓰면됨.
mutating: 이후에도 값의 소유권을 갖는다.
Borrowing: print와 같이 컴파일러가 저거에 대해 건드리지 않을것을 알고 빌려준다.
좀 더 높은 레벨에선, 함수의 컨벤션에 맞게 컴파일러가 값의 복사본을 추가해야할 수 있다.'
inline stoarge:
- heap할당을 피한다
- 작은 타입에 해야함. 전부 복사하기에 성능저하가 일어날 수 있다.
3. 어떻게 Swift의 핵심기능들이 구현되고 성능에 어떤 영향을 끼치는지
1. SDK의 많은 값타입들은 Foundation에서 URL과 같이 향후 OS업데이트에서 저장된 속성을 추가및 변경할 수 있는 권한을 보유한다. 이는 컴파일타임에 결정되지 않는다.
2. Generic type도 마찬가지로 런타임에 결정된다.
이럴때 첫번째 속성에 갈때까지는 레이아웃을 알고 있으나 이후에부터는 런타임에 의해 동적으로 채워진다. Cstack에서도 마찬가지로 96만큼만 가지고 있다가 런타임에 정해지면 다시 변수크기만큼빼서 stackPointer를 내려준다.
Async함수에서는 어떻게 작동할까?
1. CStack에서 별도의 스택에 로컬상태를 유지.
2. 런타임때 여러함수로 찢어진다.
여기서 로컬함수들은 정지지점을 cross하는데 쓰이므로 C스택에 저장할 수 없다.
마지막 제너릭
여기 두 updateAll의 차이가 뭘까?
첫번째꺼는 동일한 모델이 array에 들어간다. 타입에 대한 정보는 함수에게 한번만 전달된다.
두번ㅉ꺼는 뭐 다른 DataModel에 맞는 프로토콜이 전부들어올수있다. 당연히 최적화도 훨 어렵다. 컴파일러가 일일이 다 측정해야하기에..
사실 뒤로갈수록 너무 어려웠다...그래서 댓글에 각자 생각을 알려주시면 참고하도록 하겟슴다..
'면접준비' 카테고리의 다른 글
test Code (with TCA) (0) 2024.11.04 CLMonitor (1) 2024.10.22 힙메모리 분석 - WWDC24 (1) 2024.06.15 Uniform type Identifiers (1) 2024.04.18 매크로(Macros) (1) 2024.03.07 다음글이전글이전 글이 없습니다.댓글