ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Memory Safety
    iOS/Swift 공식문서 2022. 3. 1. 21:06

    Memory Safety

    기본적으로, Swift는 코드에서 안전하지 않은 동작이 발생하는 것을 방지한다. 예를 들어, 변수는 사용되기 이전에 초기화되어야 하며, 메모리는 할당이 해제된 이후에는 접근하지 않는다. 그리고 배열의 경우 index가 out-of-bounds error를 발생시키는지 확인한다.


    또한 Swift는 메모리의 위치를 수정하는 코드가 해당 메모리에 대한 독점적인 접근 권한을 갖도록 하여 같은 메모리 지역에 대한 여러 접근이 충돌하지 않게 해준다. Swift가 메모리를 자동으로 관리하기 때문에, 대부분의 경우 메모리 접근에 대해 생각할 필요가 없다. 그러나, 잠재적으로 충돌이 일어날 수 있는 상황을 이해하여, 메모리 접근이 충돌하는 것을 회피할 수 있는 코드를 작성할 수 있도록 하는 것이 중요하다. 코드가 메모리 접근에 대한 충돌을 포함한다면 컴파일 에러나 런타임 에러가 발생할 것이다.


    Understanding Conflicting Access to Memory

    메모리 접근은 변수에 값을 설정하거나 함수에 인자를 넘겨줄 때 발생한다.

    // A write access to the memory where one is stored.
    var one = 1
    
    // A read access from the memory where one is stored.
    print("We're number \(one)!")

    메모리 접근 충돌은 코드 여러 곳에서 같은 메모리 주소에 동시에 접근하려고 할 때 발생할 수 있다. Swift에서는 여러 줄에 걸쳐서 값을 수정하는 방법이 존재하는데, 이로 인하여 값을 수정하는 도중에 값에 접근하는 것이 가능하다.


    다음 예시를 살펴보자. 예산을 업데이트하는 예시이다. 2단계로 나뉘는데, 아이템들의 이름과 가격을 추가하는 단계와, 총 가격을 업데이트하는 단계가 있다.



    아이템을 예산에 추가하는 동안, 일시적으로 invalid한 상태가 되는 것을 확인할 수 있다. 이는 아이템이 추가되었지만 총 가격에 반영되지 않았기 때문이다. 이 상태에서 예산에 접근하면 부정확한 정보를 제공받게 된다.


    이 예시는 충돌하는 메모리 접근을 고칠 때 마주할 수 있는 문제를 보여준다. 이러한 충돌을 해결하면서 각자 다른 결과를 만드는 여러 방법이 존재하며, 어떠한 방법이 맞는지가 명확하지 않은 경우도 있다. 위 예시의 경우, 원래 총 가격 혹은 업데이트된 총 가격을 원하느냐에 따라 $5 혹은 $320이 모두 맞는 답일 수 있다. 충돌하는 메모리 접근을 고치기 전에, 우선 어떤 작업을 의도하는지를 결정해야 한다.


    note

    동시성 혹은 멀티스레드 코드를 작성한다면, 메모리 접근 충돌이 친숙한 문제일 수 있다. 그러나, 여기서 다루는 접근 충돌은 싱글 스레드에서도 발생할 수 있으며 동시성 혹은 멀티스레드 코드가 반드시 수반되는 것이 아니다. 싱글 스레드에서 메모리 접근 충돌이 있을 경우, Swift는 컴파일 에러나 런타임 에러 발생을 보증한다. 멀티스레드 코드의 경우, 스레드 간의 접근 충돌을 찾아내기 위해 Thread Sanitizer를 사용해라.


    Characteristics of Memory Access

    접근 충돌은 다음 조건들을 만족하는 2개 이상의 접근이 있을 때 발생한다.


    a. 최소 한 개는 쓰기 혹은 non-atomic한 접근이다.
    b. 같은 메모리 주소에 접근한다.
    c. 메모리에 접근하는 시간이 겹친다.


    첫번째 조건은 명확하다. 쓰기 접근은 메모리 주소를 바꾸지만, 읽기 접근은 그렇지 않다. 메모리 접근 시간은 순간적(instantaneous)이거나 장기적(long-term)일 수 있다.


    operation이 atomic하다는 것은 C의 atomic operation만 사용한다는 뜻이다. 그렇지 않으면 non-atomic하다. 접근이 순간적이라는 것은 접근이 시작할 때부터 끝날 때까지 다른 코드가 실행될 수 없다는 것을 의미한다. 당연이 2개의 순간적인 접근은 동시에 발생할 수 없다. 대부분의 메모리 접근은 순간적이다. 예를 들어 아래와 같은 읽기/쓰기 코드는 순간적인 접근이다.

    func oneMore(than number: Int) -> Int {
        return number + 1
    }
    
    var myNumber = 1
    myNumber = oneMore(than: myNumber)
    print(myNumber)
    // Prints "2"

    반면, 장기적인 접근도 존재하는데, 다른 코드의 실행에 걸쳐 있는 경우를 말한다. 순간적인 접근과의 차이점은 장기 접근이 시작하고 끝나기 전까지 다른 코드가 실행될 수 있다는 것이다. 이를 overlap이라 부른다. 장기적 접근은 다른 순간적 접근이나 장기적 접근과 overlap이 가능하다. overlap되는 접근은 주로 in-out parameter를 사용하는 함수/메서드나 구조체의 mutating method에서 발생한다.


    Conflicting Access to In-Out Parameters

    함수는 모든 in-out parameter에 대한 장기적인 접근 권한을 갖는다. in-out parameter에 대한 쓰기 접근은 모든 non-in-out parameter가 평가된 후에 시작되며 함수 호출의 전체 시간 동안 지속된다. in-out parameter가 여러개 있다면, 쓰기 접근은 매개변수가 나타나는 순서대로 시작한다.


    장기적인 쓰기 접근을 하면 in-out으로 넘겨진 원래 값에 접근할 수 없다. 원래 값에 접근하려 하면 충돌이 발생한다.

    var stepSize = 1
    
    func increment(_ number: inout Int) {
        number += stepSize
    }
    
    increment(&stepSize)
    // Error: conflicting accesses to stepSize

    위의 코드의 경우, stepSize는 전역 변수이므로 increment(_:) 함수에서 접근할 수 있다. 그러나 stepSize에 대한 읽기 접근이 number에 대한 쓰기 접근과 겹친다. 아래 그림과 같이 두 접근이 같은 메모리 주소를 참조하는 것이다. 따라서 충돌이 일어난다.



    이러한 충돌을 해결하는 방법 중 하나는 explicit한 copy를 만드는 것이다.

    // Make an explicit copy.
    var copyOfStepSize = stepSize
    increment(&copyOfStepSize)
    
    // Update the original.
    stepSize = copyOfStepSize
    // stepSize is now 2

    in-out parameter에 대한 장기적인 쓰기 접근에 의한 현상이 하나 더 있는데, 바로 한 함수의 여러 in-out parameter로 하나의 변수를 넘겨줘서 충돌이 발생하는 것이다.

    func balance(_ x: inout Int, _ y: inout Int) {
        let sum = x + y
        x = sum / 2
        y = sum - x
    }
    var playerOneScore = 42
    var playerTwoScore = 30
    balance(&playerOneScore, &playerTwoScore)  // OK
    balance(&playerOneScore, &playerOneScore)
    // Error: conflicting accesses to playerOneScore

    note

    연산자도 함수이기 때문에, 연산자의 in-out parameter에 대한 장기적 접근을 가질 수 있다. 위의 balance(_:_:) 함수가 연산자 <^>이라면, playerOneScore <^> playerOneScore을 실행시 충돌이 일어날 것이다.


    Conflicting Access to self in Methods

    구조체의 mutating method는 self에 대한 쓰기 접근 권한을 가진다. 다음과 같이 Player 클래스를 정의하고 두 인스턴스를 생성하자.

    struct Player {
        var name: String
        var health: Int
        var energy: Int
    
        static let maxHealth = 10
        mutating func restoreHealth() {
            health = Player.maxHealth
        }
    }
    
    extension Player {
        mutating func shareHealth(with teammate: inout Player) {
            balance(&teammate.health, &health)
        }
    }
    
    var oscar = Player(name: "Oscar", health: 10, energy: 10)
    var maria = Player(name: "Maria", health: 5, energy: 10)

    그 다음 코드를 실행하면 정상적으로 실행된다.

    oscar.shareHealth(with: &maria)  // OK

    아래 그림을 살펴보자. 메모리에 접근하는 시간이 overlap되어도 서로 다른 메모리 주소에 접근하기 때문에 충돌이 일어나지 않는다.



    그러나 아래 코드를 실행하는 경우 에러가 발생한다.

    oscar.shareHealth(with: &oscar)
    // Error: conflicting accesses to oscar

    이는 쓰기 접근으로 같은 메모리 주소에 동시에 접근하기 때문이다.



    Conflicting Access to Properties

    구조체, 튜플, 열거형 등의 타입은 구조체의 프로퍼티, 튜플의 element 같은 개별 요소로 구성된다. 이 타입들은 value type이기 때문에 개별 값을 바꾸면 전체 값이 바뀌게 된다. 결국 개별 값에 대한 접근은 전체 값에 대한 접근이 되는 것이다. 예를 들어 튜플에서 여러 개별 값에 동시에 쓰기 접근을 하는 것은 충돌을 발생시킨다.

    var playerInformation = (health: 10, energy: 20)
    balance(&playerInformation.health, &playerInformation.energy)
    // Error: conflicting access to properties of playerInformation

    이는 전역 변수에 저장된 구조체의 여러 프로퍼티에 동시에 접근하는 경우도 마찬가지다.

    var holly = Player(name: "Holly", health: 10, energy: 10)
    balance(&holly.health, &holly.energy)  // Error

    사실, 구조체의 프로퍼티들에 대한 접근은 안전하게 overlap될 수 있다. 위 예시의 holly가 전역 변수가 아니라 다음 코드의 oscar처럼 지역 변수였다면 overlap되는 접근이 안전함을 컴파일러가 증명할 수 있다.

    func someFunction() {
        var oscar = Player(name: "Oscar", health: 10, energy: 10)
        balance(&oscar.health, &oscar.energy)  // OK
    }

    위 코드에서 oscarhealthenergybalance(_:_:) 함수의 매개변수로 넘겨졌다. 이 상황에서 두 프로퍼티가 상호작용하지 않기 때문에 컴파일러는 메모리 안전성(memory safety)가 보존된다는 것을 증명할 수 있다.


    구조체의 프로퍼티에 대한 중첩되는 접근에 대한 제한은 메모리 안전성를 보존하기 위해 반드시 필요한 것은 아니다. 메모리 안전성보다 배타적 접근(exclusive access)가 더 엄격한 조건이다. 이는 일부 코드가 배타적 접근을 위반하더라도, 메모리 안전성을 보존함을 의미한다. Swift는 컴파일러가 메모리에 대한 배타적이기 않은 접근이 안전하다면 해당 코드를 허용한다. 특히, 다음 조건을 만족한다면 구조체의 프로퍼티에 대한 중첩 접근이 안전함을 증명할 수 있다.


    a. 오직 stored property에만 접근하고, computed나 class property에는 접근하지 않는다.
    b. 구조체가 전역 변수가 아닌 지역 변수의 값이다.
    c. 구조체가 어느 클로져에도 캡쳐되지 않았거나, non-escaping closure에만 캡쳐되었다.


    컴파일러가 접근이 안전함을 증명할 수 없다면, 접근을 허용하지 않는다.


    Reference

    https://docs.swift.org/swift-book/LanguageGuide/MemorySafety.html

    'iOS > Swift 공식문서' 카테고리의 다른 글

    Advanced Operators  (0) 2022.03.05
    Access Control  (0) 2022.03.02
    Automatic Reference Counting  (0) 2022.03.01
    Opaque Types  (0) 2022.02.27
    Generics  (0) 2022.02.25

    댓글

Designed by Tistory.