การจัดสรร stack vs heap ของ structs ใน Go และความเกี่ยวข้องกับการรวบรวมขยะ


165

ฉันยังใหม่กับ Go และฉันรู้สึกสับสนเล็กน้อยระหว่างการเขียนโปรแกรมแบบ C-style โดยที่ตัวแปรอัตโนมัติอยู่บนสแต็กและการจัดสรรหน่วยความจำจะมีชีวิตอยู่บนฮีปและการเขียนโปรแกรมแบบสแต็กตาม Python สิ่งเดียวที่อยู่บนสแต็กคือการอ้างอิง / พอยน์เตอร์ไปยังวัตถุบนฮีป

เท่าที่ฉันสามารถบอกได้ฟังก์ชั่นทั้งสองต่อไปนี้ให้ผลลัพธ์เหมือนกัน:

func myFunction() (*MyStructType, error) {
    var chunk *MyStructType = new(HeaderChunk)

    ...

    return chunk, nil
}


func myFunction() (*MyStructType, error) {
    var chunk MyStructType

    ...

    return &chunk, nil
}

เช่นจัดสรรโครงสร้างใหม่และส่งคืน

ถ้าฉันเขียนว่าใน C วัตถุแรกจะวางวัตถุบนกองและอันที่สองจะวางบนกอง ตัวแรกจะกลับตัวชี้ไปที่กองที่สองจะกลับตัวชี้ไปที่กองซึ่งจะระเหยไปตามเวลาที่ฟังก์ชันส่งคืนซึ่งจะเป็นสิ่งที่ไม่ดี

ถ้าฉันเขียนมันด้วยภาษาไพ ธ อน (หรือภาษาสมัยใหม่อื่น ๆ อีกมากมายยกเว้น C #) ตัวอย่างที่ 2 คงเป็นไปไม่ได้

ฉันได้รับว่าขยะ Go เก็บค่าทั้งสองดังนั้นทั้งสองรูปแบบดังกล่าวข้างต้นได้ดี

อ้างถึง:

โปรดทราบว่าไม่เหมือนใน C จึงเป็นเรื่องที่ดีที่จะคืนค่าที่อยู่ของตัวแปรโลคอล หน่วยเก็บที่เกี่ยวข้องกับตัวแปรยังมีชีวิตอยู่หลังจากที่ฟังก์ชันส่งคืน ในความเป็นจริงการจดที่อยู่ของตัวอักษรผสมจะจัดสรรอินสแตนซ์สดใหม่ทุกครั้งที่ประเมินดังนั้นเราจึงสามารถรวมสองบรรทัดสุดท้ายนี้ได้

http://golang.org/doc/effective_go.html#functions

แต่มันก็ทำให้เกิดคำถามสองสามข้อ

1 - ในตัวอย่าง 1 โครงสร้างจะถูกประกาศบนฮีป ตัวอย่างที่ 2 เป็นอย่างไร นั่นคือสิ่งที่ประกาศไว้ในสแต็กในลักษณะเดียวกับที่มันเป็นใน C หรือมันไปบนกองด้วยหรือไม่

2 - ถ้าตัวอย่าง 2 ถูกประกาศบนสแต็คมันจะยังคงมีอยู่หลังจากฟังก์ชันส่งคืนอย่างไร

3 - ถ้าตัวอย่าง 2 ถูกประกาศบนฮีปจริง ๆ แล้วมันเป็นอย่างไรที่ structs ถูกส่งผ่านโดยค่ามากกว่าโดยอ้างอิง? ประเด็นของพอยน์เตอร์ในกรณีนี้คืออะไร?

คำตอบ:


170

เป็นที่น่าสังเกตว่าคำว่า "stack" และ "heap" ไม่ปรากฏที่ใดก็ได้ในข้อมูลจำเพาะภาษา คำถามของคุณมีคำว่า "... ถูกประกาศบนสแต็ก" และ "... ประกาศที่ฮีป" แต่โปรดทราบว่าไวยากรณ์การประกาศของ Go ไม่มีอะไรที่เกี่ยวกับสแต็กหรือฮีป

ในทางเทคนิคนั้นจะทำให้คำตอบสำหรับคำถามของคุณทั้งหมดขึ้นอยู่กับการนำไปใช้ ในความเป็นจริงแน่นอนว่ามีกอง (ต่อ goroutine!) และกองและบางสิ่งไปในกองและบางกอง ในบางกรณีคอมไพเลอร์จะปฏิบัติตามกฎที่เข้มงวด (เช่น " newจัดสรรเสมอบนฮีป") และในบางกรณีคอมไพเลอร์จะทำการ "หลบหนีการวิเคราะห์" เพื่อตัดสินใจว่าวัตถุสามารถอยู่บนสแต็กหรือต้องจัดสรรบนฮีป

ในตัวอย่างที่ 2 ของคุณการวิเคราะห์แบบ escape จะแสดงตัวชี้ไปยัง struct escaping และคอมไพเลอร์จะต้องจัดสรรโครงสร้าง ฉันคิดว่าการใช้งาน Go ในปัจจุบันเป็นไปตามกฎที่เข้มงวดในกรณีนี้ซึ่งก็คือหากที่อยู่นั้นถูกนำไปใช้ในส่วนใด ๆ ของโครงสร้าง struct จะดำเนินการตามฮีป

สำหรับคำถามที่ 3 เรามีความเสี่ยงที่จะสับสนเกี่ยวกับคำศัพท์ ทุกอย่างใน Go นั้นผ่านไปแล้วตามตัวอักษรไม่มีการอ้างอิงโดยผ่าน ที่นี่คุณจะคืนค่าพอยน์เตอร์ พอยน์เตอร์คืออะไร พิจารณาการดัดแปลงตัวอย่างของคุณดังต่อไปนี้:

type MyStructType struct{}

func myFunction1() (*MyStructType, error) {
    var chunk *MyStructType = new(MyStructType)
    // ...
    return chunk, nil
}

func myFunction2() (MyStructType, error) {
    var chunk MyStructType
    // ...
    return chunk, nil
}

type bigStruct struct {
    lots [1e6]float64
}

func myFunction3() (bigStruct, error) {
    var chunk bigStruct
    // ...
    return chunk, nil
}

ฉันแก้ไข myFunction2 เพื่อส่งกลับ struct มากกว่าที่อยู่ของ struct เปรียบเทียบเอาต์พุตชุดประกอบของ myFunction1 และ myFunction2 ทันที

--- prog list "myFunction1" ---
0000 (s.go:5) TEXT    myFunction1+0(SB),$16-24
0001 (s.go:6) MOVQ    $type."".MyStructType+0(SB),(SP)
0002 (s.go:6) CALL    ,runtime.new+0(SB)
0003 (s.go:6) MOVQ    8(SP),AX
0004 (s.go:8) MOVQ    AX,.noname+0(FP)
0005 (s.go:8) MOVQ    $0,.noname+8(FP)
0006 (s.go:8) MOVQ    $0,.noname+16(FP)
0007 (s.go:8) RET     ,

--- prog list "myFunction2" ---
0008 (s.go:11) TEXT    myFunction2+0(SB),$0-16
0009 (s.go:12) LEAQ    chunk+0(SP),DI
0010 (s.go:12) MOVQ    $0,AX
0011 (s.go:14) LEAQ    .noname+0(FP),BX
0012 (s.go:14) LEAQ    chunk+0(SP),BX
0013 (s.go:14) MOVQ    $0,.noname+0(FP)
0014 (s.go:14) MOVQ    $0,.noname+8(FP)
0015 (s.go:14) RET     ,

ไม่ต้องกังวลว่าเอาต์พุต myFunction1 ที่นี่จะแตกต่างจากคำตอบของ peterSO (ยอดเยี่ยม) เห็นได้ชัดว่าเราใช้คอมไพเลอร์ต่างกัน มิฉะนั้นดูว่าฉันปรับเปลี่ยน myFunction2 เพื่อส่งกลับ myStructType มากกว่า * myStructType การเรียกใช้ runtime.new หายไปซึ่งในบางกรณีจะเป็นสิ่งที่ดี รอสักครู่นี่ myFunction3 คือ

--- prog list "myFunction3" ---
0016 (s.go:21) TEXT    myFunction3+0(SB),$8000000-8000016
0017 (s.go:22) LEAQ    chunk+-8000000(SP),DI
0018 (s.go:22) MOVQ    $0,AX
0019 (s.go:22) MOVQ    $1000000,CX
0020 (s.go:22) REP     ,
0021 (s.go:22) STOSQ   ,
0022 (s.go:24) LEAQ    chunk+-8000000(SP),SI
0023 (s.go:24) LEAQ    .noname+0(FP),DI
0024 (s.go:24) MOVQ    $1000000,CX
0025 (s.go:24) REP     ,
0026 (s.go:24) MOVSQ   ,
0027 (s.go:24) MOVQ    $0,.noname+8000000(FP)
0028 (s.go:24) MOVQ    $0,.noname+8000008(FP)
0029 (s.go:24) RET     ,

ยังคงไม่มีการเรียกใช้ runtime.new และใช่มันใช้งานได้จริงเพื่อส่งคืนวัตถุ 8MB ตามค่า มันใช้งานได้ แต่ปกติคุณจะไม่ต้องการ จุดของตัวชี้ที่นี่คือการหลีกเลี่ยงการผลักวัตถุ 8MB รอบ ๆ


9
ขอบคุณมาก ฉันไม่ได้ถามจริง ๆ ว่า "อะไรคือจุดของพอยน์เตอร์เลย" มันก็เหมือนกับ "สิ่งที่เป็นพอยน์เตอร์เมื่อค่าปรากฏขึ้นเพื่อทำตัวเหมือนพอยน์เตอร์" และในกรณีนั้นคำตอบของคุณนั้น
Joe

25
คำอธิบายสั้น ๆ ของการชุมนุมจะได้รับการชื่นชม
ElefEnt

59
type MyStructType struct{}

func myFunction1() (*MyStructType, error) {
    var chunk *MyStructType = new(MyStructType)
    // ...
    return chunk, nil
}

func myFunction2() (*MyStructType, error) {
    var chunk MyStructType
    // ...
    return &chunk, nil
}

ในทั้งสองกรณีการนำไปใช้งานในปัจจุบันของ Go จะจัดสรรหน่วยความจำสำหรับstructประเภทMyStructTypeบนฮีปและส่งคืนที่อยู่ ฟังก์ชั่นเทียบเท่า คอมไพเลอร์ asm source เหมือนกัน

--- prog list "myFunction1" ---
0000 (temp.go:9) TEXT    myFunction1+0(SB),$8-12
0001 (temp.go:10) MOVL    $type."".MyStructType+0(SB),(SP)
0002 (temp.go:10) CALL    ,runtime.new+0(SB)
0003 (temp.go:10) MOVL    4(SP),BX
0004 (temp.go:12) MOVL    BX,.noname+0(FP)
0005 (temp.go:12) MOVL    $0,AX
0006 (temp.go:12) LEAL    .noname+4(FP),DI
0007 (temp.go:12) STOSL   ,
0008 (temp.go:12) STOSL   ,
0009 (temp.go:12) RET     ,

--- prog list "myFunction2" ---
0010 (temp.go:15) TEXT    myFunction2+0(SB),$8-12
0011 (temp.go:16) MOVL    $type."".MyStructType+0(SB),(SP)
0012 (temp.go:16) CALL    ,runtime.new+0(SB)
0013 (temp.go:16) MOVL    4(SP),BX
0014 (temp.go:18) MOVL    BX,.noname+0(FP)
0015 (temp.go:18) MOVL    $0,AX
0016 (temp.go:18) LEAL    .noname+4(FP),DI
0017 (temp.go:18) STOSL   ,
0018 (temp.go:18) STOSL   ,
0019 (temp.go:18) RET     ,

โทร

ในการเรียกใช้ฟังก์ชันค่าฟังก์ชันและอาร์กิวเมนต์จะถูกประเมินตามลำดับปกติ หลังจากได้รับการประเมินแล้วพารามิเตอร์ของการโทรจะถูกส่งผ่านตามค่าไปยังฟังก์ชันและฟังก์ชันที่เรียกใช้เริ่มต้นการดำเนินการ พารามิเตอร์ส่งคืนของฟังก์ชันถูกส่งผ่านโดยค่ากลับไปยังฟังก์ชันการเรียกเมื่อฟังก์ชันส่งคืน

ฟังก์ชันและพารามิเตอร์ส่งคืนทั้งหมดจะถูกส่งผ่านตามค่า ค่าพารามิเตอร์ส่งคืนพร้อมชนิด*MyStructTypeเป็นที่อยู่


ขอบคุณมาก ๆ! อัปเดตแล้ว แต่ฉันยอมรับของ Sonia เพราะเรื่องเล็กน้อยเกี่ยวกับการวิเคราะห์การหลบหนี
Joe

1
peterSo คุณเป็นอย่างไรและ @Sonia ผลิตชุดประกอบนั้นอย่างไร? คุณทั้งสองมีรูปแบบเดียวกัน ฉันไม่สามารถสร้างมันได้โดยไม่คำนึงถึงคำสั่ง / แฟล็กโดยลอง objdump ไปใช้เครื่องมือ otool
10 cls

3
อ้าเข้าใจแล้ว - gcflags
10 cls 10

30

ตามไปคำถามที่พบบ่อย :

หากคอมไพเลอร์ไม่สามารถพิสูจน์ได้ว่าตัวแปรนั้นไม่ได้ถูกอ้างอิงหลังจากฟังก์ชั่นส่งคืนแล้วคอมไพเลอร์จะต้องจัดสรรตัวแปรในฮีปที่รวบรวมขยะเพื่อหลีกเลี่ยงข้อผิดพลาดของตัวชี้ห้อย


11

คุณไม่ทราบว่าตัวแปรของคุณได้รับการจัดสรรในกองซ้อนหรือกอง
...
หากคุณจำเป็นต้องรู้ว่าตัวแปรของคุณได้รับการจัดสรรที่ไหนให้ผ่านธง "-m" gc ไปที่ "go build" หรือ "go run" (เช่นgo run -gcflags -m app.go)

ที่มา: http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/index.html#stack_heap_vars


0
func Function1() (*MyStructType, error) {
    var chunk *MyStructType = new(HeaderChunk)

    ...

    return chunk, nil
}


func Function2() (*MyStructType, error) {
    var chunk MyStructType

    ...

    return &chunk, nil
}

Function1 และ Function2 อาจเป็นฟังก์ชันแบบอินไลน์ และตัวแปรส่งคืนจะไม่หนี ไม่จำเป็นต้องจัดสรรตัวแปรในฮีป

รหัสตัวอย่างของฉัน:

 1  package main
 2  
 3  type S struct {
 4          x int
 5  }
 6  
 7  func main() {
 8          F1()
 9          F2()
10          F3()
11  }
12  
13  func F1() *S {
14          s := new(S)
15          return s
16  }
17  
18  func F2() *S {
19          s := S{x: 10}
20          return &s
21  }
22  
23  func F3() S {
24          s := S{x: 9}
25          return s
26  }

ตามการส่งออกของ cmd:

go run -gcflags -m test.go

เอาท์พุท:

# command-line-arguments
./test.go:13:6: can inline F1
./test.go:18:6: can inline F2
./test.go:23:6: can inline F3
./test.go:7:6: can inline main
./test.go:8:4: inlining call to F1
./test.go:9:4: inlining call to F2
./test.go:10:4: inlining call to F3
/var/folders/nr/lxtqsz6x1x1gfbyp1p0jy4p00000gn/T/go-build333003258/b001/_gomod_.go:6:6: can inline init.0
./test.go:8:4: main new(S) does not escape
./test.go:9:4: main &s does not escape
./test.go:14:10: new(S) escapes to heap
./test.go:20:9: &s escapes to heap
./test.go:19:2: moved to heap: s

หากคอมไพเลอร์ฉลาดพอแล้วF1 () F2 () F3 ()อาจไม่ถูกเรียกใช้ เพราะมันไม่ได้หมายความว่า

ไม่สนใจว่าจะมีการจัดสรรตัวแปรในฮีปหรือสแต็กเพียงแค่ใช้ ป้องกันโดย mutex หรือช่องถ้าจำเป็น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.