Friday Q&A 2016-01-29: Swift 的結(jié)構(gòu)體存儲(chǔ)

作者: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。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 原文地址:C語(yǔ)言函數(shù)調(diào)用棧(一)C語(yǔ)言函數(shù)調(diào)用棧(二) 0 引言 程序的執(zhí)行過(guò)程可看作連續(xù)的函數(shù)調(diào)用。當(dāng)一個(gè)函數(shù)執(zhí)...
    小豬啊嗚閱讀 4,972評(píng)論 1 19
  • 一、溫故而知新 1. 內(nèi)存不夠怎么辦 內(nèi)存簡(jiǎn)單分配策略的問(wèn)題地址空間不隔離內(nèi)存使用效率低程序運(yùn)行的地址不確定 關(guān)于...
    SeanCST閱讀 8,133評(píng)論 0 27
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,569評(píng)論 19 139
  • 第5章 引用類型(返回首頁(yè)) 本章內(nèi)容 使用對(duì)象 創(chuàng)建并操作數(shù)組 理解基本的JavaScript類型 使用基本類型...
    大學(xué)一百閱讀 3,679評(píng)論 0 4
  • 一直在說(shuō)社群,但把社群做的很好的極少。目前做社群做的比較好的其實(shí)都是自媒體人,如秋葉,張輝彭縈的改變自己。這種社群...
    健健大俠閱讀 1,594評(píng)論 0 50

友情鏈接更多精彩內(nèi)容