스위프트는 indirect 키워드를 통해 값 타입의 열거형에서 무한 재귀를 가능하게 하며, 이는 특정 경우가 연관 값을 힙에 할당된 참조 카운트 박스에 저장하도록 강제합니다. 케이스가 indirect로 표시되면 컴파일러는 인라인 페이로드 저장소를 ARC에 의해 관리되는 힙 할당 컨테이너를 가리키는 포인터로 변환합니다. 이 간접 참조는 열거형이 크기 무한 확장 없이 재귀적으로 자신을 참조할 수 있게 하며, 컴파일러는 전체 값을 인라인으로 저장하는 대신 포인터만 저장하면 됩니다.
그러나 이 변환은 패턴 매칭 성능에 상당한 영향을 미칩니다. indirect 케이스에 대한 각 접근은 페이로드에 도달하기 위해 포인터 추적이 필요하며, 이는 열거형이 전부 스택에 저장될 때에 비해 CPU 캐시 지역성을 악화시킵니다. 또한 힙 할당은 병렬 맥락에서 동기화 오버헤드를 증가시키는 원자적 유지 및 해제 작업을 도입하지만, 열거형 자체는 언어 수준에서 값 의미론을 유지합니다.
indirect enum Expression { case literal(Int) case add(Expression, Expression) case multiply(Expression, Expression) } // 패턴 매칭은 역참조가 필요합니다 func evaluate(_ expr: Expression) -> Int { switch expr { case .literal(let value): return value case .add(let left, let right): return evaluate(left) + evaluate(right) case .multiply(let left, let right): return evaluate(left) * evaluate(right) } } }
우리는 깊이 중첩된 논리 표현식을 처리해야 하는 구성 엔진을 위한 도메인 특정 언어 파서를 개발하고 있었습니다. 초기 구현은 indirect 주석이 없는 표현 AST를 나타내기 위해 재귀적 열거형을 사용했으며, 이는 중첩 깊이가 몇 천 레벨을 초과하는 구성 파일을 처리할 때 즉시 스택 오버플로우 충돌을 유발했습니다.
고려된 첫 번째 해결책은 부모 및 자식 참조가 있는 클래스 기반 트리 구조로 열거형을 완전히 포기하는 것이었습니다. 이 접근 방식은 재귀적 관계에 대한 자연스러운 힙 할당을 제공했을 것입니다. 그러나 우리는 이것이 값 의미론을 희생시켜 복잡한 방어적 복사 또는 잠금 메커니즘을 구현하지 않고는 동시 컴파일 스레드 간에 구문 분석된 서브 트리를 안전하게 공유할 수 없게 만들기 때문에 거부했습니다.
우리는 두 번째 해결책인 indirect를 열거형의 재귀적 케이스, 즉 자식 표현식을 포함하는 케이스에 구체적으로 적용하기로 선택했습니다. 이것은 값 의미론을 유지하면서 무한 재귀를 위해 필요한 경우에만 힙 할당을 강제했습니다. 이 거래는 불변성 보장과 타입 안전성을 유지했기 때문에 수용 가능했으며, 자주 수정되는 표현 트리에 대해 사용자 정의 복사-쓰기 최적화를 구현해야 했습니다.
결과적으로 임의로 깊은 중첩을 처리할 수 있는 안정적인 파서가 탄생했습니다. 이후 프로파일링을 통해 indirect 케이스에서의 패턴 매칭이 포인터 간접 참조와 ARC 트래픽 때문에 약 20% 더 많은 CPU 사이클을 소모한다는 사실이 밝혀졌고, 우리는 일반적인 경우에 대해 비간접적인 보조 열거형으로 작은 고정 깊이 구조를 평면화함으로써 이를 완화했습니다.
indirect가 스위프트의 복사-쓰기 최적화와 어떻게 상호 작용합니까?
많은 후보자들은 indirect 케이스가 항상 전체 재귀 구조의 깊은 복사를 유발한다고 가정합니다. 실제로 스위프트는 간접 페이로드가 포함된 힙 박스에 복사-쓰기 의미론을 적용합니다. indirect 케이스가 있는 열거형이 새 변수에 할당될 때, 컴파일러는 내용을 복사하는 대신 힙 박스 참조를 유지합니다. 페이로드는 변이 작업이 발생하고 참조 카운트가 1을 초과할 때만 복사됩니다. 이 최적화는 큰 재귀 구조의 성능에 매우 중요하지만, 참조 카운팅 자체가 원자적이지만 복사-쓰기 논리가 스레드 간 동기화를 요구하기 때문에 스레드 안전성을 다룰 때 세심한 주의가 필요합니다.
indirect를 전체 열거형이 아닌 개별 케이스에 적용할 수 있으며, 이는 메모리 레이아웃에 어떤 영향을 미칩니까?
후보자들은 종종 indirect가 전체 열거형 선언에 적용되어야 한다고 믿습니다. 그러나 스위프트는 개별 케이스를 indirect로 표시하는 것을 허용하며, 이는 메모리 레이아웃에 상당한 영향을 미칩니다. 특정 케이스가 indirect로 표시되면 열거형은 각 간접 케이스가 힙 박스를 가리키는 단어 크기 포인터를 차지하고, 비간접 케이스는 열거형의 메모리 풋프린트 내에서 페이로드를 인라인으로 저장하는 태깅 포인터 표현을 사용합니다. 이러한 혼합 표현은 재귀가 필요한 특정 케이스만 있는 열거형에 대한 메모리 사용을 최적화합니다. 그러나 이는 패턴 매칭에서 복잡성을 도입하며, 컴파일러는 인라인 대 간접 페이로드에 대해 서로 다른 접근 코드 경로를 생성해야 하며, 열거형의 전체 크기는 가장 큰 인라인 페이로드와 태그 비트에 의해 결정됩니다. 간접 케이스 크기는 아닙니다.
indirect가 포함된 재귀 열거형이 클로저와 관련되어 있을 때 왜 유지 사이클을 생성할 수 있으며, 이는 표준 값 타입 동작과 어떻게 다릅니까?
이는 ARC에 대한 깊은 이해를 드러내는 미묘한 점입니다. 일반적으로 열거형과 같은 값 타입은 정체성 및 값 수준의 참조 카운팅이 부족하기 때문에 유지 사이클을 생성할 수 없습니다. 그러나 어떤 케이스가 indirect로 지정되면 페이로드가 힙에 할당되고 참조 카운트가 발생합니다. 만약 indirect 케이스의 연관 값에 열거형 자체를 캡처하는 클로저가 포함되어 있고, 그 클로저가 다시 열거형의 연관 값에 저장된다면, 힙 박스와 클로저 간에 유지 사이클이 발생합니다. 이는 사이클이 열거형 값 자체가 아닌 힙 할당 박스에 존재하기 때문에 클래스 기반 사이클과는 구별됩니다. 사이클을 끊으려면 [weak self] 또는 [unowned self]와 같은 캡처 리스트를 사용해야 하지만, 열거형은 일반적으로 값 타입이기 때문에 개발자들은 종종 indirect가 페이로드에 참조 의미론을 도입하여 클로저를 처리할 때 클래스와 동일한 경계를 요구한다는 것을 잊곤 합니다.