เหตุผลสำหรับขนาดใหญ่ของไฟล์ปฏิบัติการที่รวบรวมได้ของ Go


91

ฉันปฏิบัติตามโปรแกรม hello world Go ซึ่งสร้างไฟล์ปฏิบัติการบนเครื่อง linux ของฉัน แต่ฉันประหลาดใจเมื่อเห็นขนาดของโปรแกรม Hello world Go แบบธรรมดามันคือ 1.9MB!

เหตุใดการเรียกใช้งานโปรแกรมง่ายๆใน Go จึงมีขนาดใหญ่มาก


22
มหาศาล? ฉันเดาว่าคุณไม่ได้ทำ Java มากนัก!
Rick-777

20
ฉันมาจากพื้นหลัง C / C ++!
Karthic Rao

ฉันเพิ่งลองสวัสดีชาวโลกแบบสกาลานี้ : scala-native.org/en/latest/user/sbt.html#minimal-sbt-projectใช้เวลาพอสมควรในการรวบรวมดาวน์โหลดสิ่งต่างๆมากมายและไบนารีคือ 3.9 ลบ.
bli

ฉันได้อัปเดตคำตอบของฉันด้านล่างด้วยผลการวิจัยในปี 2019
VonC

1
แอพ Simple Hello World ใน C # .NET Core 3.1 พร้อมdotnet publish -r win-x64 -p:publishsinglefile=true -p:publishreadytorun=true -p:publishtrimmed=trueสร้างไฟล์ไบนารีประมาณ ~ 26MB!
Jalal

คำตอบ:


91

คำถามที่แน่นอนนี้ปรากฏในคำถามที่พบบ่อยอย่างเป็นทางการ: เหตุใดโปรแกรมเล็กน้อยของฉันจึงเป็นไบนารีขนาดใหญ่

อ้างคำตอบ:

Linkers ในห่วงโซ่เครื่องมือประชาคมโลก ( 5l, 6lและ8l) ทำคงที่เชื่อมโยง ดังนั้นไบนารีของ Go ทั้งหมดจึงรวม Go run-time พร้อมกับข้อมูลประเภทรันไทม์ที่จำเป็นเพื่อรองรับการตรวจสอบประเภทไดนามิกการสะท้อนกลับและแม้แต่สแต็กเทรซเวลาตื่นตระหนก

โปรแกรม C "สวัสดีชาวโลก" ที่เรียบง่ายที่คอมไพล์และเชื่อมโยงแบบคงที่โดยใช้ gcc บน Linux นั้นอยู่ที่ประมาณ 750 kB รวมถึงการใช้งานprintf. โปรแกรม Go ที่เทียบเท่าโดยใช้fmt.Printfมีขนาดประมาณ 1.9 MB แต่รวมถึงการสนับสนุนรันไทม์ที่มีประสิทธิภาพมากขึ้นและข้อมูลประเภท

ดังนั้นไฟล์ปฏิบัติการดั้งเดิมของ Hello World ของคุณคือ 1.9 MB เนื่องจากมีรันไทม์ที่ให้การรวบรวมขยะการสะท้อนและคุณสมบัติอื่น ๆ อีกมากมาย (ซึ่งโปรแกรมของคุณอาจไม่ได้ใช้จริงๆ แต่อยู่ที่นั่น) และการใช้งานfmtแพคเกจที่คุณใช้ในการพิมพ์"Hello World"ข้อความ (รวมถึงการอ้างอิง)

ลองทำสิ่งต่อไปนี้: เพิ่มอีกfmt.Println("Hello World! Again")บรรทัดในโปรแกรมของคุณแล้วคอมไพล์อีกครั้ง ผลลัพธ์จะไม่ใช่ 2x 1.9MB แต่ยังคงเป็นเพียง 1.9 MB! ใช่เนื่องจากไลบรารีที่ใช้ทั้งหมด ( fmtและการอ้างอิง) และรันไทม์ถูกเพิ่มลงในไฟล์ปฏิบัติการแล้ว (และจะเพิ่มไบต์อีกเพียงไม่กี่ไบต์เพื่อพิมพ์ข้อความที่ 2 ที่คุณเพิ่งเพิ่มเข้าไป)


11
โปรแกรม AC "hello world" ที่เชื่อมโยงกับ glibc แบบคงที่คือ 750K เนื่องจาก glibc ไม่ได้ออกแบบมาสำหรับการลิงก์แบบคงที่และเป็นไปไม่ได้ที่จะลิงก์แบบคงที่อย่างถูกต้องในบางกรณี โปรแกรม "hello world" ที่เชื่อมโยงกับ musl libc แบบคงที่คือ 14K
Craig Barnes

อย่างไรก็ตามฉันยังคงมองหาอยู่คงจะดีถ้ารู้ว่ามีอะไรเชื่อมโยงอยู่เพื่อที่ว่าผู้โจมตีอาจไม่ได้เชื่อมโยงในโค้ดชั่วร้าย
Richard

เหตุใดไลบรารีรันไทม์ของ Go จึงไม่อยู่ในไฟล์ DLL เพื่อให้สามารถแชร์กับไฟล์ Go exe ทั้งหมดได้? จากนั้นโปรแกรม "hello world" อาจมีขนาดไม่กี่ KB ตามที่คาดไว้แทนที่จะเป็น 2 MB การมีไลบรารีรันไทม์ทั้งหมดในทุกโปรแกรมถือเป็นข้อบกพร่องร้ายแรงสำหรับทางเลือกที่ยอดเยี่ยมสำหรับ MSVC บน Windows
David Spector

ฉันควรคาดหวังว่าจะมีการคัดค้านความคิดเห็นของฉัน: Go นั้น "ลิงก์แบบคงที่" โอเคไม่มี DLL แล้ว แต่การลิงก์แบบคงที่ไม่ได้หมายความว่าคุณต้องเชื่อมโยง (ผูก) ไลบรารีทั้งหมด แต่เป็นฟังก์ชันที่ใช้จริงในไลบรารีเท่านั้น!
David Spector

44

พิจารณาโปรแกรมต่อไปนี้:

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

ฉันได้รับไบนารีที่มีขนาดประมาณ 2 Mb

เหตุผลนี้ (ซึ่งได้อธิบายไว้ในคำตอบอื่น ๆ ) คือเราใช้แพ็คเกจ "fmt" ซึ่งมีขนาดค่อนข้างใหญ่ แต่ไบนารียังไม่ถูกถอดออกและนั่นหมายความว่าตารางสัญลักษณ์ยังคงอยู่ที่นั่น หากเราสั่งให้คอมไพเลอร์ถอดไบนารีแทนมันจะเล็กลงมาก:

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

อย่างไรก็ตามหากเราเขียนโปรแกรมใหม่เพื่อใช้ฟังก์ชัน builtin พิมพ์แทน fmt.Println เช่นนี้:

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-runtime จึงอยู่ที่ประมาณ 700 Kb


4
UPX บีบอัดไบนารีและคลายการบีบอัดแบบทันทีเมื่อดำเนินการ ฉันจะไม่ยกเลิกกลเม็ดนี้โดยไม่อธิบายว่ามันทำอะไรเพราะมันมีประโยชน์ในบางสถานการณ์ ขนาดไบนารีจะลดลงบ้างตามเวลาเริ่มต้นและการใช้ RAM นอกจากนี้ประสิทธิภาพอาจได้รับผลกระทบเล็กน้อยเช่นกัน เช่นเดียวกับตัวอย่างไฟล์ปฏิบัติการสามารถลดขนาดลงเหลือ 30% ของขนาด (stripped) และใช้เวลานานขึ้น 35ms ในการรัน
simlev

10

หมายเหตุว่าปัญหาที่ขนาดไบนารีติดตามโดยปัญหา 6853ในgolang / โครงการไป

ตัวอย่างเช่นกระทำ a26c01a (สำหรับ Go 1.4) ตัด hello world 70kB :

เพราะเราไม่ได้เขียนชื่อเหล่านั้นลงในตารางสัญลักษณ์

เมื่อพิจารณาจากคอมไพเลอร์แอสเซมเบลอร์ตัวเชื่อมโยงและรันไทม์สำหรับ 1.5 ทั้งหมดจะอยู่ใน Go คุณสามารถคาดหวังการเพิ่มประสิทธิภาพเพิ่มเติมได้


อัปเดต 2016 Go 1.7: ได้รับการปรับให้เหมาะสม: โปรดดู " ไบนารี Smaller Go 1.7 "

แต่เหล่านี้วัน (เมษายน 2019) runtime.pclntabสิ่งที่ใช้สถานที่มากที่สุดคือ
โปรดดูที่ " ทำไมไปไฟล์ปฏิบัติการของฉันเพื่อสร้างภาพขนาดใหญ่? ขนาดของ executables ไปใช้ D3 " จากราฟาเอล 'kena' แคลอรี่

ไม่ได้รับการจัดทำเป็นเอกสารที่ดีเกินไปอย่างไรก็ตามความคิดเห็นนี้จากซอร์สโค้ด 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
ฉันไม่เห็นว่าภาษาการใช้งานเขาเกี่ยวข้องกับมันอย่างไร พวกเขาจำเป็นต้องใช้ไลบรารีที่ใช้ร่วมกัน ค่อนข้างเหลือเชื่อที่พวกเขาไม่ได้อยู่ในยุคนี้แล้ว
user207421

3
@EJP: ทำไมพวกเขาต้องใช้ไลบรารีที่ใช้ร่วมกัน?
Flimzy

10
@EJP ส่วนหนึ่งของความเรียบง่ายของ Go คือการไม่ใช้ไลบรารีที่แชร์ ในความเป็นจริง Go ไม่มีการอ้างอิงใด ๆ เลย แต่ใช้ syscalls ธรรมดา เพียงปรับใช้ไบนารีเดียวและใช้งานได้จริง มันจะส่งผลเสียต่อภาษาและระบบนิเวศน์อย่างมากหากเป็นอย่างอื่น
creker

11
สิ่งที่มักถูกลืมในการมีไบนารีที่เชื่อมโยงแบบคงที่คือทำให้สามารถเรียกใช้งานได้ใน Docker-container ที่ว่างเปล่าโดยสิ้นเชิง จากมุมมองด้านความปลอดภัยสิ่งนี้เหมาะอย่างยิ่ง เมื่อคอนเทนเนอร์ว่างเปล่าคุณอาจบุกเข้าไปได้ (หากไบนารีที่เชื่อมโยงแบบสแตติกมีข้อบกพร่อง) แต่เนื่องจากไม่พบสิ่งใดในคอนเทนเนอร์การโจมตีจึงหยุดลงที่นั่น
Joppe
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.