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

作者:Mike Ash弥喉,原文鏈接憋肖,原文日期:2016-01-29
譯者:ray16897188朝群;校對:Channe姜盈;定稿:千葉知風(fēng)

Swift 的類對大多數(shù)剛接觸編程語言的人來說是很容易理解的哭当,它們和其他語言中的類十分類似猪腕。無論你是從 Objective-C、Java 還是 Ruby 過來的钦勘,在 Swift 中對于類的使用并無太大區(qū)別陋葡。而 Swift 中的結(jié)構(gòu)體就是另外一回事兒了,它們有點(diǎn)兒像類彻采,但是它們是值類型腐缤,還沒有繼承,另外我總是聽到這個(gè)什么 copy-on-write(寫入時(shí)復(fù)制)的說法肛响。那么 Swift 中的結(jié)構(gòu)體是存在哪里岭粤?它們是怎么個(gè)工作原理?今天我們來仔細(xì)研究一下如何在內(nèi)存中保存和操作結(jié)構(gòu)體特笋。

簡單的結(jié)構(gòu)體

我建了一個(gè)有兩個(gè)文件的程序來探究一下結(jié)構(gòu)體在內(nèi)存中是怎樣存儲的剃浇。對這個(gè)測試程序采用 optimizations enabled 選項(xiàng)編譯,并取消 whole-module optimization 選項(xiàng)猎物。此測試是讓一個(gè)文件調(diào)用一個(gè)文件虎囚,這就會防止編譯器將所有東西都內(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)
    }

針對 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

基本一樣彩匕,只不過它返回的是第二個(gè)參數(shù)。那 getZ 呢媒区?

pushq   %rbp
    movq    %rsp, %rbp

    movq    %rdx, %rax

    popq    %rbp
    retq

還是驼仪,基本都一樣,但返回的是第三個(gè)參數(shù)袜漩。從這我們可以看出來每個(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 對應(yīng)的低 32 bit 帶寬版本雹顺。)這樣甚至不用在調(diào)用途中操心值的額外存儲丹墨,只需每次調(diào)用時(shí)重建這個(gè)結(jié)構(gòu)體的實(shí)例就好了。因?yàn)榫幾g器明確的知道寄存器中的內(nèi)容嬉愧,這就可以大大的改變 Swift 的代碼編寫方式贩挣。注意到對 getZ 的調(diào)用和對 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í)會生成怎樣的代碼绒净。下面是這個(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 會生成怎樣的代碼呢下翎?

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)缤言,它就得把這些值存起來。結(jié)構(gòu)體的三個(gè)元素被放到 rbx视事、r14 和 r15 寄存器中胆萧,并在每次調(diào)用時(shí)從這些寄存器里將值加載到參數(shù)寄存器中。調(diào)用者會保存這三個(gè)寄存器俐东,就是說在調(diào)用過程中它們存的值會被持有跌穗。然后和之前一樣订晌,編譯器也對 getZ 生成了尾調(diào)用,以及一些更昂貴的預(yù)先清理蚌吸。

函數(shù)的開始部分調(diào)用了 getExampleInts 并從 rax锈拨、rdx 和 rcx 中加載了其中的值。顯然結(jié)構(gòu)體的值是從這些寄存器里返回的套利,看看 getExampleInts 函數(shù)來確認(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呢?里面說到了可以往 rax 和 rdx 中返回兩個(gè)值族购,卻沒說可以給 rcx 返回第三個(gè)值壳贪。而上面的代碼還是很明確的。新語言有趣的地方就在這兒寝杖,它不一定非按老規(guī)矩來违施。要是和C語言聯(lián)調(diào)就得按傳統(tǒng)規(guī)范,但是 Swift 和 Swift 之間的調(diào)用就可以玩新路子了瑟幕。

那 inout 參數(shù)呢磕蒲?如果它是像我們在 C 中所熟悉的那樣,結(jié)構(gòu)體就會被安置在內(nèi)存中只盹,然后傳過去一個(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)來的參數(shù)加載偏移量所對應(yīng)的值,合并之后將結(jié)果返回到 rax 中肛冶。jo 指令做溢出檢查街氢。如果有任一 addq 指令引起溢出,jo 指令會跳轉(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)體,大到寄存器不再適合了的話會怎樣呢董习?下面是一個(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ì)算出對這個(gè)堆棧幀有 -80 偏移量的地址,然后調(diào)用 getHuge窝趣,把計(jì)算出的地址傳參給它疯暑。getHuge 函數(shù)在源代碼里沒有任何參數(shù),但是用一個(gè)隱式參數(shù)返回較大的結(jié)構(gòu)體并不罕見哑舒。調(diào)用處為返回值分配儲存空間妇拯,而后給把一個(gè)指向該分配好的空間的指針傳給隱藏參數(shù)。棧中的這塊已分配空間告訴我們基本就是這樣的洗鸵。

第二部分將棧偏移-80的地方結(jié)構(gòu)體復(fù)制到 -160 的地方越锈。它將這個(gè)結(jié)構(gòu)體加載到五個(gè) xmm 寄存器中,每次加載十六字節(jié)的片段膘滨,然后把寄存器的內(nèi)容放回到從 -160 開始的地方甘凭。我不大清楚為什么編譯器要弄一個(gè)拷貝而不是直接用原始值。我懷疑優(yōu)化器可能還是不夠聰明火邓,意識不到它根本就不需要用到拷貝丹弱。

第三部分計(jì)算出棧偏移 -160 的地址,然后調(diào)用 getHugeElement贡翘,傳參給它計(jì)算出的地址蹈矮。之前的三個(gè)元素的試驗(yàn)中傳遞的是寄存器中的值,而對于這個(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)來的指針?biāo)赶虻牡刂贰?/p>

都說到這兒了,看一下 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 這些值存儲好,再返回壳猜。

創(chuàng)建這些test案例時(shí)我遇到過一個(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)體吹由,猜測是為了防止 getHugeElement 發(fā)生變動(dòng)。發(fā)現(xiàn)真正有意思的是這些都是使用整型寄存器盯腌、每次只復(fù)制一個(gè)元素溉知,而不是像 testHuge 函數(shù)那樣每次往 xmm 寄存器中復(fù)制兩個(gè)元素。我不確定是什么導(dǎo)致編譯器在這里選擇了整型寄存器腕够,看起來用 xmm 寄存器一次復(fù)制兩個(gè)元素是更有效率的级乍,生成碼也更簡潔。

我還試驗(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)

我們來看看當(dāng)結(jié)構(gòu)體的字段(struct fields)比整形復(fù)雜的多的情況下會發(fā)生什么大诸。下面有個(gè)簡單的類捅厂,然后包含了一個(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)體元素會被傳遞進(jìn)前三個(gè)參數(shù)寄存器中资柔,rdi焙贷、rsi 和 rdx。該函數(shù)想通過把 rdi 中的值移動(dòng)到 rax 中再返回贿堰,但得先記錄一下才行辙芍。看上去似乎是傳入 rsi 的對象引用被持有了羹与,在函數(shù)返回之前是必須被釋放掉的故硅。這段生成碼把 rdi 搬進(jìn)了一個(gè)安全的臨時(shí)寄存器 rbx,然后將對象引用移動(dòng)到 rdi纵搁,再調(diào)用 swift_release 將其釋放吃衅。隨后把 rbx 中的值移動(dòng)到 rax 中,再從函數(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 的生成碼會是特殊的一個(gè),因?yàn)樗祷氐氖菍ο蟮囊枚且粋€(gè)整型:

pushq   %rbp
    movq    %rsp, %rbp
    movq    %rsi, %rax
    popq    %rbp
    retq

十分簡短惑灵!它從 rsi 中取值山上,該值為對象引用眼耀,然后放進(jìn) rax 再將其返回英支。這里就不需要記錄,僅僅是數(shù)據(jù)的拖拽哮伟。顯然值傳進(jìn)來的時(shí)候被持有干花,被返回的時(shí)候也被持有,所以這段代碼是無需任何內(nèi)存管理的楞黄。

到目前為止我們看到的對這個(gè)結(jié)構(gòu)體的處理和前面的對那個(gè)有三個(gè)整型元素的結(jié)構(gòu)體的處理沒有太大區(qū)別池凄,除了對象引用字段傳遞進(jìn)來是被持有的,必須要由被調(diào)用者做釋放處理鬼廓。記著這一點(diǎn)肿仑,我們來看下 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 來獲得結(jié)構(gòu)體中存儲的 ExampleClass 實(shí)例,它得到返回的引用之后將其移動(dòng)到 rbx 中以安全保留碎税。

接下來調(diào)用了 getClassX尤慰,為此它得在參數(shù)寄存器中建立一個(gè)該結(jié)構(gòu)體的拷貝。兩個(gè)整型的字段是很容易的雷蹂,而對象的字段就需要按照函數(shù)所期的那樣被持有伟端。這段代碼對 rbx 中所存的值調(diào)用了 swift_retain,然后將其放入 rsi匪煌,再把 1 和 3 分別放入 rdi 和 rdx 中责蝠,構(gòu)建出完整的結(jié)構(gòu)體。最后萎庭,它調(diào)用了 getClassX霜医。

調(diào)用 getClassY 也基本一樣,然而 getClassY 返回的是一個(gè)需要被釋放的對象驳规,在調(diào)用之后這段代碼將返回值移動(dòng)至 rdi 中肴敛,調(diào)用 swift_release 來實(shí)線所要求的內(nèi)存管理。

函數(shù)調(diào)用 getClassZ 作為其尾調(diào)用达舒,所以這里的生成代碼有些許不同值朋。從 getExampleClass 得來的對象引用已被持有,所以它不需要為這個(gè)最后的調(diào)用而再被單獨(dú)持有巩搏。代碼將其放進(jìn) rsi 里昨登,再把 1 和 3 分別放進(jìn) rdi 和 rdx 里,然后做清空棧贯底,跳轉(zhuǎn)到 getClassZ 做最后的調(diào)用丰辣。

基本上說撒强,與全是整型的那個(gè)結(jié)構(gòu)體相比幾乎沒有變化。唯一的實(shí)質(zhì)的不同就是復(fù)制一個(gè)帶有對象的結(jié)構(gòu)體時(shí)需要持有這個(gè)對象笙什,銷毀這個(gè)結(jié)構(gòu)體時(shí)也需要釋放掉這個(gè)對象飘哨。

結(jié)論

Swift 中的結(jié)構(gòu)體存儲根本上講還是比較簡單,我們看到的這些也是從C語言中非常簡單的結(jié)構(gòu)體中延續(xù)過來的琐凭。一個(gè)結(jié)構(gòu)體實(shí)例在很大程度上可以看做是一些獨(dú)立值的松散集合芽隆,需要時(shí)這些值可以作為整體被操作。本地的結(jié)構(gòu)體變量可能會被存儲到棧中统屈,其中的每個(gè)元素可能會被存到寄存器里胚吁,這取決于結(jié)構(gòu)體的大小、寄存器對余下代碼的利用以及編譯器臨時(shí)冒出的什么點(diǎn)子愁憔。小的結(jié)構(gòu)體在寄存器中傳遞和返回腕扶,大的結(jié)構(gòu)體通過引用傳遞和返回。結(jié)構(gòu)體在傳遞和返回時(shí)會被復(fù)制吨掌,盡管你可以用結(jié)構(gòu)體來實(shí)現(xiàn)寫入時(shí)復(fù)制(copy-on-write)的數(shù)據(jù)類型半抱,但是基本的語言框架還是會被復(fù)制,而且在選擇復(fù)制的數(shù)據(jù)時(shí)多多少少有些盲目膜宋。

今天就到這兒吧窿侈。歡迎再回來閱讀更多的精彩編程技術(shù)。Friday Q&A 是由讀者想法驅(qū)動(dòng)的激蹲,所以如果你等不及下一期棉磨,還有一些希望看到的討論話題,就發(fā)郵件過來吧学辱!

覺得這篇文章怎么樣乘瓤?我在買一本書,里面全是這樣的文章策泣。在 iBooks 上和 Kindle 上有售衙傀,加上一個(gè)可以直接下載的PDF和ePub格式。還有傳統(tǒng)的紙質(zhì)版本萨咕。點(diǎn)擊這里查看更多信息
本文由 SwiftGG 翻譯組翻譯统抬,已經(jīng)獲得作者翻譯授權(quán),最新文章請?jiān)L問 http://swift.gg危队。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末聪建,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子茫陆,更是在濱河造成了極大的恐慌金麸,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件簿盅,死亡現(xiàn)場離奇詭異挥下,居然都是意外死亡揍魂,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進(jìn)店門棚瘟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來现斋,“玉大人,你說我怎么就攤上這事偎蘸∽#” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵禀苦,是天一觀的道長蔓肯。 經(jīng)常有香客問我,道長振乏,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任秉扑,我火速辦了婚禮慧邮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘舟陆。我一直安慰自己误澳,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布秦躯。 她就那樣靜靜地躺著忆谓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪踱承。 梳的紋絲不亂的頭發(fā)上倡缠,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天,我揣著相機(jī)與錄音茎活,去河邊找鬼昙沦。 笑死,一個(gè)胖子當(dāng)著我的面吹牛载荔,可吹牛的內(nèi)容都是我干的盾饮。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼懒熙,長吁一口氣:“原來是場噩夢啊……” “哼丘损!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起工扎,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤徘钥,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后定庵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吏饿,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡踪危,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了猪落。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贞远。...
    茶點(diǎn)故事閱讀 40,096評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖笨忌,靈堂內(nèi)的尸體忽然破棺而出蓝仲,到底是詐尸還是另有隱情,我是刑警寧澤官疲,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布袱结,位于F島的核電站,受9級特大地震影響途凫,放射性物質(zhì)發(fā)生泄漏垢夹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一维费、第九天 我趴在偏房一處隱蔽的房頂上張望果元。 院中可真熱鬧,春花似錦犀盟、人聲如沸而晒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽倡怎。三九已至,卻和暖如春贱枣,著一層夾襖步出監(jiān)牢的瞬間监署,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工冯事, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留焦匈,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓昵仅,卻偏偏與公主長得像缓熟,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子摔笤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容

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