Putting it together
주요기능 구현 살펴보기
Dynamically sized type
C 구조체의 크기는 항상 고정적이지만 Swift 유형의 크기는 런타임에 결정될 수 있습니다 두 가지 경우가 있습니다
첫째, SDK의 많은 유형은 미래의 OS 업데이트에서 저장된 속성이 추가되거나 변경될 수 있으며 Foundation의 URL 같은 유형도 여기에 포함되죠 따라서 이러한 유형의 레이아웃은 컴파일 타임에 알려지지 않는 것으로 취급해야 합니다
두 번째로 제네릭 유형의 유형 매개변수는 어떤 가능한 표현의 어떤 유형으로든 대체될 수 있어야 하므로 마찬가지로 레이아웃을 모르는 것으로 취급해야 합니다
두 번째 규칙에는 예외가 있는데 유형 매개변수가 특정 클래스로 제한되면 클래스 유형의 표현을 가져야 한다는 것을 알 수 있습니다
즉, 항상 포인터여야 하죠 이렇게 하면 제네릭으로 대체되지 않더라도 훨씬 효율적인 코드가 됩니다 제한 사항을 수용할 수 있다면 말이죠
예를 들어 첫번째 Connection 구조체에는 URL이 있습니다 URL의 레이아웃을 정적으로 알 수 없으므로 Connection의 레이아웃도 정적으로 알 수 없습니다 하지만 괜찮습니다 Connection을 담고 있는 컨테이너에 문제가 될 뿐이니까요 컴파일러는 Connection의 첫 번째 동적 크기 속성에 도달할 때까지의 정적 레이아웃을 파악할 수 있습니다 나머지 레이아웃은 Swift가 런타임에 프로그램에서 처음으로 이 유형의 레이아웃이 필요할 때 동적으로 채웁니다
URL이 24바이트가 되는 경우 런타임에 Connection의 레이아웃은 컴파일러가 정적으로 레이아웃을 알 수 있었을 때와 동일하게 구성됩니다 상수를 사용할 수 있는 대신 컴파일러는 크기와 오프셋을 동적으로 불러와야 할 뿐입니다
하지만 일부 컨테이너는 크기가 상수여야 합니다 이러한 경우에는 컴파일러가 값에 대한 메모리를 컨테이너의 할당과 별개로 할당해야 합니다
Async functions
비동기 함수의 핵심 아이디어는 C 스레드는 귀중한 리소스이며 실행을 차단하기 위해 C 스레드를 지연하면 리소스가 낭비된다는 것입니다 그 결과 비동기 함수는 두 가지 특수한 방식으로 구현됩니다
먼저 로컬 상태를 C 스택과 다른 별개의 스택에 유지합니다
두 번째로 런타임에 실제로 여러 함수로 나뉘어 실행됩니다
모든 로컬 함수는 중단점 이후에도 사용되므로 C 스택에 저장할 수 없습니다
동기 함수는 스택 포인터에서 값을 빼서 로컬 메모리를 C 스택에 저장한다고 설명했습니다
비동기 함수도 개념적으로 동일하게 작동하지만 크고 연속적인 스택 공간을 할당하지는 않습니다
비동기 작업이 하나 이상의 메모리 slab(조각)을 관리합니다
비동기함수가 스택에 메모리를 할당하려할떄 task에 메모리 요청
스택이 현재 slab에서 메모리 제공하려고하며 가능하면 task의 slab해당부분을 사용중으로 표시하고 함수에 제공
하지만 슬랩 대부분이 점유된 상태라면 할당할 공간이 없을 수 있습니다 이 경우 작업이 malloc으로 새 슬랩을 할당하며
여기에서 메모리를 할당합니다
두 경우 모두 할당 해제 시에는 메모리를 작업에 다시 돌려주며 비어 있는 것으로 표시됩니다
이 할당자는 한 작업에서만 사용되고 스택 원칙을 사용하므로 일반적으로 malloc보다 속도가 훨씬 빠릅니다 전반적인 성능 프로파일은 동기 함수와 비슷하나 호출에 대한 오버헤드가 약간 더 많습니다
실제로 실행되려면 비동기 함수는 부분 함수로 나뉘어 함수가 중단될 수 있는 중단점 사이를 채워야 합니다 여기에서는 함수 내에 await이 하나 있으므로 두 개의 부분 함수로 나뉩니다
나머지 부분 함수가 await 이후에 실행됩니다 먼저 기다렸던 작업의 결과를 출력 배열에 추가하고 루프를 계속 실행하려고 합니다 더 이상 작업이 없으면 비동기 함수 호출자에게 반환됩니다 작업이 더 있다면 루프를 반복하고 다음 작업을 기다립니다
여기에서 핵심은 C 스택에 최대 1개의 부분 함수만 존재한다는 겁니다
작업이 실제로 중단되어야 하면 C 스택에서 정상적으로 반환되고 일반적으로 바로 동시성 런타임으로 넘어가 스레드를 즉시 다른 작업에 사용할 수 있습니다
Closures
클로저는 함수 유형의 값을 전달하는 데 사용됩니다 이 함수는 탈출 불가 함수를 인수로 받습니다 Swift에서 함수 값은 항상 함수 포인터와 컨텍스트 포인트의 쌍으로 구성됩니다 C에서는 이 함수의 시그니처가 다음과 같이 표현됩니다
Swift에서 함수 값을 호출하면 단순히 함수 포인터를 호출하여 묵시적으로 컨텍스트 포인터를 추가 인수로 함께 전달합니다
둘러싸인 범위의 값을 사용하는 클로저 표현식에서는 해당 값을 컨텍스트에 전달해야 합니다 이 작동 방식은 출력해야 하는 함수 값의 유형에 따라 달라집니다
여기에서는 함수가 탈출 불가 함수이므로 호출이 완료된 후에 함수 값이 사용되지 않는다는 것을 알 수 있습니다 따라서 값의 메모리를 관리할 필요가 없고 컨텍스트를 범위 내에서 할당할 수 있습니다
컨텍스트를 스택에 할당할 수 있고 그 주소를 sumTwice에 전달하면 됩니다
클로저 함수에서는 짝을 이루는 컨텍스트의 유형을 알고 있으므로 필요한 데이터를 가져오기만 하면 됩니다
탈출 클로저에서는 다르게 작동합니다 이제는 클로저가 호출 안에서만 사용될지 알 수 없으므로 컨텍스트 객체를 힙에 할당하고 소유권을 획득하고 해제하여 관리해야 합니다
컨텍스트가 기본적으로 익명 Swift 클래스의 인스턴스처럼 작동합니다(class)
Swift의 클로저에서 로컬 변수를 참조할 때는 변수의 레퍼런스를 사용합니다 따라서 변수를 수정할 수 있으며 기존 범위와 현재 범위에 변경사항이 모두 반영됩니다(addend은 Ref타입)
탈출 불가 클로저에서만 변수를 가져오면 변수의 수명이 변하지 않습니다 따라서 클로저가 변수의 할당 공간에 대한 포인터만 가져와 이를 처리할 수 있습니다
하지만 탈출 불가 클로저에서 변수를 가져오면 클로저의 수명만큼 변수의 수명이 늘어날 수 있습니다
Generics
이러한 유형의 레이아웃은 정적으로 알 수 없고 컨테이너에 따라 다르게 처리된다고 이미 설명해 드렸습니다
Swift 프로토콜은 런타임에 함수 포인터 테이블로 표현됩니다 프로토콜의 각 요구사항당 하나죠 C에서는 테이블이 대략 이렇게 표현됩니다
프로토콜 제약이 있을 때마다 적절한 테이블에 포인터를 전달합니다
이와 같은 제네릭 함수에서는 유형 및 감시 테이블이 숨겨진 추가 매개변수가 됩니다 이 런타임 시그니처의 모든 항목이 기존 Swift 시그니처의 항목에 각각 직접 대응됩니다
프로토콜 유형의 값을 사용할 때는 다르게 작동합니다 이번에는 더 유연한 함수입니다 배열의 각 요소가 서로 다른 유형의 데이터가 될 수 있습니다 하지만 이렇게 하면 실행 효율성이 떨어집니다
AnyDataModel 같은 프로토콜의 인라인 표현을 C로 나타내면 이렇습니다 값을 저장할 공간이 있고 값의 유형과 준수할 사항을 저장할 필드가 있습니다
하지만 이는 고정 크기를 가져야 합니다 표현에서 크기를 변경하며 다양한 유형의 데이터 모델을 지원할 수 없습니다
값을 저장할 공간을 얼마나 크게 만들건 맞지 않는 데이터 모델이 있을 수 있습니다
어떻게 해야 할까요?
Swift는 임의 버퍼 크기의 포인터 3개를 사용해 프로토콜 유형에 저장된 값을 해당 버퍼에 넣을 수 있다면 인라인 방식으로 집어 넣습니다
그렇지 않으면 힙에 값을 위한 공간을 할당하고 그 포인터를 버퍼에 저장합니다
이 함수 시그니처들은 매우 비슷해 보이지만 실제로는 매우 다른 특성을 가지고 있습니다
첫 번째 함수(<Model: DataModel>)는 같은 유형의 데이터 모델로 구성된 배열을 받습니다 데이터 모델은 배열에 효율적으로 채워지며 별도의 최상위 수준 인수로 함수에 유형 정보가 함수에 한 번 전달됩니다
호출자가 호출되는 유형을 알 수 있다면 함수를 특수화할 수도 있습니다 여기에서는 알려진 유형의 배열로 호출하고 있습니다 이러한 호출은 옵티마이저가 손쉽게 인라인 처리하거나 정확히 이 인수 유형에 특수화된 버전의 함수를 생성할 수도 있습니다 이렇게 하면 제네릭과 관련된 추상화 비용이 제거되며 MyDataModel이 준수하는 구현에서 직접 update를 호출할 수 있습니다
두 번째 함수([any DataModel])는 다양한 데이터 모델로 구성된 배열을 받습니다 더 유연하죠 다양한 유형의 데이터 모델을 사용한다면 이렇게 해야 할 겁니다 그러나 배열의 요소들이 각자 다른 동적 유형을 가져 배열에 값이 조밀하게 채워지지 않습니다
실제로 이를 최적화하는 게 훨씬 더 어렵습니다 데이터가 배열에 어떻게 흘러 들어가고 함수에서 사용되는지 컴파일러가 완벽하게 추론해야 합니다 그렇다고 성능이 완전히 저하되는 건 아니지만 이 한 부분에서는 컴파일러의 도움을 별로 받지 못한다는 겁니다
무조건 프로토콜을 사용하면 안된다라기보다
성능적인 측면을 생각하여 trade off하며 적절하게 골라쓰라는 내용이였습니다
'iyOmSd > Title: Swift' 카테고리의 다른 글
[SwiftUI] WWDC24 Enhance your UI animations and transitions (0) | 2024.11.12 |
---|---|
[Swift] WWDC24 Consume noncopyable types in Swift (0) | 2024.11.11 |
[Swift] WWDC24 Explore Swift Performance(1) (1) | 2024.11.09 |
[Swift] Xcode16 빌드시 CUICatalog initWithName:fromBundle:error: 런타임 에러 (0) | 2024.11.07 |
[Swift] Python을 이용한 Excel -> Json 맵핑 스크립트 만들기 (1) | 2024.09.29 |