Copy-on-wirte in Swift

oto·2023년 4월 13일
0

서론

swift의 value type에는 struct와 enum 등이 있고, reference type에는 class가 있습니다. 여기서 두 type의 가장 큰 차이는 instance의 전달 방식에 있습니다. value type은 원본에서 복사된 사본을 전달하고, reference type은 원본의 참조 값을 전달합니다.

그리고 swift에서는 reference type보다 value type 사용을 권장합니다. instance가 단순한 구조인 stack에 할당되는지 복잡한 구조인 heap에 할당되는지에 따라 성능차이가 발생하기도하고, 원본 데이터의 의도치않은 변경을 막기위해서 때문이기도 합니다.

그런데 여기서 value type에는 문제가 있습니다. 만약 72만개의 값이 저장되있는 배열이 있다면, instance를 전달할 때마다 72만개의 값을 갖는 배열 사본을 생성한다는 얘기입니다. 이러면 오히려 성능에 더 큰 문제가 생길 수 있습니다.
OMG..

copy-on-write

여기서 발생하는 문제를 줄여주는 것이 바로 COW(copy-on-write) 기능입니다. COW는 자료구조에 변화가 생기지 않았다면, 사본을 만들지않고 원본을 가리킵니다. 이를 통해 런타임 오버헤드를 줄일 수 있습니다.

Swift 표준 라이브러리의 모든 자료구조에는 COW기능이 구현돼있다고 합니다. 하지만 custom value type에서는 이 기능을 자동으로 갖지는 못합니다. 물론, 우리는 표준 라이브러리를 주로 사용하고 직접 구현할 일은 많지않을겁니다. 하지만, 사람 일은 모르는 것이니 어떻게 동작하는지 COW를 직접 한번 구현해볼까요?

저는 간단한 Stack을 구현해봤습니다.

fileprivate class BackendStack<T> {
    private var items = [T]()
    
    public init() {}
    private init(_ items: [T]) {
        self.items = items
    }
    
    public func push(item: T) {
        items.append(item)
    }
    
    public func pop() -> T? {
        if items.count > 0 {
            return items.removeLast()
        } else {
            return nil
        }
    }
    
    public func count() -> Int {
        return items.count
    }
    
    public func copy() -> BackendStack<T> {
        return BackendStack<T>(items)
    }
}

BackendStack Type은 Stack의 백엔드 저장 공간 타입으로, 원본의 참조를 갖고있기위해 원본을 가리킬 reference type으로 구현하였습니다. 데이터를 추가하기위한 push method, 데이터 삭제를 위한 pop method, 데이터 갯수를 알기위한 count method, 데이터가 변경됐을 때 복사를 위한 copy method를 작성하였습니다.

이제 stack을 구현해볼까요?
구현 전에 알아둘 것은 swift에는 isKnownUniquelyReferenced()라는 전역 함수가 있습니다. 이 함수는 reference type의 인스턴스에 참조가 오직 하나뿐일 때 true를 반환하고, 하나 이상일 경우에는 false를 반환하는 기능을 합니다.

이 전역함수를 이용해 사본을 생성할지 말지를 결정할 수 있습니다.

struct MyStack<T> {
    private var internalStack = BackendStack<T>()
    
    mutating private func checkUniquelyReferencedInternalStack() {
        if !isKnownUniquelyReferenced(&internalStack) {
            print("OMG.. Making a copy of internalStack")
            internalStack = internalStack.copy()
        } else {
        	print("WOW.. Not making a copy of internalStack")
        }
    }
    
    public mutating func push(item: T) {
        checkUniquelyReferencedInternalStack()
        internalStack.push(item: item)
    }
    
    public mutating func pop() -> T? {
        checkUniquelyReferencedInternalStack()
        return internalStack.pop()
    }
    
    public func count() -> Int {
        return internalStack.count()
    }
    
    mutating public func uniquelyReferenced() -> Bool {
        return isKnownUniquelyReferenced(&internalStack)
    }
}

checkUniquelyReferencedInternalStack()에서는 유일한 참조가 아닐 경우, BackendStack에서 구현했던 copy() method를 통해 사본을 만들어줍니다.

그리고 값이 변경되는 push() method와 pop() method를 사용할 때에는 유일한 참조인지 확인을 해줍니다. 값이 변경될 때, 유일한 참조라면 복사본을 생성하지않고 값을 변경해줍니다.(원본에서 변경을 해주는 것이니까 복사본을 만들 이유가 x)

count() method의 경우, 값이 변경되는 함수가 아니므로 유일한 참조인지 확인을 할 필요가 없습니다.

uniquelyReferenced() method는 현재 인스턴스가 유일한 참조인지 확인하기위해 추가해준 method입니다.

그럼 구현한 코드를 사용하면서 확인해볼까요?

var stack1 = MyStack<Int>()
stack1.push(item: 3)
print(stack1.uniquelyReferenced()) // "true"

var stack2 = stack1
print(stack1.uniquelyReferenced()) // "false"
print(stack2.uniquelyReferenced()) // "false"

stack2.push(item: 1)
print(stack1.uniquelyReferenced()) // "true"
print(stack2.uniquelyReferenced()) // "true"

먼저 stack1에 위에서 구현한 MyStack instance를 생성하고 "3"을 push했습니다. stack1이 유일한 참조를 갖는지 확인을 해보니 true를 반환합니다.

이제 stack2에 stack1의 instance를 넘겨줬습니다. stack1과 stack2 모두 유일한 참조인지 확인했을 때 false가 나옵니다. stack1과 stack2가 가리키고 있는 instance가 같기때문입니다.

여기서 stack2에 1을 push하면서 값에 변화를 주었습니다. 여기서 stack2는 stack1의 새롭게 생성된 사본에 "1"의 값을 push하게 됩니다. 즉, stack1과 stack2는 다른 각각의 instance를 가리키게 되는 것입니다. 그래서 두 instance의 유일한 참조를 확인했을 때, 둘 다 true로 바뀐 것을 확인할 수 있습니다.

결론

value type은 instance의 사본을 넘겨준다는 점에서 '데이터가 너무 크면 어떡하지?'혹은 '전달을 미친듯이 많이 하게되면 어떡하지?'같은 의문들이 있었습니다. COW 기능을 공부하면서 이런 의문을 해소할 수 있었습니다. 그리고 스위프트에서 value type을 권장하지만, COW같은 기능을 구현하기 위해선 reference type 역시 필요하고, 중요하며, 두 type을 상황에 맞게 적절하게 사용해야한다고 생각했습니다.

해당 글은 '스위프트4 프로토콜지향 프로그래밍 3/e'를 보고 공부한 내용을 정리하였습니다 :)

profile
iOS Developer

0개의 댓글