CoW in Swift (in Burmese)

Kyaw Zay Ya Lin Tun
4 min readFeb 5, 2023

--

Photo by Luba Ertel on Unsplash

Prerequisite

  • Basic knowledge of Swift syntax
  • Generic

What I’ll cover

  • Value types Vs Reference types
  • What is CoW
  • How to implement CoW in Swift
  • နိဂုံး

Value types Vs Reference types

အခြားအခြားသော statically-typed programming language တွေလိုမျိုးပဲ Swift programming language မှာလည်း object တွေကို value type နဲ့ reference type ဆိုပြီး နှစ်မျိုးခွဲခြားထားပါတယ်။ Struct, enum နဲ့ tuples type တွေဟာ value type တွေဖြစ်ပြီးတော့ class type တွေကတော့ reference type ဖြစ်ပါတယ်။ Value type တွေကို copy တဲ့အခါ single copy (deep copy) ဖြစ်ပြီးတော့ reference type ဆိုရင်တော့ shared copy (shallow copy) ဖြစ်ပါတယ်။ အောက်ကပုံကို တစ်ချက်ကြည့်ကြည့်လိုက်ပါ။

Value type တွေကို stack ပေါ်မှာသိမ်းပြီး reference type တွေကိုတော့ heap ပေါ်မှာ သွားသိမ်းပါတယ်။ iOS app တွေဟာ multithreaded environment မှာ run နေတာဖြစ်ပါတယ်။ Thread တစ်ခုချင်းစီမှာ သူ့သက်ဆိုင်ရာ stack တစ်ခုစီရှိတဲ့အတွက် stack ပေါ်မှာရှိနေတဲ့ struct type တွေဟာ thread safe ဖြစ်တယ်လို့ ယေဘူယျပြောနိုင်ပါတယ်။ ဒါပေမယ့် thread တွေအားလုံးကတော့ heap တစ်ခုတည်းကိုသာ share သုံးနေရတဲ့အတွက် heap ပေါ်မှာ သိမ်းတဲ့ class type တွေကတော့ သူ့ဘာသာ သဘာဝအားဖြင့်(by default) thread safe မဖြစ်ပါဘူး။​ ဒါကြောင့် class နဲ့ struct ဆိုရင် struct ကို ဉီးစားပေးသုံးကြဖို့ recommend တာပါ။ Reference type လည်းသုံးချင်တယ် thread safe လည်းဖြစ်ချင်တယ်ဆိုရင် WWDC21 မှာ Structured Concurrency နဲ့အတူ မိတ်ဆက်ခဲ့တဲ့ actor type ကို သုံးမှရပါမယ်။ ဒီနေ့ article က actor အကြောင်းပြောတာ မဟုတ်တဲ့အတွက် အဲ့အကြောင်းခဏမေ့ထားလိုက်ကြဉီးစို့။

What is CoW

သို့သော် struct ကိုသုံးတိုင်း အားလုံးကောင်းသွားတာတော့ မဟုတ်ပါဘူး။ မကောင်းတဲ့အချက်လည်းရှိနေပါသေးတယ်။ အဲ့တာက ဘာလဲဆိုတော့ struct type တစ်ခုကို copy လုပ်တိုင်း အဲ့ struct type ထဲမှာရှိတဲ့ value တွေအကုန်လုံးကို manual copy လုပ်ပြီး နောက်ထပ် memory slot တစ်ခုထဲကို assign လုပ်ရတာဖြစ်တဲ့အတွက် memory overhead တွေ ရှိနေပါတယ်။

struct MyStruct {
let title: String
}

let structOne = MyStruct(title: "Struct One")
let structTwo = structOne

အပေါ်က code မှာဆိုရင် `let structTwo = structOne` လို့ရေးလိုက်တဲ့အခါ structOne ရဲ့ value ကို copy ပြီး structTwo ထဲကို လာ assign တာဖြစ်ပါတယ်။ String value တစ်ခုတည်းသာပါဝင်တဲ့ MyStruct လို type မျိုးကို manual copy လုပ်တာမသိသာပေမယ့် တအား heavy ဖြစ်တဲ့ struct တွေဆိုရင် သိသာပါတယ်။

ဉပမာပြောရရင် Swfit ရဲ့ array သည် value type ဖြစ်ပါတယ်။ Element အခုတစ်ထောင်ရှိတဲ့ array တစ်ခုကို နောက် variable တစ်လုံးထဲထည့်ချင်တယ်ဆိုကြပါစို့။ ဒီအခါ မူရင်းရှိပြီးသား array element အခုတစ်ထောင်လုံးကို copy လုပ်ရမယ်ဆိုရင် မနိပ်ပါဘူး။ Array element များလာလေလေ copy လုပ်ရတဲ့ cost က တက်လာလေလေဖြစ်မှာပါ။

ဒီလို ပြသနာမျိုးကိုဖြေရှင်းဖို့အတွက် programming မှာ copy-on-write ဆိုတဲ့ concept ပေါ်ပေါက်လာပါတယ်။ ယေဘူယျအားဖြင့် ပြောရမယ်ဆိုရင် write လုပ်ဖို့ လိုအပ်တဲ့အခါမှသာ object တစ်ခုလုံးကို copy လုပ်ပြီး write မလုပ်ဘူး read ပဲလုပ်မယ်ဆိုရင်တော့ memory address ကိုပဲ copy လုပ်တဲ့ mechanism ကို CoW လို့ ခေါ်တာဖြစ်ပါတယ်။ နောက်တစ်မျိုးထပ်ပြောရရင် write လုပ်ဖို့လိုမှသာ value type တစ်ခုကဲ့သို့ ပြုမူပြီး write လုပ်ဖို့မလိုဘူးဆိုရင် reference type သဖွယ် shallow copy လုပ်တဲ့ mechanism ပါ။ Swift ရဲ့ array နဲ့ dictionary တို့မှာ CoW ကို framework level ကနေ implement လုပ်ပေးထားတဲ့အတွက် free ရပါတယ်။ ဟုတ်မဟုတ် code ရေးပြီး စမ်းကြည့်ကြရအောင်။

// A helper func to print out the memory address
func print(address o: UnsafeRawPointer ) {
print(String(format: "%p", Int(bitPattern: o)))
}

// 1.
var arrOne = (0 ..< 1000).map { $0 }
// 2.
var arrTwo = arrOne

// 3.
print(address: arrOne) // 0x10a009220
print(address: arrTwo) // 0x10a009220

// 4.
arrTwo.append(1001)

// 5.
print(address: arrTwo) // 0x10a00b220
  1. Element အခုတစ်ထောင်ရှိတဲ့ arrary တစ်ခုရေးထားပါတယ်။
  2. arrOne ကို arrTwo ထဲကို copy လိုက်ပါတယ်။
  3. arrOne နဲ့ arrTwo တို့ရဲ့ memory address တွေကို ထုတ်ကြည့်ပါတယ်။ CoW ကို framework level မှာ ကြို implement လုပ်ပေးထားတဲ့အတွက် data write မလုပ်သေးသမျှကာလပတ်လုံး arrOne နဲ့ arrTwo တို့ဟာ same memory address ကိုသာ point လုပ်နကြပါတယ်။ Console မှာထွက်လာတဲ့ memory address နှစ်ခု တူညီနေတာကိုကြည့်ရင် သိနိုင်ပါတယ်။ ဆိုလိုတာကတော့ မလိုသေးတဲ့အတွက် element အခုတစ်ထောင်လုံးကို deep copy မလုပ်သေးဘူးလို့ ပြောချင်တာပါ။
  4. arrTwo ထဲကို 1001 ဆိုတဲ့ value append လုပ်လိုက်ပါတယ်။
  5. arrTwo ကို data write လိုက်တာဖြစ်တဲ့အတွက် arrOne နဲ့ share သုံးဖို့ဆိုတာမဖြစ်နိုင်တော့ပါဘူး။ ဒီလို မဖြစ်မနေ copy လုပ်ရမယ့်အချိန်ကျတော့မှသာ arrOne ထဲက element အခုတစ်ထောင်ကို arrTwo ထဲကို copy ယူလာပြီး နောက်ထပ် 1001 ကို append လုပ်လိုက်တာဖြစ်ပါတယ်။ ဒီအတွက်ကြောင့် memory address ကို ထုတ်ကြည့်လိုက်တဲ့အခါ arrOne ရဲ့ memory address နဲ့ မတူတော့ပါဘူး။

How to implement CoW in Swift

CoW ကို Swift ရဲ့ Array, Dictionary တို့လို collection type တွေမှာ built-in feature အနေနဲ့ အလိုအလျှောက်ပါပေမယ့် အခြား ကိုယ့်ဘာသာရေးထားတဲ့ value type တွေမှာတော့ အလိုလျှောက်ရမှာမဟုတ်ပါဘူး။ ကိုယ့်ရဲ့ ကိုယ်ပိုင် type တွေမှာလည်း CoW ကို သုံးချင်တယ်ဆိုရင်တော့ Swift main repository ရဲ့ OptimizationTips.rst ထဲက နမူနာ code ကို ယူသုံးနိုင်ပါတယ်။

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

// 2.
struct Box<T> {
var ref: Ref<T>
init(_ x: T) { ref = Ref(x) }

var value: T {
// 3.
get { return ref.val }
set {
// 4.
if !isKnownUniquelyReferenced(&ref) {
ref = Ref(newValue)
return
}
// 5.
ref.val = newValue
}
}
}
  1. Ref ဆိုတဲ့ class ရဲ့အဓိကတာဝန်က generic value type ကို reference type တစ်ခုအဖြစ် ပြန် wrap လုပ်ပေးဖို့ဖြစ်ပါတယ်။
  2. Box ကတော့ Ref ကို object ကိုယူပြီး CoW ကို property setter ကနေတစ်ဆင့် ပြန် implement လုပ်ပါတယ်။
  3. value ရဲ့ getter မှာတော့ ref ရဲ့ val ကိုပဲ return ပြန်ပေးလိုက်ပါတယ်။
  4. setter ထဲမှာတော့ လက်ရှိ ref object က program တစ်လျှောက် သူ့ကို strong reference ယူထားတဲ့နေရာ ရှိလားကြည့်ပြီး မရှိဘူးဆိုတော့မှ ref object အသစ်ဆောက်ပြီး ref variable ထဲကို assign ထည့်လိုက်ပါတယ်။ ဒီနေရာမှာ ဘာကြောင့် value type ကို Ref<T> နဲ့ wrap ရလည်းဆိုတာ မြင်မယ်ထင်ပါတယ်။ Reference type (class) မဟုတ်ရင် reference count မရှိလို့ပါ။ Reference count မရှိရင် ကျွန််တော်တို့လုပ်ချင်တဲ့ CoW လည်း လုပ်လို့မရပဲ အမြဲ deep copy ပဲဖြစ်နေတော့မှာပါ။
  5. တစ်ကယ်လို့ strong reference ယူထားတဲ့နေရာ ရှိတယ်ဆိုရင်တော့ ရှိပြီးသား ref ထဲက val ကိုပဲ mutate လုပ်ပါတယ်။ ဒီနည်းနဲ့ CoW ကို custom type တွေမှာ implement လုပ်နိုင်မှာဖြစ်ပါတယ်။
// Usage
struct Person {
let name: String
}

let kyaw: Box<Person> = Box(Person(name: "Monkey"))
var monkey: Box<Person> = kyaw

print(kyaw.value.name) // Monkey
print(monkey.value.name) // Monkey

monkey.value.name = "Kyaw Monkey"

print(kyaw.value.name) // Monkey
print(monkey.value.name) // Kyaw Monkey

Person က value type struct ဖြစ်နေတဲ့အခါမှာ CoW ကို implement လုပ်ထားတဲ့အတွက် monkey.value.name ကို value အသစ်ပြောင်းလိုက်သည့်တိုင် kyaw.value.name ကို ထိခိုက်မှုမရှိတာကိုတွေ့ရပါမယ်။ တစ်ကယ်လို့ Person ကသာ reference type class ဖြစ်ခဲ့ရဲ့ reference type တို့ရဲ့ထုံးစံအတိုင်း shallow copy လုပ်သွားပြီး original object နဲ့ copy object နှစ်ခုလုံးရဲ့ state ကို effect ဖြစ်သွားတာကို တွေ့ရပါလိမ့်မယ်။

class Person {
var name: String

init(name: String) {
self.name = name
}
}

let kyaw: Box<Person> = Box(Person(name: "Monkey"))
let monkey: Box<Person> = kyaw

print(kyaw.value.name) // Monkey
print(monkey.value.name) // Monkey

monkey.value.name = "Kyaw Monkey"

print(kyaw.value.name) // Kyaw Monkey
print(monkey.value.name) // Kyaw Monkey

နိဂုံး

ဒီတစ်ခါမှာတော့ optimization trick တစ်ခုဖြစ်တဲ့ CoW အကြောင်းကို ပြောပြပေးခဲ့တာပဲဖြစ်ပါတယ်။ CoW ကို Swift standard library ရဲ့ နေရာတော်တော်များများမှာ implement လုပ်ပေးထားတဲ့အတွက် မသိသာပေမယ့် code optmise လုပ်တဲ့အခါမှာ အသုံးဝင်ပါတယ်။

--

--

Kyaw Zay Ya Lin Tun
Kyaw Zay Ya Lin Tun

Written by Kyaw Zay Ya Lin Tun

Lead iOS Dev @CodigoApps • Programming Mentor • Swift enthusiast • Community Builder • Organising CocoaHeads Myanmar 🇲🇲

No responses yet