Go에서 구조체의 스택과 힙 할당 및 가비지 수집과의 관계


165

나는 Go를 처음 사용하고 자동 변수가 스택에 있고 메모리에 힙이 할당되는 C 스타일 스택 기반 프로그래밍과 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가 실제로 힙에서 선언되면 구조체가 참조가 아닌 값으로 전달되는 방법은 무엇입니까? 이 경우 포인터의 요점은 무엇입니까?

답변:


170

"스택"및 "힙"이라는 단어는 언어 사양의 어느 곳에도 나타나지 않습니다. 귀하의 질문은 "... 스택에 선언되었습니다"및 "... 힙에 선언되었습니다"로 표시되지만 Go 선언 구문은 스택 또는 힙에 대해서는 아무 것도 언급하지 않습니다.

기술적으로 모든 질문 구현에 대한 답변을 의존적으로 만듭니다. 실제로, 스택 (고 루틴 당!)과 힙이 있으며 일부는 스택과 힙에 있습니다. 경우에 따라 컴파일러는 엄격한 규칙 ( " new항상 힙에 할당")을 따르고 다른 경우에는 "탈출 분석"을 수행하여 객체가 스택에 존재할 수 있는지 또는 힙에 할당되어야하는지 결정합니다.

예제 2에서 이스케이프 분석은 구조체 이스케이프에 대한 포인터를 표시하므로 컴파일러는 구조체를 할당해야합니다. 이 경우 Go의 현재 구현은 엄격한 규칙을 따른다고 생각합니다. 즉, 주소가 구조체의 일부에서 가져 오면 구조체가 힙에 간다는 것입니다.

질문 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를 수정했습니다. 이제 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의 (우수한) 답변과 다르다는 것을 걱정하지 마십시오. 우리는 분명히 다른 컴파일러를 실행하고 있습니다. 그렇지 않으면 * myStructType 대신 myStructType을 반환하도록 myFunction2를 수정 한 것을 참조하십시오. 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 소스는 동일합니다.

--- 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이있는 리턴 매개 변수 값 은 주소입니다.


매우 감사합니다! 피의자는 있지만 탈출 분석에 대한 소문 때문에 소니아를 받아들입니다.
Joe

1
피터, 당신과 @ 소니아는 어떻게 그 어셈블리를 생산합니까? 둘 다 같은 형식입니다. objdump, go tool, otool을 시도한 명령 / 플래그에 관계없이 생산할 수 없습니다.
10 cls

3
아, 알았어-gcflags.
10 cls

30

Go의 FAQ 에 따르면 :

컴파일러가 함수가 반환 된 후 변수가 참조되지 않았 음을 증명할 수없는 경우 컴파일러는 포인터가 매달려 있지 않도록 가비지 수집 힙에 변수를 할당해야합니다.



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 () 이 호출되지 않을 수 있습니다. 아무 의미가 없기 때문입니다.

변수가 힙 또는 스택에 할당되는지 여부는 신경 쓰지 말고 사용하십시오. 필요한 경우 뮤텍스 또는 채널로 보호하십시오.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.