作者:Mike Ash,原文鏈接,原文日期:2016-01-29
譯者:ray16897188;校對(duì):Channe;定稿:千葉知風(fēng)
Swift 的類對(duì)大多數(shù)剛接觸編程語(yǔ)言的人來(lái)說(shuō)是很容易理解的,它們和其他語(yǔ)言中的類十分類似。無(wú)論你是從 Objective-C、Java 還是 Ruby 過(guò)來(lái)的,在 Swift 中對(duì)于類的使用并無(wú)太大區(qū)別。而 Swift 中的結(jié)構(gòu)體就是另外一回事兒了,它們有點(diǎn)兒像類,但是它們是值類型,還沒有繼承,另外我總是聽到這個(gè)什么 copy-on-write(寫入時(shí)復(fù)制)的說(shuō)法。那么 Swift 中的結(jié)構(gòu)體是存在哪里?它們是怎么個(gè)工作原理?今天我們來(lái)仔細(xì)研究一下如何在內(nèi)存中保存和操作結(jié)構(gòu)體。
簡(jiǎn)單的結(jié)構(gòu)體
我建了一個(gè)有兩個(gè)文件的程序來(lái)探究一下結(jié)構(gòu)體在內(nèi)存中是怎樣存儲(chǔ)的。對(duì)這個(gè)測(cè)試程序采用 optimizations enabled 選項(xiàng)編譯,并取消 whole-module optimization 選項(xiàng)。此測(cè)試是讓一個(gè)文件調(diào)用一個(gè)文件,這就會(huì)防止編譯器將所有東西都內(nèi)聯(lián),從而讓我們能更清楚的看明白東西都存在哪兒,以及數(shù)據(jù)在函數(shù)間如何傳遞。
從創(chuàng)建一個(gè)三個(gè)元素的結(jié)構(gòu)體開始:
struct ExampleInts {
var x: Int
var y: Int
var z: Int
}
又寫了三個(gè)函數(shù),都是各自接收一個(gè)結(jié)構(gòu)體的實(shí)例,然后分別返回該實(shí)例的一個(gè)字段(field):
func getX(parameter: ExampleInts) -> Int {
return parameter.x
}
func getY(parameter: ExampleInts) -> Int {
return parameter.y
}
func getZ(parameter: ExampleInts) -> Int {
return parameter.z
}
在另一個(gè)文件中創(chuàng)建了一個(gè)結(jié)構(gòu)體實(shí)例,然后調(diào)用所有的 get 函數(shù):
func testGets() {
let s = ExampleInts(x: 1, y: 2, z: 3)
getX(s)
getY(s)
getZ(s)
}
針對(duì) getX,編譯器生成了如下代碼:
pushq %rbp
movq %rsp, %rbp
movq %rdi, %rax
popq %rbp
retq
查看一下匯編的備忘單,知道參數(shù)是按順序被傳進(jìn)寄存器 rdi、rsi、rdx、rcx、r8 和 r9 中,然后返回值被存放在 rax 中。這里前兩個(gè)指令只是函數(shù)序言(function prologue),而后兩個(gè)是函數(shù)尾聲(function epilogue)。真正做的工作就是 movq %rdi, %rax:提取第一個(gè)參數(shù)并將其返回。再看一下 getY:
pushq %rbp
movq %rsp, %rbp
movq %rsi, %rax
popq %rbp
retq
基本一樣,只不過(guò)它返回的是第二個(gè)參數(shù)。那 getZ 呢?
pushq %rbp
movq %rsp, %rbp
movq %rdx, %rax
popq %rbp
retq
還是,基本都一樣,但返回的是第三個(gè)參數(shù)。從這我們可以看出來(lái)每個(gè)單獨(dú)的結(jié)構(gòu)體元素都是被看做獨(dú)立的參數(shù),被單獨(dú)的傳遞進(jìn)函數(shù)中。在接收端挑出某個(gè)元素,僅僅就是選擇它所在的相應(yīng)的寄存器。
在調(diào)用點(diǎn)驗(yàn)證一下。下面是 testGets 的編譯器生成碼:
pushq %rbp
movq %rsp, %rbp
movl $1, %edi
movl $2, %esi
movl $3, %edx
callq __TF4main4getXFVS_11ExampleIntsSi
movl $1, %edi
movl $2, %esi
movl $3, %edx
callq __TF4main4getYFVS_11ExampleIntsSi
movl $1, %edi
movl $2, %esi
movl $3, %edx
popq %rbp
jmp __TF4main4getZFVS_11ExampleIntsSi
可以看出這個(gè)結(jié)構(gòu)體實(shí)例的是直接在組建于參數(shù)寄存器上的。(edi、esi 和 edx 寄存器分別是 rdi、rsi 和 rdx 對(duì)應(yīng)的低 32 bit 帶寬版本。)這樣甚至不用在調(diào)用途中操心值的額外存儲(chǔ),只需每次調(diào)用時(shí)重建這個(gè)結(jié)構(gòu)體的實(shí)例就好了。因?yàn)榫幾g器明確的知道寄存器中的內(nèi)容,這就可以大大的改變 Swift 的代碼編寫方式。注意到對(duì) getZ 的調(diào)用和對(duì) getX、getY 的調(diào)用有些許不同:由于它是該函數(shù)中的最后一部分,編譯器以尾調(diào)用(tail call)的形式將其生成,清空本地調(diào)用棧幀(local call frame),然后讓 getZ 直接返回到 testGets 函數(shù)被調(diào)用的地方。
再讓我們看一下當(dāng)編譯器不知道結(jié)構(gòu)體的內(nèi)容時(shí)會(huì)生成怎樣的代碼。下面是這個(gè) test 函數(shù)的變體,從其他的地方獲得結(jié)構(gòu)體的實(shí)例:
func testGets2() {
let s = getExampleInts()
getX(s)
getY(s)
getZ(s)
}
getExampleInts 創(chuàng)建了一個(gè)結(jié)構(gòu)體實(shí)例然后將其返回,但這個(gè)函數(shù)是在另一個(gè)文件中,所以優(yōu)化 testGets2 的時(shí)候編譯器是不知道發(fā)生了什么情況的。函數(shù)如下:
func getExampleInts() -> ExampleInts {
return ExampleInts(x: 1, y: 2, z: 3)
}
當(dāng)編譯器不知道結(jié)構(gòu)體的內(nèi)容時(shí) testGets2 會(huì)生成怎樣的代碼呢?
pushq %rbp
movq %rsp, %rbp
pushq %r15
pushq %r14
pushq %rbx
pushq %rax
callq __TF4main14getExampleIntsFT_VS_11ExampleInts
movq %rax, %rbx
movq %rdx, %r14
movq %rcx, %r15
movq %rbx, %rdi
movq %r14, %rsi
movq %r15, %rdx
callq __TF4main4getXFVS_11ExampleIntsSi
movq %rbx, %rdi
movq %r14, %rsi
movq %r15, %rdx
callq __TF4main4getYFVS_11ExampleIntsSi
movq %rbx, %rdi
movq %r14, %rsi
movq %r15, %rdx
addq $8, %rsp
popq %rbx
popq %r14
popq %r15
popq %rbp
jmp __TF4main4getZFVS_11ExampleIntsSi
由于編譯器不能在每個(gè)階段都直接將相應(yīng)的值重現(xiàn),它就得把這些值存起來(lái)。結(jié)構(gòu)體的三個(gè)元素被放到 rbx、r14 和 r15 寄存器中,并在每次調(diào)用時(shí)從這些寄存器里將值加載到參數(shù)寄存器中。調(diào)用者會(huì)保存這三個(gè)寄存器,就是說(shuō)在調(diào)用過(guò)程中它們存的值會(huì)被持有。然后和之前一樣,編譯器也對(duì) getZ 生成了尾調(diào)用,以及一些更昂貴的預(yù)先清理。
函數(shù)的開始部分調(diào)用了 getExampleInts 并從 rax、rdx 和 rcx 中加載了其中的值。顯然結(jié)構(gòu)體的值是從這些寄存器里返回的,看看 getExampleInts 函數(shù)來(lái)確認(rèn)下:
pushq %rbp
movl $1, %edi
movl $2, %esi
movl $3, %edx
popq %rbp
jmp __TFV4main11ExampleIntsCfMS0_FT1xSi1ySi1zSi_S0_
這代碼把值 1、2 和 3 放進(jìn)參數(shù)寄存器中,然后調(diào)用結(jié)構(gòu)體的構(gòu)造器。下面是構(gòu)造器的生成碼:
pushq %rbp
movq %rsp, %rbp
movq %rdx, %rcx
movq %rdi, %rax
movq %rsi, %rdx
popq %rbp
retq
夠清楚了,它向 rax、rdx 和 rcx 中返回三個(gè)值。備忘單并未提及往多個(gè)寄存器中返回多個(gè)值。那官方的PDF呢?里面說(shuō)到了可以往 rax 和 rdx 中返回兩個(gè)值,卻沒說(shuō)可以給 rcx 返回第三個(gè)值。而上面的代碼還是很明確的。新語(yǔ)言有趣的地方就在這兒,它不一定非按老規(guī)矩來(lái)。要是和C語(yǔ)言聯(lián)調(diào)就得按傳統(tǒng)規(guī)范,但是 Swift 和 Swift 之間的調(diào)用就可以玩新路子了。
那 inout 參數(shù)呢?如果它是像我們?cè)?C 中所熟悉的那樣,結(jié)構(gòu)體就會(huì)被安置在內(nèi)存中,然后傳過(guò)去一個(gè)指針。下面是兩個(gè) test 函數(shù)(當(dāng)然是在兩個(gè)不同文件里的):
func testInout() {
var s = getExampleInts()
totalInout(&s)
}
func totalInout(inout parameter: ExampleInts) -> Int {
return parameter.x + parameter.y + parameter.z
}
下面是 testInout 的生成碼:
pushq %rbp
movq %rsp, %rbp
subq $32, %rsp
callq __TF4main14getExampleIntsFT_VS_11ExampleInts
movq %rax, -24(%rbp)
movq %rdx, -16(%rbp)
movq %rcx, -8(%rbp)
leaq -24(%rbp), %rdi
callq __TF4main10totalInoutFRVS_11ExampleIntsSi
addq $32, %rsp
popq %rbp
retq
函數(shù)序言中先創(chuàng)建了一個(gè) 32 字節(jié)的堆棧幀,再調(diào)用 getExampleInts,而后的調(diào)用把結(jié)果的值分別存在偏移量為 -24、-16 和 -8 的棧槽(stack slots)中。隨即計(jì)算出指向偏移為 -24 的指針,將其加載到 rdi 參數(shù)寄存器中后調(diào)用 totalInout。下面是這個(gè)函數(shù)的生成碼:
pushq %rbp
movq %rsp, %rbp
movq (%rdi), %rax
addq 8(%rdi), %rax
jo LBB4_3
addq 16(%rdi), %rax
jo LBB4_3
popq %rbp
retq
LBB4_3:
ud2
以上是從傳遞進(jìn)來(lái)的參數(shù)加載偏移量所對(duì)應(yīng)的值,合并之后將結(jié)果返回到 rax 中。jo 指令做溢出檢查。如果有任一 addq 指令引起溢出,jo 指令會(huì)跳轉(zhuǎn)到 ud2 指令,將程序終結(jié)。
可以看出這正是我們所想的那樣:把一個(gè)結(jié)構(gòu)體傳遞給一個(gè) inout 參數(shù)時(shí),該結(jié)構(gòu)體被置進(jìn)連續(xù)的內(nèi)存中,隨后得到一個(gè)指向該內(nèi)存的地址。
大結(jié)構(gòu)體
如果我們處理的是一些更大的結(jié)構(gòu)體,大到寄存器不再適合了的話會(huì)怎樣呢?下面是一個(gè)有十個(gè)元素的結(jié)構(gòu)體:
struct TenInts {
var elements = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
}
而這面是一個(gè) get 函數(shù),創(chuàng)建一該結(jié)構(gòu)體的實(shí)例并將其返回。為防止內(nèi)聯(lián)它被放在另一個(gè)文件中:
func getHuge() -> TenInts {
return TenInts()
}
一個(gè)獲取該結(jié)構(gòu)體中單個(gè)元素的函數(shù):
func getHugeElement(parameter: TenInts) -> Int {
return parameter.elements.5
}
最后是一個(gè) test 函數(shù):
func testHuge() {
let s = getHuge()
getHugeElement(s)
}
看下生成碼,從 testHuge 開始:
pushq %rbp
movq %rsp, %rbp
subq $160, %rsp
leaq -80(%rbp), %rdi
callq __TF4main7getHugeFT_VS_7TenInts
movups -80(%rbp), %xmm0
movups -64(%rbp), %xmm1
movups -48(%rbp), %xmm2
movups -32(%rbp), %xmm3
movups -16(%rbp), %xmm4
movups %xmm0, -160(%rbp)
movups %xmm1, -144(%rbp)
movups %xmm2, -128(%rbp)
movups %xmm3, -112(%rbp)
movups %xmm4, -96(%rbp)
leaq -160(%rbp), %rdi
callq __TF4main14getHugeElementFVS_7TenIntsSi
addq $160, %rsp
popq %rbp
retq
這段代碼(除去函數(shù)序言和尾聲)可以分成三部分。
第一部分計(jì)算出對(duì)這個(gè)堆棧幀有 -80 偏移量的地址,然后調(diào)用 getHuge,把計(jì)算出的地址傳參給它。getHuge 函數(shù)在源代碼里沒有任何參數(shù),但是用一個(gè)隱式參數(shù)返回較大的結(jié)構(gòu)體并不罕見。調(diào)用處為返回值分配儲(chǔ)存空間,而后給把一個(gè)指向該分配好的空間的指針傳給隱藏參數(shù)。棧中的這塊已分配空間告訴我們基本就是這樣的。
第二部分將棧偏移-80的地方結(jié)構(gòu)體復(fù)制到 -160 的地方。它將這個(gè)結(jié)構(gòu)體加載到五個(gè) xmm 寄存器中,每次加載十六字節(jié)的片段,然后把寄存器的內(nèi)容放回到從 -160 開始的地方。我不大清楚為什么編譯器要弄一個(gè)拷貝而不是直接用原始值。我懷疑優(yōu)化器可能還是不夠聰明,意識(shí)不到它根本就不需要用到拷貝。
第三部分計(jì)算出棧偏移 -160 的地址,然后調(diào)用 getHugeElement,傳參給它計(jì)算出的地址。之前的三個(gè)元素的試驗(yàn)中傳遞的是寄存器中的值,而對(duì)于這個(gè)更大的結(jié)構(gòu)體,傳遞的是指針。
其他函數(shù)的生成碼確認(rèn)了這點(diǎn):結(jié)構(gòu)體是以指針形式傳進(jìn)傳出的,并存活在棧中。從 getHugeElement 開始:
pushq %rbp
movq %rsp, %rbp
movq 40(%rdi), %rax
popq %rbp
retq
加載了離傳入?yún)?shù) 40 個(gè)偏移量的內(nèi)容。每個(gè)元素為 8 字節(jié),偏移量是 40 就是第 5 個(gè)元素,該函數(shù)返回這個(gè)值。
getHuge 函數(shù):
pushq %rbp
movq %rsp, %rbp
pushq %rbx
subq $88, %rsp
movq %rdi, %rbx
leaq -88(%rbp), %rdi
callq __TFV4main7TenIntsCfMS0_FT_S0_
movups -88(%rbp), %xmm0
movups -72(%rbp), %xmm1
movups -56(%rbp), %xmm2
movups -40(%rbp), %xmm3
movups -24(%rbp), %xmm4
movups %xmm0, (%rbx)
movups %xmm1, 16(%rbx)
movups %xmm2, 32(%rbx)
movups %xmm3, 48(%rbx)
movups %xmm4, 64(%rbx)
movq %rbx, %rax
addq $88, %rsp
popq %rbx
popq %rbp
retq
和上面的 testHuge 很像:分配棧空間,調(diào)用一個(gè)函數(shù),這回是 TenInts 構(gòu)造器函數(shù),然后把返回值復(fù)制到它最終的地方:隱式參數(shù)傳進(jìn)來(lái)的指針?biāo)赶虻牡刂贰?/p>
都說(shuō)到這兒了,看一下 TenInts 構(gòu)造器吧:
pushq %rbp
movq %rsp, %rbp
movq $1, (%rdi)
movq $2, 8(%rdi)
movq $3, 16(%rdi)
movq $4, 24(%rdi)
movq $5, 32(%rdi)
movq $6, 40(%rdi)
movq $7, 48(%rdi)
movq $8, 56(%rdi)
movq $9, 64(%rdi)
movq $10, 72(%rdi)
movq %rdi, %rax
popq %rbp
retq
類似于另一個(gè)函數(shù),它也是用了一個(gè)指向新結(jié)構(gòu)體的隱式指針作為參數(shù),然后把從 1 到 10 這些值存儲(chǔ)好,再返回。
創(chuàng)建這些test案例時(shí)我遇到過(guò)一個(gè)有意思的地方。這是一個(gè) test 函數(shù),調(diào)用了三次 getHugeElement:
func testThreeHuge() {
let s = getHuge()
getHugeElement(s)
getHugeElement(s)
getHugeElement(s)
}
其生成碼如下:
pushq %rbp
movq %rsp, %rbp
pushq %r15
pushq %r14
pushq %r13
pushq %r12
pushq %rbx
subq $392, %rsp
leaq -120(%rbp), %rdi
callq __TF4main7getHugeFT_VS_7TenInts
movq -120(%rbp), %rbx
movq %rbx, -376(%rbp)
movq -112(%rbp), %r8
movq %r8, -384(%rbp)
movq -104(%rbp), %r9
movq %r9, -392(%rbp)
movq -96(%rbp), %r10
movq %r10, -400(%rbp)
movq -88(%rbp), %r11
movq %r11, -368(%rbp)
movq -80(%rbp), %rax
movq -72(%rbp), %rcx
movq %rcx, -408(%rbp)
movq -64(%rbp), %rdx
movq %rdx, -416(%rbp)
movq -56(%rbp), %rsi
movq %rsi, -424(%rbp)
movq -48(%rbp), %rdi
movq %rdi, -432(%rbp)
movq %rbx, -200(%rbp)
movq %rbx, %r14
movq %r8, -192(%rbp)
movq %r8, %r15
movq %r9, -184(%rbp)
movq %r9, %r12
movq %r10, -176(%rbp)
movq %r10, %r13
movq %r11, -168(%rbp)
movq %rax, -160(%rbp)
movq %rax, %rbx
movq %rcx, -152(%rbp)
movq %rdx, -144(%rbp)
movq %rsi, -136(%rbp)
movq %rdi, -128(%rbp)
leaq -200(%rbp), %rdi
callq __TF4main14getHugeElementFVS_7TenIntsSi
movq %r14, -280(%rbp)
movq %r15, -272(%rbp)
movq %r12, -264(%rbp)
movq %r13, -256(%rbp)
movq -368(%rbp), %rax
movq %rax, -248(%rbp)
movq %rbx, -240(%rbp)
movq -408(%rbp), %r14
movq %r14, -232(%rbp)
movq -416(%rbp), %r15
movq %r15, -224(%rbp)
movq -424(%rbp), %r12
movq %r12, -216(%rbp)
movq -432(%rbp), %r13
movq %r13, -208(%rbp)
leaq -280(%rbp), %rdi
callq __TF4main14getHugeElementFVS_7TenIntsSi
movq -376(%rbp), %rax
movq %rax, -360(%rbp)
movq -384(%rbp), %rax
movq %rax, -352(%rbp)
movq -392(%rbp), %rax
movq %rax, -344(%rbp)
movq -400(%rbp), %rax
movq %rax, -336(%rbp)
movq -368(%rbp), %rax
movq %rax, -328(%rbp)
movq %rbx, -320(%rbp)
movq %r14, -312(%rbp)
movq %r15, -304(%rbp)
movq %r12, -296(%rbp)
movq %r13, -288(%rbp)
leaq -360(%rbp), %rdi
callq __TF4main14getHugeElementFVS_7TenIntsSi
addq $392, %rsp
popq %rbx
popq %r12
popq %r13
popq %r14
popq %r15
popq %rbp
retq
此函數(shù)的結(jié)構(gòu)和前面的那個(gè)版本類似,調(diào)用 getHuge,復(fù)制結(jié)果,然后調(diào)用三次 getHugeElement。每次的調(diào)用都再次的復(fù)制該結(jié)構(gòu)體,猜測(cè)是為了防止 getHugeElement 發(fā)生變動(dòng)。發(fā)現(xiàn)真正有意思的是這些都是使用整型寄存器、每次只復(fù)制一個(gè)元素,而不是像 testHuge 函數(shù)那樣每次往 xmm 寄存器中復(fù)制兩個(gè)元素。我不確定是什么導(dǎo)致編譯器在這里選擇了整型寄存器,看起來(lái)用 xmm 寄存器一次復(fù)制兩個(gè)元素是更有效率的,生成碼也更簡(jiǎn)潔。
我還試驗(yàn)了非常大的結(jié)構(gòu)體:
struct HundredInts {
var elements = (TenInts(), TenInts(), TenInts(), TenInts(), TenInts(), TenInts(), TenInts(), TenInts(), TenInts(), TenInts())
}
struct ThousandInts {
var elements = (HundredInts(), HundredInts(), HundredInts(), HundredInts(), HundredInts(), HundredInts(), HundredInts(), HundredInts(), HundredInts(), HundredInts())
}
func getThousandInts() -> ThousandInts {
return ThousandInts()
}
getThousandInts 的生成碼相當(dāng)?shù)寞偪瘢?/p>
pushq %rbp
pushq %rbx
subq $8008, %rsp
movq %rdi, %rbx
leaq -8008(%rbp), %rdi
callq __TFV4main12ThousandIntsCfMS0_FT_S0_
movq -8008(%rbp), %rax
movq %rax, (%rbx)
movq -8000(%rbp), %rax
movq %rax, 8(%rbx)
movq -7992(%rbp), %rax
movq %rax, 16(%rbx)
movq -7984(%rbp), %rax
movq %rax, 24(%rbx)
movq -7976(%rbp), %rax
movq %rax, 32(%rbx)
movq -7968(%rbp), %rax
movq %rax, 40(%rbx)
movq -7960(%rbp), %rax
movq %rax, 48(%rbx)
movq -7952(%rbp), %rax
movq %rax, 56(%rbx)
movq -7944(%rbp), %rax
movq %rax, 64(%rbx)
movq -7936(%rbp), %rax
movq %rax, 72(%rbx)
...
movq -104(%rbp), %rax
movq %rax, 7904(%rbx)
movq -96(%rbp), %rax
movq %rax, 7912(%rbx)
movq -88(%rbp), %rax
movups -80(%rbp), %xmm0
movups -64(%rbp), %xmm1
movups -48(%rbp), %xmm2
movups -32(%rbp), %xmm3
movq %rax, 7920(%rbx)
movq -16(%rbp), %rax
movups %xmm0, 7928(%rbx)
movups %xmm1, 7944(%rbx)
movups %xmm2, 7960(%rbx)
movups %xmm3, 7976(%rbx)
movq %rax, 7992(%rbx)
movq %rbx, %rax
addq $8008, %rsp
popq %rbx
popq %rbp
retq
編譯器為復(fù)制這個(gè)結(jié)構(gòu)體生成了兩千多條指令。這種情況下貌似調(diào)用 memcpy 函數(shù)非常合適,而我覺得為這種大的出奇的結(jié)構(gòu)體做優(yōu)化應(yīng)該不是編譯器團(tuán)隊(duì)現(xiàn)在的首要目標(biāo)。
類字段(Class Fields)
我們來(lái)看看當(dāng)結(jié)構(gòu)體的字段(struct fields)比整形復(fù)雜的多的情況下會(huì)發(fā)生什么。下面有個(gè)簡(jiǎn)單的類,然后包含了一個(gè)結(jié)構(gòu)體:
class ExampleClass {}
struct ContainsClass {
var x: Int
var y: ExampleClass
var z: Int
}
這里是一堆試驗(yàn)的函數(shù)(分在兩個(gè)不同文件中防止內(nèi)聯(lián)):
func testContainsClass() {
let s = ContainsClass(x: 1, y: getExampleClass(), z: 3)
getClassX(s)
getClassY(s)
getClassZ(s)
}
func getExampleClass() -> ExampleClass {
return ExampleClass()
}
func getClassX(parameter: ContainsClass) -> Int {
return parameter.x
}
func getClassY(parameter: ContainsClass) -> ExampleClass {
return parameter.y
}
func getClassZ(parameter: ContainsClass) -> Int {
return parameter.z
}
從 getters 的生成碼看起,首先是 getClassX:
pushq %rbp
movq %rsp, %rbp
pushq %rbx
pushq %rax
movq %rdi, %rbx
movq %rsi, %rdi
callq _swift_release
movq %rbx, %rax
addq $8, %rsp
popq %rbx
popq %rbp
retq
三個(gè)結(jié)構(gòu)體元素會(huì)被傳遞進(jìn)前三個(gè)參數(shù)寄存器中,rdi、rsi 和 rdx。該函數(shù)想通過(guò)把 rdi 中的值移動(dòng)到 rax 中再返回,但得先記錄一下才行??瓷先ニ坪跏莻魅?rsi 的對(duì)象引用被持有了,在函數(shù)返回之前是必須被釋放掉的。這段生成碼把 rdi 搬進(jìn)了一個(gè)安全的臨時(shí)寄存器 rbx,然后將對(duì)象引用移動(dòng)到 rdi,再調(diào)用 swift_release 將其釋放。隨后把 rbx 中的值移動(dòng)到 rax 中,再?gòu)暮瘮?shù)中返回。
getClassZ 也很類似,除了它是從 rdx,而不是 rdi 中獲取值的:
pushq %rbp
movq %rsp, %rbp
pushq %rbx
pushq %rax
movq %rdx, %rbx
movq %rsi, %rdi
callq _swift_release
movq %rbx, %rax
addq $8, %rsp
popq %rbx
popq %rbp
retq
getClassY 的生成碼會(huì)是特殊的一個(gè),因?yàn)樗祷氐氖菍?duì)象的引用而非一個(gè)整型:
pushq %rbp
movq %rsp, %rbp
movq %rsi, %rax
popq %rbp
retq
十分簡(jiǎn)短!它從 rsi 中取值,該值為對(duì)象引用,然后放進(jìn) rax 再將其返回。這里就不需要記錄,僅僅是數(shù)據(jù)的拖拽。顯然值傳進(jìn)來(lái)的時(shí)候被持有,被返回的時(shí)候也被持有,所以這段代碼是無(wú)需任何內(nèi)存管理的。
到目前為止我們看到的對(duì)這個(gè)結(jié)構(gòu)體的處理和前面的對(duì)那個(gè)有三個(gè)整型元素的結(jié)構(gòu)體的處理沒有太大區(qū)別,除了對(duì)象引用字段傳遞進(jìn)來(lái)是被持有的,必須要由被調(diào)用者做釋放處理。記著這一點(diǎn),我們來(lái)看下 testContainsClass 的生成碼:
pushq %rbp
movq %rsp, %rbp
pushq %r14
pushq %rbx
callq __TF4main15getExampleClassFT_CS_12ExampleClass
movq %rax, %rbx
movq %rbx, %rdi
callq _swift_retain
movq %rax, %r14
movl $1, %edi
movl $3, %edx
movq %rbx, %rsi
callq __TF4main9getClassXFVS_13ContainsClassSi
movq %r14, %rdi
callq _swift_retain
movl $1, %edi
movl $3, %edx
movq %rbx, %rsi
callq __TF4main9getClassYFVS_13ContainsClassCS_12ExampleClass
movq %rax, %rdi
callq _swift_release
movl $1, %edi
movl $3, %edx
movq %rbx, %rsi
popq %rbx
popq %r14
popq %rbp
jmp __TF4main9getClassZFVS_13ContainsClassSi
這個(gè)函數(shù)做的第一件事就是調(diào)用 getExampleClass 來(lái)獲得結(jié)構(gòu)體中存儲(chǔ)的 ExampleClass 實(shí)例,它得到返回的引用之后將其移動(dòng)到 rbx 中以安全保留。
接下來(lái)調(diào)用了 getClassX,為此它得在參數(shù)寄存器中建立一個(gè)該結(jié)構(gòu)體的拷貝。兩個(gè)整型的字段是很容易的,而對(duì)象的字段就需要按照函數(shù)所期的那樣被持有。這段代碼對(duì) rbx 中所存的值調(diào)用了 swift_retain,然后將其放入 rsi,再把 1 和 3 分別放入 rdi 和 rdx 中,構(gòu)建出完整的結(jié)構(gòu)體。最后,它調(diào)用了 getClassX。
調(diào)用 getClassY 也基本一樣,然而 getClassY 返回的是一個(gè)需要被釋放的對(duì)象,在調(diào)用之后這段代碼將返回值移動(dòng)至 rdi 中,調(diào)用 swift_release 來(lái)實(shí)線所要求的內(nèi)存管理。
函數(shù)調(diào)用 getClassZ 作為其尾調(diào)用,所以這里的生成代碼有些許不同。從 getExampleClass 得來(lái)的對(duì)象引用已被持有,所以它不需要為這個(gè)最后的調(diào)用而再被單獨(dú)持有。代碼將其放進(jìn) rsi 里,再把 1 和 3 分別放進(jìn) rdi 和 rdx 里,然后做清空棧,跳轉(zhuǎn)到 getClassZ 做最后的調(diào)用。
基本上說(shuō),與全是整型的那個(gè)結(jié)構(gòu)體相比幾乎沒有變化。唯一的實(shí)質(zhì)的不同就是復(fù)制一個(gè)帶有對(duì)象的結(jié)構(gòu)體時(shí)需要持有這個(gè)對(duì)象,銷毀這個(gè)結(jié)構(gòu)體時(shí)也需要釋放掉這個(gè)對(duì)象。
結(jié)論
Swift 中的結(jié)構(gòu)體存儲(chǔ)根本上講還是比較簡(jiǎn)單,我們看到的這些也是從C語(yǔ)言中非常簡(jiǎn)單的結(jié)構(gòu)體中延續(xù)過(guò)來(lái)的。一個(gè)結(jié)構(gòu)體實(shí)例在很大程度上可以看做是一些獨(dú)立值的松散集合,需要時(shí)這些值可以作為整體被操作。本地的結(jié)構(gòu)體變量可能會(huì)被存儲(chǔ)到棧中,其中的每個(gè)元素可能會(huì)被存到寄存器里,這取決于結(jié)構(gòu)體的大小、寄存器對(duì)余下代碼的利用以及編譯器臨時(shí)冒出的什么點(diǎn)子。小的結(jié)構(gòu)體在寄存器中傳遞和返回,大的結(jié)構(gòu)體通過(guò)引用傳遞和返回。結(jié)構(gòu)體在傳遞和返回時(shí)會(huì)被復(fù)制,盡管你可以用結(jié)構(gòu)體來(lái)實(shí)現(xiàn)寫入時(shí)復(fù)制(copy-on-write)的數(shù)據(jù)類型,但是基本的語(yǔ)言框架還是會(huì)被復(fù)制,而且在選擇復(fù)制的數(shù)據(jù)時(shí)多多少少有些盲目。
今天就到這兒吧。歡迎再回來(lái)閱讀更多的精彩編程技術(shù)。Friday Q&A 是由讀者想法驅(qū)動(dòng)的,所以如果你等不及下一期,還有一些希望看到的討論話題,就發(fā)郵件過(guò)來(lái)吧!
覺得這篇文章怎么樣?我在買一本書,里面全是這樣的文章。在 iBooks 上和 Kindle 上有售,加上一個(gè)可以直接下載的PDF和ePub格式。還有傳統(tǒng)的紙質(zhì)版本。點(diǎn)擊這里查看更多信息
本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán),最新文章請(qǐng)?jiān)L問(wèn) http://swift.gg。