Go 포인터의 용도

18113 단어 Gotech
이 글은 Go 2 Advent Calendar 2020 22일째 글입니다.
Go에는 포인터의 개념이 있습니다.
포인터를 통해 변수를 전달하면 값을 복사하지 않고 주소만 전달하면 되기 때문에 효율적이다. 그러나 고라면 포인터를 너무 많이 사용하면 낙타 컬렉션에 부담을 주고 CPU 시간을 많이 소모할 수 있다.
예를 들어 다음과 같은 S 구조체를 준비하고 복제한 후 되돌아오는 함수, 바늘로 되돌아오는 함수
type S struct {
  a, b, c int64
  d, e, f string
  g, h, i float64
}

func byCopy() S {
  return S{
    a: 1, b: 1, c: 1,
    e: "foo", f: "foo",
    g: 1.0, h: 1.0, i: 1.0,
  }
}

func byPointer() *S {
  return &S{
    a: 1, b: 1, c: 1,
    e: "foo", f: "foo",
    g: 1.0, h: 1.0, i: 1.0,
  }
}
준비된 함수를 다음 코드에 적어 기준을 얻는다.
func BenchmarkStack(b *testing.B) {
  var s S

  f, err := os.Create("stack.out")
  if err != nil {
          panic(err)
  }
  defer f.Close()

  err = trace.Start(f)
  if err != nil {
          panic(err)
  }

  for i := 0; i < b.N; i++ {
          s = byCopy()
  }

  trace.Stop()

  b.StopTimer()

  _ = fmt.Sprintf("%v", s.a)
}

func BenchmarkMemoryHeap(b *testing.B) {
  var s *S

  f, err := os.Create("heap.out")
  if err != nil {
          panic(err)
  }
  defer f.Close()

  err = trace.Start(f)
  if err != nil {
          panic(err)
  }

  for i := 0; i < b.N; i++ {
          s = byPointer()
  }

  trace.Stop()

  b.StopTimer()

  _ = fmt.Sprintf("%v", s.a)
}
다음 결과를 얻을 수 있습니다(goversion go 1.15.6 linux/amd64)
복제 함수 결과 향상
BenchmarkStack                127888153              9.41 ns/op              0 B/op          0 allocs/op
BenchmarkMemoryHeap           16394270              68.2 ns/op              96 B/op          1 allocs/op

  • Go: Should I Use a Pointer instead of a Copy of my Struct?(기준 인용원)
  • Go의 GC 지출이 높아지는 상황과 회피 전략
  • 사용처


    그러면 어떤 자리에서 지침을 사용해야 하나요?

    함수 내에서 매개 변수와 수신기를 고쳐야 하는 상황


    함수에서 매개 변수와 수신기를 다시 써야 한다면, 다시 쓰는 대상은 바늘로 전달해야 한다
    type S struct { value string }
    
    func (s S) SetA (v string) {
      s.value = v
    }
    
    func (s *S) SetB (v string) {
      s.value = v
    }
    
    func main() {
      var s S
      s.SetA("a")
      fmt.Println(s.value) // sはゼロ値のまま
      s.SetB("b")
      fmt.Println(s.value) // b
    }
    
    goo play ground로 해볼게요.
    반면 함수 내 수신기에 변경이 없는 함수는 값 수신기를 사용하는 측이 서명만으로'이 함수는 수신기에 변경이 추가되지 않는다'고 명확하게 표시하기 때문에 가독성 측면에서도 우수하다
  • Effective Go (Pointers vs Values)
  • Uber Go Style Guide 일본어 번역(Receivers and Interfaces)
  • 복사를 피할 데이터를 매개 변수, 수신기로 설정할 때

    os.File 또는 sync.Mutex 등 복사본이 발생한 후 문제가 발생할 수 있는 구조는 지침으로 처리하지 마십시오.
    // os.Open では File 型をポインタで返している
    func Open(name string) (file *File, err error) {
      return OpenFile(name, O_RDONLY, 0)
    }
    
  • Go Code Review Comments 일본어 번역(Receiver Type)
  • Design Philosophy On Data And Semantics
  • 큰 구조나 배열을 처리할 때


    큰 구조와 배열을 처리할 때 바늘을 사용하는 것이 더욱 효율적이다
    반면intstring 같은 원시적인 유형과 필드가 많지 않은 구조체에 대해서는 복사 비용에 너무 신경 쓰지 말고 지침으로 처리하면 GC의 호환성과 효율성이 떨어지기 때문에 값 수신기를 사용하는 것이 좋다.
    "Go Code Review Comments의""큰 음식""기준은 다음과 같습니다."
    만약 구조체의 모든 값이 매개 변수에 전달된다고 가정한다면.너무 많이 느껴지면 바늘로 쓸 수 있는 크기다.
    어려운 기준이지만 편의성, 변경 용이성, GC의 부하 등을 고려해 결정한다
    망설일 때 수신기를 바늘 위에 놓아라.
    이런 기술도 있는데, 만약 주저하면 바늘을 사용하는 것이 좋다
    Go Code Review Comments 일본어 번역(Receiver Type)

    큰 구조체를 절편에 유지하는 상황


    슬라이스가 cap 이상append에 도달하거나 for ~ range에서 슬라이스 요소를 얻었을 때 모든 기록된 복사본이 발생하기 때문에 슬라이스 요소가 비교적 큰 구조체를 가지고 있다면 지침에 놓으면 복제 원가를 억제할 수 있다
    // listのcapが足りない場合新しくアロケートされる
    list = append(list, x)
    
    // xにlistの要素がコピーされる
    for _, x := range list {
        ...
    }
    
  • 구글의 매개 변수, 되돌아오는 값, 수신기가 바늘인지 값인지 판단하는 기준에 현혹되다
  • replace []Issue with []*Issue
  • 총결산


    수치와 지침의 구분 기준이 내 마음속에 조금 명확해졌다
    이렇게 보면 지침의 활약 위치는 상당히 제한되어 있다
    이번에 GC의 부하에 대해 깊이 접촉하지 못했는데, 그 부근의 @imoty 보도는 매우 이해하기 쉽다
    Go의 GC 지출이 높아지는 상황과 회피 전략
    지침을 잘 사용해서 독자가 쉽게 뜻을 이해할 수 있는 보수성이 높은 코드를 써라

    좋은 웹페이지 즐겨찾기