맵, 슬라이스 및 Go 가비지 컬렉터



이 기사에서는 가비지 수집기 작동에 주의해야 하는 이유를 보여주는 몇 가지 예를 제시할 것입니다. 이 기사의 요점은 특히 매우 많은 양의 포인터를 처리할 때 포인터를 저장하는 방식이 가비지 수집기의 성능에 큰 영향을 미친다는 점을 이해하는 것입니다.

제시된 예제는 모두 기본 Go 데이터 유형인 포인터, 슬라이스 및 맵을 사용합니다.

가비지 수집이란 무엇입니까?



가비지 수집은 사용되지 않는 메모리 공간을 확보하는 프로세스입니다. 즉, 가비지 수집기는 어떤 개체가 범위를 벗어났고 더 이상 참조할 수 없는지 확인하고 사용하는 메모리 공간을 해제합니다. 이 프로세스는 프로그램 실행 전이나 후에가 아니라 Go 프로그램이 실행되는 동안 동시에 발생합니다. Go 가비지 수집 문서에 따르면:

“The GC runs concurrently with mutator threads, is type accurate(also known as precise). allows multiple GC threads to run in parallel. It is a concurrent mark and sweep that uses a write barrier. It is non-generational and non-compacting. Allocation is done using size segregated per P allocation areas to minimize fragmentation while eliminating locks in the common case.“



슬라이스 사용



이 예에서는 슬라이스를 사용하여 많은 양의 구조를 저장합니다. 각 구조는 두 개의 정수 값을 저장합니다. 아래 언급된 Go 코드를 따르십시오.

package main

import "runtime"

type data struct{
  i,j int  
}

func main() {
  var N = 40000000
  var str []data

  for i:=0;i<N;i++ {
    value := i
    str = append(str, data{value,value})
  }

  runtime.GC()
  _ = str[0]
}


마지막 문(_ = str[0])은 str 변수가 for 루프 외부에서 참조되거나 사용되지 않기 때문에 가비지 수집기가 너무 일찍 str 변수를 가비지 수집하는 것을 방지하는 데 사용됩니다. 다음 세 가지 Go 프로그램에서도 동일한 기술이 사용됩니다. 이 중요한 세부 사항 외에도 for 루프는 슬라이스에 저장된 구조에 값을 넣는 데 사용됩니다.

포인터가 있는 지도 사용



여기서는 모든 포인터를 정수로 저장하기 위해 맵을 사용할 것입니다. 이 프로그램에는 다음 Go 코드가 포함되어 있습니다.

package main

import "runtime"

func main() {
  var N = 40000000
  myMap := make(map[int]*int)

  for i:=0;i<N;i++ {
    value := i
    myMap[value] = &value
  }

  runtime.GC()
  _ = myMap[0]
}


정수 포인터를 저장하는 맵의 이름은 myMap입니다. for 루프는 정수 값을 맵에 넣는 데 사용됩니다.

포인터 없이 지도 사용



여기서는 포인터 없이 일반 값을 저장하는 맵을 사용할 것입니다. Go 코드는 다음과 같습니다.

package main

import "runtime"

func main() {
  var N = 40000000
  myMap := make(map[int]int)

  for i:=0;i<N;i++ {
    value := i
    myMap[value] = value
  }

  runtime.GC()
  _ = myMap[0]
}


이전과 마찬가지로 for 루프는 정수 값을 맵에 넣는 데 사용됩니다.

지도 분할



이 섹션의 구현은 맵을 샤딩이라고도 하는 맵의 맵으로 분할합니다. 이 섹션의 프로그램에는 다음 Go 코드가 포함되어 있습니다.

package main

import "runtime"

func main() {
  var N = 40000000
  split := make([]map[int]int,200)

  for i := range split{
    split[i] = make(map[int]int)
  }
  for i:=0;i<N;i++ {
    value := i
    split[i%200][value] = value
  }

  runtime.GC()
  _ = split[0][0]
}


이번에는 두 개의 for 루프를 사용합니다. 하나는 해시 해시를 생성하기 위한 for 루프이고 다른 하나는 해시 해시에 원하는 데이터를 저장하기 위한 것입니다.

제시된 기술의 성능 비교



4개의 프로그램 모두 거대한 데이터 구조를 사용하기 때문에 많은 양의 메모리를 소비하고 있습니다. 많은 메모리 공간을 사용하는 프로그램은 Go 가비지 수집기를 더 자주 트리거합니다. 따라서 이 섹션에서는 time(1) 명령을 사용하여 이러한 네 가지 구현 각각의 성능을 비교할 것입니다.

이 제시된 출력에서 ​​중요한 것은 정확한 숫자가 아니라 네 가지 접근 방식 간의 시간 차이입니다. 시작합니다:

$ time go run 1_sliceGC.go

real    0m1.511s
user    0m0.000s
sys     0m0.015s


$ time go run 2_mapStar.go

real    0m10.395s
user    0m0.000s
sys     0m0.015s


$ time go run 3_mapNoStar.go

real    0m8.227s
user    0m0.000s
sys     0m0.015s


$ time go run 4_mapSplit.go

real    0m8.028s
user    0m0.000s
sys     0m0.015s


따라서 맵은 Go 가비지 컬렉터의 속도를 늦추는 반면 슬라이스는 Go 가비지 컬렉터와 훨씬 더 잘 협력합니다. 이것은 맵의 문제가 아니라 Go 가비지 수집기 작동 방식의 결과라는 점에 유의해야 합니다. 그러나 막대한 양의 데이터를 저장하는 맵을 처리하지 않는 한 이 문제는 프로그램에서 분명해지지 않습니다.

my blog

읽어 주셔서 감사합니다 :)

좋은 웹페이지 즐겨찾기