Skip to content

Dashboard

Cơ chế Copy-On-Write trong Swift

Created by Admin

Mở đầu

Các kiểu dữ liệu trong Swift được chia làm 2 loại: Tham chiếu (reference type) và Tham trị (value type):

  • Tham trị (value type): Mỗi thể hiện giữ 1 bản sao dữ liệu riêng biệt. Các thể hiện này thường được định nghĩa bởi Struct, Enum hoặc Tuple
  • Tham chiếu (reference type): Các thể hiện dùng chung 1 bản sao data. Các thể hiện này thường là class hoặc closure

Hãy cùng xem ví dụ để thấy được sự khác nhau khi sử dụng classstruct

class Car {
    var company: String
    var model: String
    
    init(company: String, model: String) {
        self.company = company
        self.model = model
    }
}

var audiA4 = Car(company: "Audi", model: "A4")
print(audiA4.model) // Prints "A4"
var newAudi = audiA4
newAudi.model = "Q5"
print(audiA4.model) // Prints "Q5"
print(newAudi.model) // Prints ?Q5"

Các biến tham chiếu sẽ dùng chung 1 địa chỉ ô nhớ nên khi có sự thay đổi là tất cả sẽ cùng thay đổi.

struct Car {
    var company: String
    var model: String
}

var audiA4 = Car(company: "Audi", model: "A4")
print(audiA4.model) // Prints "A4"
var newAudi = audiA4
newAudi.model = "Q5"
print(audiA4.model) // Prints "A4"
print(newAudi.model) // Prints ?Q5"

Nhưng với các biến tham trị thì khác. Tham trị được biến đến với nhiều ưu điểm như đảm bảo dữ liệu được bảo toàn nguyên vẹn. Khi gán biến tham trị sang 1 biến tham trị khác hoặc truyền vào function (trừ tham số inout) thì toàn bộ dữ liệu sẽ được copy sang 1 địa chỉ ô nhớ mới. Với những dữ liệu lớn, việc sao chép có thể tốn thời gian và làm giảm hiệu năng của hệ thống.

Khi các biến chỉ toàn là tham trị thì sẽ không cần tới ARC, nhưng khi một trị tham chiếu chứa 1 biến tham chiếu thì sẽ không may thừa kế khả năng này.

final class ExampleClass {
  let exampleString = "Ex value"
}

struct ExampleStruct {
  let ref1 = ExampleClass()
  let ref2 = ExampleClass()
  let ref3 = ExampleClass()
  let ref4 = ExampleClass()
}

Việc chứa nhiều biến tham chiếu sẽ yêu cầu gọi malloc/free rất nhiều lần và việc quá tải cho ARC mỗi lần tạo 1 bản sao mới. Để giảm tải việc này, Swift có cách riêng để tối ưu hóa: Copy-On-Write

Copy-On-Write là gì?

Copy-On-Write enables value types to be referenced when they are copied just like reference types. The real copy only happens when you have an already existing strong reference and you are trying to modify that copy.


Hiểu đơn giản là cơ chế này sẽ giúp giảm tối ưu hóa việc tạo bản sao mới. Khi gán hoặc truyền biến tham trị vào hàm thì các thể hiện đó sẽ trỏ chung cùng 1 địa chỉ ô nhớ và chỉ tạo bản sao mới cho thể hiện nào có thay đổi. Nghe thì có vẻ khó hiểu, cùng xem ví dụ nhé: Ta có hàm hỗ trợ để xem địa chỉ ô nhớ của biến như sau:

func address(of object: UnsafeRawPointer) -> String {
    let addr = Int(bitPattern: object)
    return String(format: "%p", addr)
}

Giờ đến món chính:

var array1 = [1, 2, 3, 4]

address(of: array1)     // 0x60000006e420

var array2 = array1

address(of: array2)     // 0x60000006e420

array1.append(2)

address(of: array1)     // 0x6080000a88a0
address(of: array2)     // 0x60000006e420

Trong ví dụ này, 2 array cùng trỏ vào 1 địa chỉ cho đến khi 1 trong 2 thay đổi. việc này rất hữu ích trong việc cải thiện hiệu năng ứng dụng khi sử dụng các tham trị có dữ liệu lớn. Nhưng thật đáng buồn là mặc định các biến tham trị của Swift không có điều này, nó chỉ có sẵn trong Swift Standard Library cho 1 số kiểu dữ liệu như Array và các Collections. Nhưng đừng buồn, chúng ta có thể tự mình triển khai nó.

Triển khai Copy-On-Write

Giả sử ta có struct User mà cần thực hiện Copy-On-Write:

struct User {
    var identifier = 1
}

Sau đó sẽ tạo class để bọc kiểu tham trị kia:

final class Ref<T> {
  var val : T
  init(_ v : T) {val = v}
}

Ở đây sử dụng class vì khi ta gán tham chiếu cho 1 biến khác, 2 biến sẽ đều trỏ tới cùng 1 địa chỉ ô nhớ thay vì tạo ra bản sao mới như tham trị.

Sau đó tạo thêm 1 struct để bọc Ref lại:

struct Box<T> {
    private var ref: Ref<T>
    init(value: T) {
        ref = Ref(value: value)
    }

    var value: T {
        get { return ref.value }
        set {
            guard isKnownUniquelyReferenced(&ref) else {
                ref = Ref(value: newValue)
                return
            }
            ref.value = newValue
        }
    }
}

Bởi vì struct là tham trị nên khi gán sang 1 biến khác, giá trị sẽ được copy nhưng ref vẫn đang được 2 bản sao trỏ vào vì nó là tham chiếu.

Sau đó khi lần đầu thay đổi value của 1 trong 2 biến Box, ta sẽ tạo ra thể hiện mới của ref nhờ vào hàm này:

guard isKnownUniquelyReferenced(&ref) else {
    ref = Ref(value: newValue)
    return
}

isKnownUniquelyReferenced sẽ trả về Bool để xác định xem object truyền vào đã có một strong reference riêng lẻ nào chưa

Bằng cách này, 2 biến Box sẽ không dùng chung thể hiện ref nữa. Đây là toàn bộ code:

final class Ref<T> {
    var value: T
    init(value: T) {
        self.value = value
    }
}

struct Box<T> {
    private var ref: Ref<T>
    init(value: T) {
        ref = Ref(value: value)
    }

    var value: T {
        get { return ref.value }
        set {
            guard isKnownUniquelyReferenced(&ref) else {
                ref = Ref(value: newValue)
                return
            }
            ref.value = newValue
        }
    }
}

Và giờ ta có thể sử dụng Copy-On-Write cho những biến tham trị của mình:

let user = User()

let box = Box(value: user)
var box2 = box                  // box2 shares instance of box.ref

box2.value.identifier = 2       // Creates new object for box2.ref

Kết luận

Trên đây mình đã giới thiệu cho các bạn thế nào là Copy-On-Write và cách nó hoạt động. Bạn có thể tìm hiểu thêm ở OptimizationTips.rst trong Swift main repo của Apple. Happy coding!!!

Source: https://viblo.asia/p/co-che-copy-on-write-trong-swift-Qbq5Q9ym5D8