표준 스위프트 값 유형은 암시적 복사 및 ARC를 사용하여 힙에 할당된 자원을 관리하며, 값을 함수 경계 간에 자유롭게 복사할 수 있도록 허용합니다. 반면 ~Copyable로 선언된 구조체(비복사 가능)는 암시적 복사를 전면적으로 금지하며, 고유한 소유권을 강제합니다. 이러한 구조체가 함수에 전달될 때, 스위프트는 명시적인 소유권 주석을 요구합니다: consuming은 소유권을 호출자에게 영구적으로 이전하고, borrowing은 이동하거나 복사하지 않고 임시로 읽기 전용 접근을 허용하며, inout은 임시로 독점적인 가변 접근을 제공합니다. 이 모델은 이동 전 사용 또는 이중 복사 오류에 대한 컴파일 타임 안전성을 보장하고 ARC 오버헤드를 제거합니다.
우리는 2MB 시장 데이터 패킷이 커널 공간 DMA 버퍼를 나타내는 고주파 거래 애플리케이션을 개발하고 있었으며, 이 버퍼는 일관성과 성능을 위해 고유해야 했습니다.
문제: 이 버퍼를 처리 단계(네트워크 수신, 검증, 전략 엔진) 간에 기본 메모리를 복사하거나 핫 패스에서 참조 카운팅을 유발하지 않고 전달하는 것이 었습니다. 표준 클래스는 용납할 수 없는 ARC 지연을 초래했으며, 수동 불안전 포인터는 메모리 누수 및 덴들링 참조의 위험을 안고 있었습니다.
해결책 1: 참조 카운트 클래스. 우리는 버퍼를 deinit 핸들러가 있는 클래스로 감싸는 것을 고려했습니다. 장점으로는 친숙한 메모리 관리 및 쉬운 공유가 있었습니다. 그러나 단점은 심각했습니다: 구성 요소 간의 모든 통과는 원자적 유지/해제를 트리거하여 캐시 지역성을 파괴하고 100마이크로초 대기 시간 요구를 위반했습니다.
해결책 2: 비정상적인 원시 포인터. UnsafeMutablePointer<UInt8>을 사용한 수동 할당으로 ARC를 완전히 피할 수 있었습니다. 장점은 오버헤드가 없고 완벽한 제어가 가능하다는 점이었고, 단점은 컴파일 타임 안전성이 보장되지 않았습니다. 개발자는 버퍼를 쉽게 이중 해제하거나 해제된 메모리에 접근하여 생산 환경에서 충돌을 일으킬 수 있었습니다.
해결책 3: 소유권 수식어가 있는 비복사 가능 구조체. 우리는 포인터를 포함하는 struct MarketDataBuffer: ~Copyable를 정의했습니다. 버퍼를 받는 함수는 소유권을 취득하기 위해 consuming을 사용했으며(예: func process(_ buffer: consuming MarketDataBuffer)), 검사 함수는 borrowing을 사용했습니다(예: func validate(_ buffer: borrowing MarketDataBuffer)). 이렇게 하면 고유 소유권의 컴파일 타임 강제가 제공되며 런타임 오버헤드는 0입니다.
선택한 솔루션과 결과: 우리는 솔루션 3을 선택했습니다. 그 결과 컴파일러가 우발적 복사 및 이동 이후 사용 오류를 방지하는 결정론적 데이터 파이프라인이 생성되었습니다. 시스템은 0의 ARC 트래픽으로 패킷을 처리하고 DMA 버퍼의 논리적 소유자가 항상 정확히 하나임을 보장하여 대기 시간 일관성을 크게 향상시켰습니다.
함수 매개변수를 consuming으로 표시하면 함수가 반환된 후 호출자가 비복사 가능 값을 사용할 수 있는 능력에 어떤 영향을 미치나요?
매개변수가 consuming으로 표시되면 함수는 진입 시 값의 소유권을 취득합니다. ~Copyable 유형의 경우 이는 복사가 아닌 파괴적인 이동을 의미합니다. 호출자는 값을 포기해야 하며, 함수 호출이 완료된 후 원래 변수는 초기화되지 않으며 접근할 수 없습니다. 이를 접근하려고 하면 컴파일 타임 오류가 발생합니다. 이는 선형 소유권을 강제하여 값이 수명 전반에 걸쳐 정확히 하나의 소유자가 있도록 보장합니다. 복사 가능 유형의 경우, consuming은 요구 사항을 충족하기 위해 암시적 복사를 촉발하지만 비복사 가능 유형의 경우 중복이 발생하지 않습니다.
왜 비복사 가능 유형은 스위프트 6.0 이전의 표준 제네릭 컬렉션인 Array에 저장될 수 없나요?
스위프트 6.0 이전에는 표준 라이브러리의 제네릭 유형이 암시적으로 유형 매개변수가 Copyable을 준수해야 했습니다. 비복사 가능 유형은 ~Copyable 제약을 사용하여 명시적으로 Copyable을 선택해제하기 때문에 이러한 암시적 요구를 위반하여 Array 또는 Optional에 저장될 수 없었습니다. 스위프트 6.0은 비복사 가능 제네릭을 도입하여 컨테이너가 ~Copyable 제약을 전파하여 비복사 가능 요소를 조건부로 지원할 수 있도록 하였습니다. 그러나 append와 같은 작업은 consuming 의미를 사용해야 하며, 비복사 가능 요소를 포함하는 경우 컬렉션 자체는 비복사 가능해져 API 경계에서의 소유권 관리를 신중하게 해야 합니다.
비복사 가능 유형에 적용할 때 borrowing 매개변수 수식어와 전통적인 inout 수식어의 차이는 무엇인가요?
borrowing 수식어는 소유권을 이전하지 않고 값에 대한 임시 불변 접근을 부여합니다. 호출자는 값을 유지하며 함수가 반환된 후에도 계속 사용할 수 있습니다(함수 내에서 소비되지 않은 경우). 반면 inout은 가변 대출을 나타내며: 독점 접근이 필요하고 값을 함수에 이동하여 호출 기간 동안 변형을 허용한 후 다시 이동합니다. 비복사 가능 유형에 대해 borrowing은 소유권을 relinquish하지 않고 읽기 전용 검사를 수행하는 데 필수적인 반면, inout은 수정에 필요합니다. 특히, borrowing은 함수가 값을 소비하거나 이동하는 것을 방지하고, inout은 값을 호출자에게 유효하고 수정된 상태로 반환할 것을 보장합니다.