Go의 컴파일 된 실행 파일 크기가 큰 이유


90

내 리눅스 컴퓨터에서 기본 실행 파일을 생성하는 hello world Go 프로그램을 준수했습니다. 하지만 간단한 Hello world Go 프로그램의 크기를보고 놀랐습니다. 1.9MB였습니다!

Go에서 이러한 간단한 프로그램의 실행 파일이 그렇게 큰 이유는 무엇입니까?


22
거대한? 나는 당신이 자바를 많이하지 않는 것 같아요!
Rick-777

19
글쎄, C / C ++ 배경의 메신저!
Karthic Rao

난 방금이 scala-native hello world를 시도 해봤습니다 : scala-native.org/en/latest/user/sbt.html#minimal-sbt-project 컴파일하고 많은 것을 다운로드하는데 꽤 시간이 걸렸고 바이너리는 3.9입니다. MB.
bli

2019 년 결과로 아래 답변 을 업데이트했습니다 .
VonC

1
C # .NET Core 3.1의 간단한 Hello World 앱은 dotnet publish -r win-x64 -p:publishsinglefile=true -p:publishreadytorun=true -p:publishtrimmed=true약 26MB의 바이너리 파일 을 생성합니다!
Jalal

답변:


90

이 정확한 질문은 공식 FAQ에 나와 있습니다. 내 사소한 프로그램이 왜 그렇게 큰 바이너리입니까?

답변 인용 :

GC의 툴 체인 (의 링커 5l, 6l8l) 정적 링크 할. 따라서 모든 Go 바이너리에는 동적 유형 검사, 리플렉션 및 패닉 타임 스택 추적을 지원하는 데 필요한 런타임 유형 정보와 함께 Go 런타임이 포함됩니다.

Linux에서 gcc를 사용하여 정적으로 컴파일 및 링크 된 간단한 C "hello, world"프로그램은 printf. 사용하는 동등한 Go 프로그램 fmt.Printf은 약 1.9MB이지만 더 강력한 런타임 지원 및 유형 정보를 포함합니다.

따라서 Hello World의 기본 실행 파일은 가비지 수집, 리플렉션 및 기타 많은 기능을 제공하는 런타임이 포함되어 있기 때문에 1.9MB입니다 (프로그램에서 실제로 사용하지는 않지만 거기에 있음). 그리고 텍스트 fmt를 인쇄하는 데 사용한 패키지 구현 "Hello World"(및 해당 종속성).

이제 다음을 시도 fmt.Println("Hello World! Again")하십시오. 프로그램에 다른 줄을 추가 하고 다시 컴파일하십시오. 결과는 2x 1.9MB가 아니지만 여전히 1.9MB입니다! 예, 사용 된 모든 라이브러리 ( fmt및 해당 종속성) 및 런타임이 이미 실행 파일에 추가되었으므로 방금 추가 한 두 번째 텍스트를 인쇄하기 위해 몇 바이트 만 더 추가됩니다.


10
glibc와 정적으로 링크 된 AC "hello world"프로그램은 750K입니다. glibc는 명시 적으로 정적 링크 용으로 설계되지 않았고 경우에 따라 적절한 정적 링크가 불가능하기 때문입니다. musl libc와 정적으로 연결된 "hello world"프로그램은 14K입니다.
Craig Barnes

나는 여전히 찾고 있지만 공격자가 악의적 인 코드로 링크되지 않도록 링크 된 내용을 아는 것이 좋을 것입니다.
Richard

그렇다면 모든 Go exe 파일간에 공유 할 수 있도록 Go 런타임 라이브러리가 DLL 파일에없는 이유는 무엇입니까? 그러면 "hello world"프로그램은 예상대로 2MB가 아닌 몇 KB가 될 수 있습니다. 모든 프로그램에 전체 런타임 라이브러리가 있다는 것은 Windows에서 MSVC를 대체 할 수있는 훌륭한 대안이 될 수있는 치명적인 결함입니다.
David Spector

Go가 "정적으로 연결되어있다"는 내 의견에 대해 이의를 제기하는 것이 좋습니다. 좋아, DLL이 없습니다. 그러나 정적 연결은 전체 라이브러리를 연결 (바인딩)해야한다는 의미가 아니라 실제로 라이브러리에서 사용되는 함수 만 연결해야 함을 의미합니다!
David Spector

43

다음 프로그램을 고려하십시오.

package main

import "fmt"

func main() {
    fmt.Println("Hello World!")
}

Linux AMD64 시스템 (Go 1.9)에서 다음과 같이 빌드하면 :

$ go build
$ ls -la helloworld
-rwxr-xr-x 1 janf group 2029206 Sep 11 16:58 helloworld

크기가 약 2MB 인 바이너리를 얻습니다.

그 이유는 (다른 답변에서 설명했듯이) 우리가 상당히 큰 "fmt"패키지를 사용하고 있지만 바이너리도 제거되지 않았으며 이는 심볼 테이블이 여전히 존재한다는 것을 의미합니다. 대신 컴파일러에게 바이너리를 제거하도록 지시하면 훨씬 작아집니다.

$ go build -ldflags "-s -w"
$ ls -la helloworld
-rwxr-xr-x 1 janf group 1323616 Sep 11 17:01 helloworld

그러나 다음과 같이 fmt.Println 대신 내장 함수 print를 사용하도록 프로그램을 다시 작성하면 :

package main

func main() {
    print("Hello World!\n")
}

그런 다음 컴파일하십시오.

$ go build -ldflags "-s -w"
$ ls -la helloworld
-rwxr-xr-x 1 janf group 714176 Sep 11 17:06 helloworld

우리는 더 작은 바이너리로 끝납니다. 이것은 UPX 패킹과 같은 트릭을 사용하지 않고도 얻을 수있는만큼 작기 때문에 Go 런타임의 오버 헤드는 대략 700Kb입니다.


3
UPX는 바이너리를 압축하고 실행시 즉시 압축을 해제합니다. 어떤 시나리오에서 유용 할 수 있기 때문에 그것이하는 일을 설명하지 않고는 속임수를 무시하지는 않을 것입니다. 바이너리 크기는 시작 시간과 RAM 사용량을 희생시키면서 약간 줄어 듭니다. 또한 성능도 약간의 영향을받을 수 있습니다. 예를 들어 실행 파일을 (스트립 된) 크기의 30 %로 축소하고 실행하는 데 35ms가 더 오래 걸릴 수 있습니다.
simlev

10

바이너리 크기 문제는 golang / go 프로젝트문제 6853 으로 추적됩니다 .

예를 들어, a26c01a (Go 1.4 용)를 커밋하여 hello world를 70kB로 잘라냅니다 .

그 이름을 기호 테이블에 쓰지 않기 때문입니다.

1.5 용 컴파일러, 어셈블러, 링커 및 런타임이 모두 Go에 포함된다는 점을 고려하면 추가 최적화를 기대할 수 있습니다.


2016 Go 1.7 업데이트 : 최적화되었습니다. " Smaller Go 1.7 바이너리 "를 참조하십시오 .

하지만 오늘날 (2019 년 4 월) 가장 많이 차지하는 것은 runtime.pclntab. Raphael 'kena'Poss의 " Go 실행 파일이 왜 그렇게 큰가요? D3를 사용하는 Go 실행 파일의 크기 시각화
"를 참조하십시오 .

문서화가 잘되어 있지는 않지만 Go 소스 코드의이 주석은 그 목적을 암시합니다.

// A LineTable is a data structure mapping program counters to line numbers.

이 데이터 구조의 목적은 Go 런타임 시스템이 충돌시 또는 runtime.GetStackAPI 를 통한 내부 요청시 설명 스택 추적을 생성 할 수 있도록하는 것 입니다.

그래서 유용 해 보입니다. 그런데 왜 그렇게 큽니까?

앞서 링크 된 소스 파일에 숨겨진 URL https://golang.org/s/go12symtab 은 Go 1.0과 1.2 사이에 발생한 일을 설명하는 문서로 리디렉션됩니다. 의역하기 :

1.2 이전에는 Go 링커가 압축 된 라인 테이블을 내 보냈고 프로그램은 런타임에 초기화시 압축을 풀었습니다.

Go 1.2에서는 실행 파일의 라인 테이블을 추가 압축 해제 단계없이 런타임에 직접 사용하기에 적합한 최종 형식으로 미리 확장하기로 결정했습니다.

즉, Go 팀은 초기화 시간을 절약하기 위해 실행 파일을 더 크게 만들기로 결정했습니다.

또한 데이터 구조를 살펴보면 컴파일 된 바이너리의 전체 크기가 각 함수의 크기와 함께 프로그램의 함수 수에서 매우 선형적인 것으로 보입니다.

https://science.raphael.poss.name/go-executable-size-visualization-with-d3/size-demo-ss.png


2
나는 그가 구현 언어가 그것과 어떤 관련이 있는지 알지 못합니다. 공유 라이브러리를 사용해야합니다. 그들이이 시대에 아직하지 않았다는 것은 다소 놀랍습니다.
Marquis of Lorne

2
@EJP : 공유 라이브러리를 사용해야하는 이유는 무엇입니까?
Flimzy

9
@EJP, Go의 단순성 중 일부는 공유 라이브러리를 사용하지 않는 것입니다. 사실 Go는 종속성이 전혀 없으며 일반 시스템 호출을 사용합니다. 단일 바이너리를 배포하기 만하면 작동합니다. 그렇지 않으면 언어와 생태계를 크게 손상시킬 것입니다.
creker

10
정적으로 연결된 바이너리의 경우 자주 잊혀진 부분은 완전히 비어있는 Docker-container에서 바이너리를 실행할 수 있다는 것입니다. 보안 관점에서 이것은 이상적입니다. 컨테이너가 비어 있으면 침입 할 수 있지만 (정적으로 연결된 바이너리에 결함이있는 경우) 컨테이너에서 찾을 수있는 것이 없으므로 공격이 중지됩니다.
Joppe
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.