iOS 스터디 Part6 Copy-on-Write Optimization
Last updated
Last updated
값 타입(=즉 Struct)은 → 복사가 많이 필요 qh 값의 할당이나 함수 매개변수로 전달되는 경우 복사가 많이 필요합니다.
컴파일러는 안전하다고 판단되면 복사를 피하려고 노력하지만, 개발자는 복사를 대기시키는 기법인 copy-on-write를 사용하여 타입을 구현하여 최적화를 시키려 합니다.
= 메모리상에서는 복사없이 참조를 하고 있다가 변경이 생기면 메모리를 복사하고 복사한 메모리를 수정하는 방법
이는 표준 라이브러리의 컬렉션 타입(Array, Dictionary, Set, String)과 같이 대량의 데이터를 보유할 수 있는 타입에 특히 중요합니다. 이러한 타입들은 모두 copy-on-write를 사용하여 구현됩니다.
x(또는 y)를 변형하는 순간 배열은 버퍼를 하나 이상의 다른 변수와 공유한다는 것을 감지하고 변형을 적용하기 전에 버퍼의 복사본을 만듭니다.
값 타입의 장점 = 참조 계수화의 오버헤드가 발생하지 않는다
그러나 copy-on-write 구조체는 내부적으로 참조를 저장하고, 구조체가 생성될 때마다 내부 참조 계수를 증가시켜야 합니다.
참조 계수를 증가하거나 감소하는 것은 비교적 느린 작업입니다 (예를 들어, 몇 바이트를 다른 위치에 복사하는 것보다) 이는 스레드 안전해야 하므로 락 오버헤드가 발생하기 때문입니다.
우리는 먼저 HTTP 요청 구조체의 극도로 단순화된 버전부터 시작합니다:
위에서 설명한 참조 계수화 오버헤드를 최소화하기 위해, 먼저 모든 속성을 private 저장 클래스로 래핑합니다:
이렇게 하면 HTTPRequest 구조체는 storage라는 하나의 속성만 포함하고, 복사시 내부 저장 인스턴스의 참조 카운트를 하나만 증가시킵니다.
이제 내부 저장 인스턴스의 private path와 headers 속성을 노출하기 위해 구조체에 계산 속성을 추가합니다:
이 속성들에 대한 setter의 구현이 중요합니다: 내부 저장 인스턴스에 새 값을 설정해서는 안 됩니다. 왜냐하면 이 객체는 여러 변수 사이에서 잠재적으로 공유될 수 있기 때문입니다.
클래스 인스턴스에 요청 데이터를 저장하는 것은 private 구현 세부사항이어야 하므로, 클래스 기반의 구조체가 원래의 것과 정확히 같은 방식으로 동작하도록 해야 합니다. 이는 HTTP 요청 변수의 속성을 변형할 때 해당 변수의 값만 변경되어야 함을 의미합니다.
첫 번째 단계로, setter가 호출될 때마다 내부 저장 클래스의 복사본을 생성할 수 있습니다. 복사본을 만들기 위해 Storage에 copy 메서드를 추가합니다:
그런 다음 새 값을 설정하기 전에 storage 속성에 현재 저장소의 복사본을 할당할 수 있습니다:
이제 HTTPRequest 구조체는 완전히 클래스 인스턴스에 의해 지원되지만, 여전히 원래의 것과 같은 값 의미론을 가지고 있습니다. 즉, 모든 속성이 구조체 자체의 속성인 것처럼 동작합니다:
현재 구조의 문제점 = 변경 사항을 만들 때마다 내부 저장소의 복사본이 생성됩니다.
효율적인 copy-on-write 동작을 제공하기 위해 객체(이 경우 Storage 인스턴스)가 유일하게 참조되는지 여부를 알아야 합니다. 유일하게 참조되는지 여부를 알아내려면 isKnownUniquelyReferenced 함수를 사용
이러한 지식을 활용하여, storage가 변형되기 전에 storage가 유일하게 참조되는지 확인하는 HTTPRequest의 변형을 작성할 수 있습니다. 모든 속성 setter에서 이러한 확인을 작성하는 것을 피하기 위해 로직을 storageForWriting 속성으로 래핑할 것입니다:
루프를 작성하여 copy print코드를 테스트해봅니다.
디버그 문이 한 번만 출력됩니다. 첫 번째로 req를 변형할 때만 출력됩니다. 이후 반복에서는 고유성이 감지되어 복사본이 생성되지 않습니다. 컴파일러가 수행하는 최적화와 결합하여, copy-on-write는 대부분의 불필요한 값 타입 복사를 피합니다.
Copy-On-Write 유형의 속성에 willSet 옵저버가 있으면 Copy-On-Write 최적화가 해제됩니다. willSet이 변수 newValue를 본문 내에서 사용할 수 있게 함으로써 발생하는데, 이로 인해 컴파일러가 값의 임시 복사본을 만들게 됩니다. 결과적으로 값은 더 이상 고유하게 참조되지 않게 되며, 속성의 변형은 모두 복사를 트리거합니다.