Chồng và phân bổ đống phân tầng trong Go và cách chúng liên quan đến việc thu gom rác


165

Tôi mới sử dụng Go và tôi gặp một chút bất đồng giữa lập trình dựa trên ngăn xếp kiểu C trong đó các biến tự động sống trên ngăn xếp và bộ nhớ được phân bổ sống trên heap và lập trình dựa trên ngăn xếp kiểu Python trong đó điều duy nhất sống trên ngăn xếp là các tham chiếu / con trỏ tới các đối tượng trên heap.

Theo như tôi có thể nói, hai hàm sau cho cùng một đầu ra:

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

    ...

    return chunk, nil
}


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

    ...

    return &chunk, nil
}

tức là phân bổ một cấu trúc mới và trả lại nó.

Nếu tôi viết rằng trong C, cái đầu tiên sẽ đặt một đối tượng vào heap và cái thứ hai sẽ đặt nó lên ngăn xếp. Cái đầu tiên sẽ trả về một con trỏ cho heap, cái thứ hai sẽ trả về một con trỏ cho stack, nó sẽ bị bốc hơi theo thời gian hàm trả về, đó sẽ là một điều xấu.

Nếu tôi đã viết nó bằng Python (hoặc nhiều ngôn ngữ hiện đại khác trừ C #) thì ví dụ 2 sẽ không thể thực hiện được.

Tôi nhận được rằng rác Go thu thập cả hai giá trị, vì vậy cả hai hình thức trên đều ổn.

Để trích:

Lưu ý rằng, không giống như trong C, hoàn toàn ổn khi trả về địa chỉ của biến cục bộ; bộ lưu trữ liên quan đến biến còn tồn tại sau khi hàm trả về. Trong thực tế, việc lấy địa chỉ của một chữ tổng hợp sẽ phân bổ một thể hiện mới mỗi lần nó được đánh giá, vì vậy chúng ta có thể kết hợp hai dòng cuối cùng này.

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

Nhưng nó đặt ra một vài câu hỏi.

1 - Trong ví dụ 1, struct được khai báo trên heap. Còn ví dụ 2 thì sao? Đó có phải là khai báo trên ngăn xếp theo cách tương tự như trong C hay nó cũng đi theo đống?

2 - Nếu ví dụ 2 được khai báo trên ngăn xếp, làm thế nào nó tồn tại sau khi hàm trả về?

3 - Nếu ví dụ 2 thực sự được khai báo trên heap, làm thế nào mà các cấu trúc được truyền theo giá trị thay vì tham chiếu? Điểm của con trỏ trong trường hợp này là gì?

Câu trả lời:


170

Điều đáng chú ý là các từ "stack" và "heap" không xuất hiện ở bất cứ đâu trong thông số ngôn ngữ. Câu hỏi của bạn được diễn đạt với "... được khai báo trên ngăn xếp" và "... được khai báo trên heap", nhưng lưu ý rằng cú pháp khai báo Go không nói gì về stack hoặc heap.

Điều đó về mặt kỹ thuật làm cho câu trả lời cho tất cả các câu hỏi của bạn phụ thuộc vào việc thực hiện. Trong thực tế, tất nhiên, có một ngăn xếp (mỗi con goroutine!) Và một đống và một số thứ đi trên ngăn xếp và một số trên đống. Trong một số trường hợp, trình biên dịch tuân theo các quy tắc cứng nhắc (như " newluôn luôn phân bổ trên heap") và trong các trường hợp khác, trình biên dịch sẽ "phân tích thoát" để quyết định xem một đối tượng có thể sống trên ngăn xếp hay nếu nó phải được phân bổ trên heap.

Trong ví dụ 2 của bạn, phân tích thoát sẽ hiển thị con trỏ tới cấu trúc thoát và do đó trình biên dịch sẽ phải phân bổ cấu trúc. Tôi nghĩ rằng việc triển khai Go hiện tại tuân theo một quy tắc cứng nhắc trong trường hợp này, tuy nhiên, đó là nếu địa chỉ được lấy từ bất kỳ phần nào của cấu trúc, cấu trúc sẽ đi vào heap.

Đối với câu hỏi 3, chúng tôi có nguy cơ bị nhầm lẫn về thuật ngữ. Mọi thứ trong Go đều được truyền theo giá trị, không có thông qua tham chiếu. Ở đây bạn đang trả về một giá trị con trỏ. Điểm của con trỏ là gì? Xem xét sửa đổi sau đây của ví dụ của bạn:

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
}

Tôi đã sửa đổi myFunction2 để trả về struct chứ không phải địa chỉ của struct. So sánh đầu ra lắp ráp của myFunction1 và myFunction2 bây giờ,

--- 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     ,

Đừng lo lắng rằng đầu ra myFactor1 ở đây khác với câu trả lời (xuất sắc) của peterSO. Chúng tôi rõ ràng đang chạy các trình biên dịch khác nhau. Mặt khác, hãy xem rằng tôi đã điều chỉnh myFunction2 để trả về mySturationType chứ không phải * mySturationType. Cuộc gọi đến runtime.new đã biến mất, trong một số trường hợp sẽ là một điều tốt. Mặc dù vậy, đây là myFactor3,

--- 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     ,

Vẫn không có lệnh gọi runtime.new và có, nó thực sự hoạt động để trả về một đối tượng 8MB theo giá trị. Nó hoạt động, nhưng bạn thường không muốn. Điểm của một con trỏ ở đây sẽ là tránh đẩy xung quanh các đối tượng 8 MB.


9
Cảm ơn vô cùng. Tôi thực sự không hỏi "điểm của con trỏ là gì", nó giống như "điểm của con trỏ là gì khi các giá trị dường như hành xử giống như con trỏ", và dù sao thì câu trả lời đó cũng được đưa ra bởi câu trả lời của bạn.
Joe

25
Một lời giải thích ngắn về lắp ráp sẽ được đánh giá cao.
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
}

Trong cả hai trường hợp, việc triển khai Go hiện tại sẽ phân bổ bộ nhớ cho một structloại MyStructTypetrên một đống và trả về địa chỉ của nó. Các chức năng là tương đương; nguồn asm của trình biên dịch là như nhau.

--- 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     ,

Các cuộc gọi

Trong một lệnh gọi hàm, giá trị hàm và đối số được ước tính theo thứ tự thông thường. Sau khi chúng được ước tính, các tham số của cuộc gọi được truyền theo giá trị cho hàm và hàm được gọi bắt đầu thực thi. Các tham số trả về của hàm được truyền theo giá trị trở lại hàm gọi khi hàm trả về.

Tất cả các tham số hàm và trả về được truyền theo giá trị. Giá trị tham số trả về với loại *MyStructTypelà một địa chỉ.


Cảm ơn rất nhiều! Được khuyến khích, nhưng tôi chấp nhận Sonia vì một chút về phân tích thoát.
Joe

1
peterSo, bạn và @Sonia sản xuất lắp ráp đó như thế nào? Cả hai bạn có cùng định dạng. Tôi không thể sản xuất nó bất kể lệnh / cờ, đã thử objdump, go tool, otool.
10 cls

3
À, hiểu rồi - gcflags.
10 ngày 10

30

Theo Câu hỏi thường gặp của Go :

nếu trình biên dịch không thể chứng minh rằng biến không được tham chiếu sau khi hàm trả về, thì trình biên dịch phải phân bổ biến trên heap thu thập rác để tránh các lỗi con trỏ lơ lửng.



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

    ...

    return chunk, nil
}


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

    ...

    return &chunk, nil
}

Function1 và Function2 có thể là hàm nội tuyến. Và biến trở lại sẽ không thoát. Không cần thiết phải phân bổ biến trên heap.

Mã ví dụ của tôi:

 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  }

Theo đầu ra của cmd:

go run -gcflags -m test.go

đầu ra:

# 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

Nếu trình biên dịch đủ thông minh, F1 () F2 () F3 () có thể không được gọi. Bởi vì nó không có nghĩa.

Đừng quan tâm đến việc một biến được phân bổ trên heap hay stack, chỉ cần sử dụng nó. Bảo vệ nó bằng mutex hoặc kênh nếu cần thiết.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.