Làm thế nào để biên dịch nhanh như vậy?


216

Tôi đã Googled và chọc quanh trang web của Go, nhưng dường như tôi không thể tìm thấy lời giải thích cho thời gian xây dựng phi thường của Go. Chúng có phải là sản phẩm của các tính năng ngôn ngữ (hoặc thiếu chúng), trình biên dịch được tối ưu hóa cao hay thứ gì khác không? Tôi không cố gắng quảng bá Go; Tôi chỉ tò mò thôi.


12
@ Hỗ trợ, tôi biết điều đó. Tôi nghĩ rằng việc thực hiện một trình biên dịch theo cách mà nó biên dịch với sự nhanh chóng đáng chú ý là bất cứ điều gì ngoài việc tối ưu hóa sớm. Nhiều khả năng, nó đại diện cho kết quả của thực tiễn thiết kế và phát triển phần mềm tốt. Ngoài ra, tôi không thể chịu được khi thấy những lời của Knuth được đưa ra khỏi ngữ cảnh và áp dụng không chính xác.
Adam Crossland

55
Phiên bản bi quan của câu hỏi này là "Tại sao C ++ biên dịch chậm như vậy?" stackoverflow.com/questions/588884/ từ
dan04

14
Tôi đã bỏ phiếu để mở lại câu hỏi này vì nó không dựa trên ý kiến. Người ta có thể đưa ra một cái nhìn tổng quan về kỹ thuật (không có ý kiến) về ngôn ngữ và / hoặc các lựa chọn trình biên dịch mà tốc độ biên dịch của cơ sở.
Martin Tournoij

Đối với các dự án nhỏ, Go dường như chậm với tôi. Điều này là do tôi nhớ rằng Turbo-Pascal nhanh hơn rất nhiều trên một máy tính có thể chậm hơn hàng ngàn lần. prog21.dadgum.com/47.html?repost=true . Mỗi lần tôi gõ "go build" và không có gì xảy ra trong vài giây, tôi nghĩ lại về các trình biên dịch Fortran cũ nát và các thẻ đục lỗ. YMMV. TLDR: "chậm" và "nhanh" là các thuật ngữ tương đối.
RedGrittyBrick

Chắc chắn khuyên bạn nên đọc dave.cheney.net/2014/06/07/five-things-that-make-go-fast để biết thêm thông tin chi tiết
Karthik

Câu trả lời:


192

Phân tích phụ thuộc.

Câu hỏi thường gặp về Go được sử dụng để chứa câu sau:

Go cung cấp một mô hình để xây dựng phần mềm giúp cho việc phân tích phụ thuộc trở nên dễ dàng và tránh được phần lớn chi phí của kiểu C bao gồm các tệp và thư viện.

Mặc dù cụm từ này không còn trong FAQ nữa, chủ đề này được xây dựng trong bài nói chuyện tại Google , so sánh cách tiếp cận analysyis phụ thuộc của C / C ++ và Go.

Đó là lý do chính của việc biên dịch nhanh. Và đây là do thiết kế.


Cụm từ này không còn trong Câu hỏi thường gặp nữa, nhưng giải thích chi tiết hơn về chủ đề "phân tích phụ thuộc" so sánh cách tiếp cận C / C ++ và Pascal / Modula / Go có sẵn trong bài nói chuyện tại Google
rob74

76

Tôi nghĩ rằng không phải trình biên dịch Go là nhanh , mà là các trình biên dịch khác chậm .

Trình biên dịch C và C ++ phải phân tích số lượng tiêu đề khổng lồ - ví dụ, biên dịch C ++ "hello world" yêu cầu biên dịch các dòng mã 18k, gần một nửa megabyte nguồn!

$ cpp hello.cpp | wc
  18364   40513  433334

Trình biên dịch Java và C # chạy trong VM, có nghĩa là trước khi chúng có thể biên dịch bất cứ thứ gì, hệ điều hành phải tải toàn bộ VM, sau đó chúng phải được biên dịch JIT từ mã byte sang mã gốc, tất cả đều mất một thời gian.

Tốc độ biên dịch phụ thuộc vào một số yếu tố.

Một số ngôn ngữ được thiết kế để được biên dịch nhanh. Ví dụ, Pascal được thiết kế để được biên dịch bằng trình biên dịch một lượt.

Trình biên dịch có thể được tối ưu hóa quá. Ví dụ, trình biên dịch Turbo Pascal được viết bằng trình biên dịch được tối ưu hóa bằng tay, kết hợp với thiết kế ngôn ngữ, dẫn đến một trình biên dịch thực sự nhanh làm việc trên phần cứng lớp 286. Tôi nghĩ rằng ngay cả bây giờ, trình biên dịch Pascal hiện đại (ví dụ FreePascal) nhanh hơn trình biên dịch Go.


19
Trình biên dịch C # của Microsoft không chạy trong VM. Nó vẫn được viết bằng C ++, chủ yếu vì lý do hiệu suất.
blucz

19
Turbo Pascal và Delphi sau này là những ví dụ tốt nhất cho trình biên dịch nhanh chóng. Sau khi kiến ​​trúc sư của cả hai đã chuyển sang Microsoft, chúng tôi đã thấy những cải tiến lớn về cả trình biên dịch MS và ngôn ngữ. Đó không phải là sự trùng hợp ngẫu nhiên.
TheBlastOne

7
Mã 18k (chính xác là 18364) mã là 433334 byte (~ 0,5MB)
el.pescado

9
Trình biên dịch C # đã được biên dịch với C # từ năm 2011. Chỉ cần một bản cập nhật trong trường hợp bất kỳ ai đọc nó sau này.
Kurt Koller

3
Tuy nhiên, trình biên dịch C # và CLR chạy MSIL được tạo ra là những thứ khác nhau. Tôi khá chắc chắn CLR không được viết bằng C #.
vui vẻ

39

Có nhiều lý do khiến trình biên dịch Go nhanh hơn nhiều so với hầu hết các trình biên dịch C / C ++:

  • Lý do hàng đầu : Hầu hết các trình biên dịch C / C ++ thể hiện các thiết kế đặc biệt xấu (từ quan điểm tốc độ biên dịch). Ngoài ra, từ góc độ tốc độ biên dịch, một số phần của hệ sinh thái C / C ++ (chẳng hạn như các biên tập viên trong đó các lập trình viên đang viết mã của họ) không được thiết kế với tốc độ biên dịch.

  • Lý do hàng đầu : Tốc độ biên dịch nhanh là một lựa chọn có ý thức trong trình biên dịch Go và cả ngôn ngữ Go

  • Trình biên dịch Go có trình tối ưu hóa đơn giản hơn trình biên dịch C / C ++

  • Không giống như C ++, Go không có mẫu và không có chức năng nội tuyến. Điều này có nghĩa là Go không cần thực hiện bất kỳ khởi tạo mẫu hoặc chức năng nào.

  • Trình biên dịch Go tạo mã lắp ráp mức thấp sớm hơn và trình tối ưu hóa hoạt động trên mã lắp ráp, trong khi trong trình biên dịch C / C ++ thông thường, tối ưu hóa sẽ hoạt động trên biểu diễn bên trong của mã nguồn gốc. Chi phí hoạt động thêm trong trình biên dịch C / C ++ xuất phát từ thực tế là cần phải tạo biểu diễn bên trong.

  • Liên kết cuối cùng (5l / 6l / 8l) của chương trình Go có thể chậm hơn so với liên kết chương trình C / C ++, vì trình biên dịch Go đang trải qua tất cả các mã lắp ráp đã sử dụng và có thể nó cũng đang thực hiện các hành động bổ sung khác mà C / C ++ trình liên kết không làm

  • Một số trình biên dịch C / C ++ (GCC) tạo hướng dẫn ở dạng văn bản (sẽ được chuyển cho trình biên dịch), trong khi trình biên dịch Go tạo hướng dẫn ở dạng nhị phân. Việc làm thêm (nhưng không nhiều) cần phải được thực hiện để chuyển đổi văn bản thành nhị phân.

  • Trình biên dịch Go chỉ nhắm mục tiêu một số lượng nhỏ kiến ​​trúc CPU, trong khi trình biên dịch GCC nhắm vào một số lượng lớn CPU

  • Trình biên dịch được thiết kế với mục tiêu tốc độ biên dịch cao, chẳng hạn như Jike, rất nhanh. Trên CPU 2GHz, Jike có thể biên dịch hơn 20000 dòng mã Java mỗi giây (và chế độ biên dịch gia tăng thậm chí còn hiệu quả hơn).


17
Trình biên dịch của Go sắp xếp các hàm nhỏ. Tôi không chắc cách nhắm mục tiêu một số lượng nhỏ CPU khiến bạn chậm hơn ... Tôi giả sử gcc không tạo mã PPC trong khi tôi đang biên dịch cho x86.
Brad Fitzpatrick

@BradFitzpatrick ghét phải hồi sinh một bình luận cũ nhưng bằng cách nhắm mục tiêu một số lượng nhỏ hơn các nhà phát triển nền tảng của trình biên dịch có thể dành nhiều thời gian hơn để tối ưu hóa nó cho mỗi bình luận.
Kiên trì

sử dụng một hình thức trung gian cho phép bạn hỗ trợ nhiều kiến ​​trúc hơn vì bây giờ bạn chỉ phải viết một phụ trợ mới cho mỗi kiến ​​trúc mới
phuclv

34

Hiệu quả biên dịch là một mục tiêu thiết kế chính:

Cuối cùng, nó được dự định là nhanh: sẽ mất ít nhất vài giây để xây dựng một tệp thực thi lớn trên một máy tính. Để đáp ứng các mục tiêu này cần phải giải quyết một số vấn đề ngôn ngữ: một hệ thống loại biểu cảm nhưng nhẹ; đồng thời và thu gom rác thải; đặc tả phụ thuộc cứng nhắc; và như thế. Câu hỏi thường gặp

Câu hỏi thường gặp về ngôn ngữ khá thú vị liên quan đến các tính năng ngôn ngữ cụ thể liên quan đến phân tích cú pháp:

Thứ hai, ngôn ngữ đã được thiết kế để dễ dàng phân tích và có thể được phân tích cú pháp mà không cần bảng ký hiệu.


6
Đo không phải sự thật. Bạn không thể phân tích đầy đủ mã nguồn mà không có bảng ký hiệu.

12
Tôi cũng không thấy lý do tại sao bộ sưu tập rác tăng cường thời gian biên dịch. Nó chỉ không.
TheBlastOne

3
Đây là những trích dẫn từ Câu hỏi thường gặp: golang.org/doc/go_faq.html Tôi không thể nói nếu họ không hoàn thành mục tiêu của mình (bảng biểu tượng) hoặc nếu logic của họ bị lỗi (GC).
Larry OBrien

5
@FUZxxl Truy cập golang.org/ref/spec#Primary_expressions và xem xét hai chuỗi [Toán hạng, Gọi] và [Chuyển đổi]. Ví dụ Mã nguồn đi: định danh1 (định danh2). Không có bảng ký hiệu, không thể quyết định xem ví dụ này là cuộc gọi hay chuyển đổi. | Bất kỳ ngôn ngữ nào cũng có thể được phân tích cú pháp ở một mức độ nào đó mà không có bảng ký hiệu. Đúng là hầu hết các phần của mã nguồn Go có thể được phân tích cú pháp mà không có bảng ký hiệu, nhưng không thể nhận ra tất cả các yếu tố ngữ pháp được xác định trong thông số golang.

3
@Atom Bạn làm việc chăm chỉ để ngăn trình phân tích cú pháp trở thành đoạn mã báo lỗi. Các trình phân tích cú pháp thường làm một công việc kém là báo cáo các thông báo lỗi mạch lạc. Ở đây, bạn tạo một cây phân tích cho biểu thức như thể aTypelà một tham chiếu biến, và sau đó trong giai đoạn phân tích ngữ nghĩa khi bạn phát hiện ra nó không phải là bạn in một lỗi có ý nghĩa tại thời điểm đó.
Sam Harwell

26

Trong khi hầu hết những điều trên là đúng, có một điểm rất quan trọng không thực sự được đề cập đến: quản lý phụ thuộc.

Đi chỉ cần bao gồm các gói mà bạn đang nhập trực tiếp (như những gói đã nhập những gì họ cần). Điều này trái ngược hoàn toàn với C / C ++, trong đó mọi tệp đơn bắt đầu bao gồm các tiêu đề x, bao gồm các tiêu đề y, v.v.


22

Một thử nghiệm tốt cho hiệu quả dịch thuật của trình biên dịch là tự biên dịch: mất bao lâu để một trình biên dịch nhất định tự biên dịch? Đối với C ++ thì phải mất một thời gian rất dài (giờ?). Khi so sánh, trình biên dịch Pascal / Modula-2 / Oberon sẽ tự biên dịch trong chưa đầy một giây trên một máy hiện đại [1].

Go đã được truyền cảm hứng bởi các ngôn ngữ này, nhưng một số lý do chính cho hiệu quả này bao gồm:

  1. Một cú pháp được xác định rõ ràng là âm thanh toán học, để quét và phân tích hiệu quả.

  2. Một ngôn ngữ được biên dịch tĩnh và an toàn kiểu sử dụng biên dịch riêng biệt với kiểm tra phụ thuộc và kiểm tra kiểu qua các ranh giới mô-đun, để tránh đọc lại các tệp tiêu đề và biên dịch lại các mô-đun khác - trái ngược với biên dịch độc lập như trong C / C ++ không có kiểm tra mô-đun chéo nào được trình biên dịch thực hiện (do đó cần phải đọc lại tất cả các tệp tiêu đề đó nhiều lần, ngay cả đối với chương trình "hello world" một dòng đơn giản).

  3. Việc triển khai trình biên dịch hiệu quả (ví dụ: phân tích cú pháp từ trên xuống dưới đơn lẻ, đệ quy) - điều này tất nhiên được trợ giúp rất nhiều bởi các điểm 1 và 2 ở trên.

Những nguyên tắc này đã được biết đến và thực hiện đầy đủ vào những năm 1970 và 1980 bằng các ngôn ngữ như Mesa, Ada, Modula-2 / Oberon và một số ngôn ngữ khác, và chỉ bây giờ (trong những năm 2010) mới tìm được ngôn ngữ hiện đại như Go (Google) , Swift (Apple), C # (Microsoft) và một số người khác.

Chúng ta hãy hy vọng rằng điều này sẽ sớm trở thành chuẩn mực và không phải là ngoại lệ. Để đạt được điều đó, hai điều cần phải xảy ra:

  1. Đầu tiên, các nhà cung cấp nền tảng phần mềm như Google, Microsoft và Apple nên bắt đầu bằng cách khuyến khích các nhà phát triển ứng dụng sử dụng phương pháp biên dịch mới, đồng thời cho phép họ sử dụng lại cơ sở mã hiện có của họ. Đây là những gì Apple hiện đang cố gắng thực hiện với ngôn ngữ lập trình Swift, ngôn ngữ có thể cùng tồn tại với Objective-C (vì nó sử dụng cùng một môi trường thời gian chạy).

  2. Thứ hai, bản thân các nền tảng phần mềm cơ bản cuối cùng sẽ được viết lại theo thời gian bằng cách sử dụng các nguyên tắc này, đồng thời thiết kế lại hệ thống phân cấp mô-đun trong quy trình để làm cho chúng ít nguyên khối hơn. Tất nhiên đây là nhiệm vụ của voi ma mút và có thể là phần tốt hơn của một thập kỷ (nếu họ đủ can đảm để thực hiện nó - điều mà tôi không chắc chắn trong trường hợp của Google).

Trong mọi trường hợp, đó là nền tảng thúc đẩy việc áp dụng ngôn ngữ chứ không phải theo cách khác.

Người giới thiệu:

[1] http://www.inf.ethz.ch/personal/wirth/ProjectOberon/PO.System.pdf , trang 6: "Trình biên dịch tự biên dịch trong khoảng 3 giây". Báo giá này dành cho một ban phát triển Xilinx Spartan-3 chi phí thấp chạy ở tần số xung nhịp 25 MHz và có 1 MByte bộ nhớ chính. Từ đó , trình biên dịch có thể dễ dàng ngoại suy thành "dưới 1 giây" cho bộ xử lý hiện đại chạy ở tần số xung nhịp cao hơn 1 GHz và vài GB bộ nhớ chính (tức là mạnh hơn vài bậc so với bo mạch Xilinx Spartan-3), ngay cả khi tính đến tốc độ I / O. Đã trở lại vào năm 1990 khi Oberon được chạy trên bộ xử lý NS32X32 25 MHz với bộ nhớ chính 2-4 MB, trình biên dịch sẽ tự biên dịch chỉ sau vài giây. Khái niệm thực sự chờ đợi hoàn thành một chu trình biên dịch hoàn toàn xa lạ với các lập trình viên Oberon ngay cả khi đó. Đối với các chương trình điển hình, nó luôn luônmất nhiều thời gian hơn để loại bỏ ngón tay khỏi nút chuột đã kích hoạt lệnh biên dịch hơn là chờ trình biên dịch hoàn tất quá trình biên dịch vừa kích hoạt. Đó thực sự là sự hài lòng ngay lập tức, với thời gian chờ đợi gần như bằng không. Và chất lượng của mã được sản xuất, mặc dù không phải lúc nào cũng hoàn toàn ngang bằng với các trình biên dịch tốt nhất hiện có, rất tốt cho hầu hết các tác vụ và nói chung là khá chấp nhận được.


1
Trình biên dịch Pascal / Modula-2 / Oberon / Oberon-2 sẽ tự biên dịch trong chưa đầy một giây trên một máy hiện đại [cần dẫn nguồn]
CoffeeandCode

1
Trích dẫn thêm, xem tài liệu tham khảo [1].
Andreas

1
"... nguyên tắc ... tìm đường đến các ngôn ngữ hiện đại như Go (Google), Swift (Apple)" Không chắc Swift đã đưa vào danh sách đó như thế nào: trình biên dịch Swift là băng hà . Tại một cuộc họp của Cốc Cốc Berlin gần đây, một người nào đó đã cung cấp một số con số cho một khung cỡ trung bình, họ đã đạt tới 16 LỘC mỗi giây.
mpw

13

Go được thiết kế để nhanh chóng, và nó cho thấy.

  1. Quản lý phụ thuộc: không có tệp tiêu đề, bạn chỉ cần xem các gói được nhập trực tiếp (không cần phải lo lắng về những gì họ nhập), do đó bạn có phụ thuộc tuyến tính.
  2. Ngữ pháp: ngữ pháp của ngôn ngữ đơn giản, do đó dễ dàng phân tích cú pháp. Mặc dù số lượng các tính năng bị giảm, do đó bản thân mã trình biên dịch rất chặt chẽ (một vài đường dẫn).
  3. Không cho phép quá tải: bạn nhìn thấy một biểu tượng, bạn biết nó đề cập đến phương pháp nào.
  4. Có thể biên dịch song song với Go vì mỗi gói có thể được biên dịch độc lập.

Lưu ý rằng GO không phải là ngôn ngữ duy nhất có các tính năng như vậy (mô-đun là chuẩn mực trong các ngôn ngữ hiện đại), nhưng họ đã làm tốt điều đó.


Điểm (4) không hoàn toàn đúng. Các mô-đun phụ thuộc lẫn nhau nên được biên dịch theo thứ tự phụ thuộc để cho phép nội tuyến và nội dung mô-đun chéo.
fuz

1
@FUZxxl: Điều này chỉ liên quan đến giai đoạn tối ưu hóa, bạn có thể có sự song song hoàn hảo cho đến thế hệ IR phụ trợ; Do đó, chỉ tối ưu hóa mô-đun chéo mới được quan tâm, có thể được thực hiện ở giai đoạn liên kết và dù sao liên kết cũng không song song. Tất nhiên, nếu bạn không muốn sao chép công việc của mình (phân tích lại), bạn nên biên dịch theo cách "mạng": 1 / mô-đun không phụ thuộc, 2 / mô-đun chỉ phụ thuộc vào (1), 3 / mô-đun chỉ phụ thuộc vào (1) và (2), ...
Matthieu M.

2
Điều này là hoàn toàn dễ dàng để sử dụng các tiện ích cơ bản như Makefile.
fuz

12

Trích dẫn từ cuốn sách " Ngôn ngữ lập trình Go " của Alan Donovan và Brian Kernighan:

Biên dịch Go nhanh hơn đáng kể so với hầu hết các ngôn ngữ được biên dịch khác, ngay cả khi xây dựng từ đầu. Có ba lý do chính cho tốc độ của trình biên dịch. Đầu tiên, tất cả các lần nhập phải được liệt kê rõ ràng ở đầu mỗi tệp nguồn, vì vậy trình biên dịch không phải đọc và xử lý toàn bộ tệp để xác định các phụ thuộc của nó. Thứ hai, các phụ thuộc của một gói tạo thành một biểu đồ chu kỳ có hướng, và vì không có chu kỳ, các gói có thể được biên dịch riêng và có thể song song. Cuối cùng, tệp đối tượng cho gói Go được biên dịch ghi lại thông tin xuất khẩu không chỉ cho chính gói đó mà còn cho các phụ thuộc của nó. Khi biên dịch một gói, trình biên dịch phải đọc một tệp đối tượng cho mỗi lần nhập nhưng không cần nhìn xa hơn các tệp này.


9

Ý tưởng cơ bản của việc biên dịch thực sự rất đơn giản. Về nguyên tắc, một trình phân tích cú pháp gốc đệ quy có thể chạy ở tốc độ giới hạn I / O. Tạo mã về cơ bản là một quá trình rất đơn giản. Một bảng biểu tượng và hệ thống loại cơ bản không phải là một cái gì đó đòi hỏi nhiều tính toán.

Tuy nhiên, không khó để làm chậm trình biên dịch.

Nếu có một giai đoạn tiền xử lý, với đa cấp bao gồm các chỉ thị, định nghĩa vĩ mô và biên dịch có điều kiện, cũng hữu ích như những điều đó, không khó để tải xuống. (Ví dụ, tôi đang nghĩ về các tệp tiêu đề Windows và MFC.) Đó là lý do tại sao các tiêu đề được biên dịch trước là cần thiết.

Về mặt tối ưu hóa mã được tạo, không có giới hạn về số lượng xử lý có thể được thêm vào pha đó.


7

Đơn giản (theo cách nói của tôi), vì cú pháp rất dễ (để phân tích và phân tích cú pháp)

Chẳng hạn, không có nghĩa là thừa kế kiểu, không phân tích có vấn đề để tìm hiểu xem kiểu mới có tuân theo các quy tắc được áp đặt bởi loại cơ sở hay không.

Ví dụ trong ví dụ mã này: "giao diện" trình biên dịch không đi và kiểm tra xem loại dự định có thực hiện giao diện đã cho trong khi phân tích loại đó không. Chỉ cho đến khi nó được sử dụng (và NẾU nó được sử dụng), kiểm tra được thực hiện.

Ví dụ khác, trình biên dịch sẽ cho bạn biết nếu bạn đang khai báo một biến và không sử dụng nó (hoặc nếu bạn có nghĩa vụ giữ một giá trị trả về và bạn thì không)

Sau đây không biên dịch:

package main
func main() {
    var a int 
    a = 0
}
notused.go:3: a declared and not used

Các loại thực thi và nguyên tắc này làm cho mã kết quả an toàn hơn và trình biên dịch không phải thực hiện các xác nhận bổ sung mà lập trình viên có thể làm.

Nhìn chung, tất cả các chi tiết này làm cho một ngôn ngữ dễ dàng phân tích hơn dẫn đến việc biên dịch nhanh.

Một lần nữa, theo lời của tôi.


3

Tôi nghĩ rằng Go được thiết kế song song với việc tạo trình biên dịch, vì vậy họ là những người bạn tốt nhất từ ​​khi sinh ra. (IMO)


0
  • Nhập phụ thuộc một lần cho tất cả các tệp, vì vậy thời gian nhập không tăng theo cấp số nhân với quy mô dự án.
  • Ngôn ngữ học đơn giản hơn có nghĩa là giải thích chúng mất ít tính toán hơn.

Còn gì nữa không

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.