Độ phức tạp thời gian của trình biên dịch


54

Tôi quan tâm đến sự phức tạp thời gian của một trình biên dịch. Rõ ràng đây là một câu hỏi rất phức tạp vì có nhiều trình biên dịch, tùy chọn trình biên dịch và các biến để xem xét. Cụ thể, tôi quan tâm đến LLVM nhưng sẽ quan tâm đến bất kỳ suy nghĩ nào mà mọi người có hoặc nơi để bắt đầu nghiên cứu. Một google khá dường như mang lại ít ánh sáng.

Tôi đoán sẽ có một số bước tối ưu hóa theo cấp số nhân, nhưng ít ảnh hưởng đến thời gian thực tế. Ví dụ, hàm mũ dựa trên số là đối số của hàm.

Từ đỉnh đầu của tôi, tôi sẽ nói rằng việc tạo cây AST sẽ là tuyến tính. Tạo IR sẽ yêu cầu bước qua cây trong khi tìm kiếm các giá trị trong các bảng ngày càng tăng, vì vậy hoặc O ( n log n ) . Tạo mã và liên kết sẽ là một loại hoạt động tương tự. Do đó, dự đoán của tôi sẽ là O ( n 2 ) , nếu chúng ta loại bỏ số mũ của các biến không tăng trưởng thực tế.O(n2)Ôi(viết sai rồiđăng nhậpviết sai rồi)Ôi(viết sai rồi2)

Tôi có thể hoàn toàn sai mặc dù. Có ai có bất kỳ suy nghĩ về nó?


7
Bạn phải cẩn thận khi bạn yêu cầu bất cứ điều gì là "hàm mũ", "tuyến tính", hoặc . Ít nhất với tôi, việc bạn đo lường đầu vào của bạn không rõ ràng (Số mũ trong cái gì? cho cái gì?)O ( n log n ) nÔi(viết sai rồi2)Ôi(viết sai rồiđăng nhậpviết sai rồi)viết sai rồi
Juho

2
Khi bạn nói LLVM, bạn có nghĩa là Clang? LLVM là một dự án lớn với một số tiểu dự án trình biên dịch khác nhau nên hơi mơ hồ.
Nate CK

5
Đối với C #, ít nhất là theo cấp số nhân đối với các sự cố trong trường hợp xấu nhất (bạn có thể mã hóa bài toán SAT hoàn thành NP trong C #). Đây không chỉ là tối ưu hóa, nó là cần thiết để chọn quá tải chính xác của một chức năng. Đối với ngôn ngữ như C ++, điều đó sẽ không thể giải quyết được, vì các mẫu đã hoàn tất.
CodeInChaos

2
@Zane Tôi không hiểu quan điểm của bạn. Khởi tạo mẫu xảy ra trong quá trình biên dịch. Bạn có thể mã hóa các vấn đề khó khăn thành các mẫu theo cách buộc trình biên dịch giải quyết vấn đề đó để tạo ra một đầu ra chính xác. Bạn có thể coi trình biên dịch là trình thông dịch của ngôn ngữ lập trình mẫu hoàn chỉnh.
CodeInChaos

3
Độ phân giải quá tải C # khá khó khăn khi bạn kết hợp nhiều tình trạng quá tải với các biểu thức lambda. Bạn có thể sử dụng điều đó để mã hóa một công thức boolean theo cách như vậy, để xác định xem có quá tải áp dụng yêu cầu bài toán 3SAT NP-Complete không. Để thực sự biên dịch vấn đề, trình biên dịch phải thực sự tìm ra giải pháp cho công thức đó, thậm chí có thể khó hơn. Eric Lippert nói về điều đó một cách chi tiết trong bài đăng trên blog của mình Lambda Expressions so với các phương thức ẩn danh, Phần thứ năm
CodeInChaos

Câu trả lời:


50

Cuốn sách tốt nhất để trả lời câu hỏi của bạn có lẽ là: Cooper và Torczon, "Engineering a Compiler", 2003. Nếu bạn có quyền truy cập vào thư viện trường đại học, bạn sẽ có thể mượn một bản sao.

Trong một trình biên dịch sản xuất như llvm hoặc gcc, các nhà thiết kế cố gắng hết sức để giữ tất cả các thuật toán bên dưới trong đó n là kích thước của đầu vào. Đối với một số phân tích cho các giai đoạn "tối ưu hóa", điều này có nghĩa là bạn cần sử dụng phương pháp phỏng đoán thay vì tạo mã thực sự tối ưu.Ôi(viết sai rồi2)viết sai rồi

Lexex là một máy trạng thái hữu hạn, vì vậy kích thước của đầu vào (tính bằng ký tự) và tạo ra một luồng mã thông báo O ( n ) được truyền cho trình phân tích cú pháp.Ôi(viết sai rồi)Ôi(viết sai rồi)

Đối với nhiều trình biên dịch cho nhiều ngôn ngữ, trình phân tích cú pháp là LALR (1) và do đó xử lý luồng mã thông báo theo thời gian theo số lượng mã thông báo đầu vào. Trong quá trình phân tích cú pháp, bạn thường phải theo dõi bảng ký hiệu, nhưng, đối với nhiều ngôn ngữ, có thể được xử lý bằng một chồng bảng băm ("từ điển"). Mỗi lần truy cập từ điển là O ( 1 ) , nhưng đôi khi bạn có thể phải đi bộ ngăn xếp để tìm kiếm một biểu tượng. Độ sâu của ngăn xếp là O ( s ) trong đó s là độ sâu lồng của các phạm vi. (Vì vậy, trong các ngôn ngữ giống như C, bạn có bao nhiêu lớp niềng răng xoăn.)Ôi(viết sai rồi)Ôi(1)Ôi(S)S

Sau đó, cây phân tích thường được "làm phẳng" thành một biểu đồ luồng điều khiển. Các nút của biểu đồ luồng điều khiển có thể là các lệnh 3 địa chỉ (tương tự như ngôn ngữ lắp ráp RISC) và kích thước của biểu đồ luồng điều khiển thường sẽ là tuyến tính theo kích thước của cây phân tích cú pháp.

Sau đó, một loạt các bước loại bỏ dự phòng thường được áp dụng (loại bỏ phổ biến phụ, chuyển động mã bất biến vòng lặp, lan truyền liên tục, ...). (Điều này thường được gọi là "tối ưu hóa" mặc dù hiếm khi có kết quả tối ưu, mục tiêu thực sự là cải thiện mã càng nhiều càng tốt trong giới hạn không gian và thời gian mà chúng tôi đã đặt trên trình biên dịch.) Mỗi ​​bước loại bỏ dự phòng sẽ thường yêu cầu bằng chứng về một số sự thật về biểu đồ luồng điều khiển. Những bằng chứng này thường được thực hiện bằng cách sử dụng phân tích luồng dữ liệu . Hầu hết các phân tích luồng dữ liệu được thiết kế sao cho chúng sẽ hội tụ trong đi qua biểu đồ luồng trong đó dÔi(Cười mở miệng)Cười mở miệnglà (đại khái) độ sâu lồng vòng lặp và vượt qua biểu đồ luồng mất thời gian trong đó n là số lượng lệnh 3 địa chỉ.Ôi(viết sai rồi)viết sai rồi

Để tối ưu hóa tinh vi hơn, bạn có thể muốn thực hiện các phân tích tinh vi hơn. Tại thời điểm này, bạn bắt đầu chạy vào sự đánh đổi. Bạn muốn các thuật toán phân tích của bạn mất ít hơn nhiều so với Ôi(viết sai rồi2)thời gian trong kích thước của biểu đồ luồng của toàn chương trình, nhưng điều này có nghĩa là bạn cần thực hiện mà không có thông tin (và các biến đổi cải tiến chương trình) có thể tốn kém để chứng minh. Một ví dụ kinh điển về điều này là phân tích bí danh, trong đó đối với một số cặp ghi nhớ bạn muốn chứng minh rằng hai lần ghi đó không bao giờ có thể nhắm mục tiêu vào cùng một vị trí bộ nhớ. (Bạn có thể muốn thực hiện phân tích bí danh để xem liệu bạn có thể di chuyển một lệnh này sang hướng dẫn khác không.) Nhưng để có được thông tin chính xác về bí danh, bạn có thể cần phân tích mọi đường dẫn điều khiển có thể thông qua chương trình, theo cấp số mũ trong chương trình (và do đó theo cấp số nhân về số lượng nút trong biểu đồ luồng điều khiển.)

Tiếp theo bạn nhận được vào phân bổ đăng ký. Phân bổ đăng ký có thể được coi là một vấn đề tô màu đồ thị và tô màu cho một đồ thị với số lượng màu tối thiểu được gọi là NP-Hard. Vì vậy, hầu hết các trình biên dịch sử dụng một số loại heuristic tham lam kết hợp với tràn đăng ký với mục tiêu giảm số lượng tràn đăng ký tốt nhất có thể trong giới hạn thời gian hợp lý.

Cuối cùng, bạn nhận được vào việc tạo mã. Việc tạo mã thường được thực hiện một khối cơ bản tối đa tại một thời điểm trong đó một khối cơ bản là một tập hợp các nút biểu đồ luồng điều khiển được kết nối tuyến tính với một mục nhập duy nhất và một lối thoát duy nhất. Điều này có thể được định dạng lại dưới dạng vấn đề bao gồm biểu đồ trong đó biểu đồ bạn đang cố gắng che là biểu đồ phụ thuộc của bộ hướng dẫn 3 địa chỉ trong khối cơ bản và bạn đang cố gắng che bằng một bộ biểu đồ đại diện cho máy có sẵn hướng dẫn. Vấn đề này là theo cấp số nhân về kích thước của khối cơ bản lớn nhất (về nguyên tắc, có thể có cùng thứ tự với kích thước của toàn bộ chương trình), do đó, điều này một lần nữa thường được thực hiện với phương pháp phỏng đoán trong đó chỉ có một tập hợp con nhỏ của các lớp phủ có thể kiểm tra.


4
Thứ ba! Ngẫu nhiên, nhiều vấn đề mà trình biên dịch cố gắng giải quyết (ví dụ phân bổ đăng ký) là NP-hard, nhưng những vấn đề khác chính thức là không thể giải quyết được. Giả sử, ví dụ, bạn có một cuộc gọi p () theo sau là một cuộc gọi q (). Nếu p là một hàm thuần túy, thì bạn có thể sắp xếp lại các cuộc gọi một cách an toàn miễn là p () không lặp vô hạn. Chứng minh điều này đòi hỏi phải giải quyết vấn đề tạm dừng. Như với các vấn đề NP-hard, một người viết trình biên dịch có thể đưa ra càng nhiều hoặc ít nỗ lực trong việc xấp xỉ một giải pháp càng khả thi.
Bút danh

4
Ồ, một điều nữa: Có một số loại hệ thống được sử dụng ngày nay rất phức tạp về mặt lý thuyết. Suy luận kiểu Hindley-Milner được biết đến với các ngôn ngữ hoàn chỉnh DEXPTIME và các ngôn ngữ giống ML phải thực hiện chính xác. Tuy nhiên, thời gian chạy là tuyến tính trong thực tế vì a) các trường hợp bệnh lý không bao giờ xuất hiện trong các chương trình trong thế giới thực và b) các lập trình viên trong thế giới thực có xu hướng đưa vào các chú thích loại, nếu chỉ để nhận thông báo lỗi tốt hơn.
Bút danh

1
Câu trả lời tuyệt vời, điều duy nhất còn thiếu là phần đơn giản của lời giải thích, được đánh vần theo thuật ngữ đơn giản: Biên dịch một chương trình có thể được thực hiện trong O (n). Tối ưu hóa một chương trình trước khi biên dịch, như bất kỳ trình biên dịch hiện đại nào sẽ làm, là một nhiệm vụ thực tế không giới hạn. Thời gian thực sự không bị chi phối bởi bất kỳ giới hạn vốn có của nhiệm vụ, mà là do nhu cầu thực tế để trình biên dịch hoàn thành vào một lúc nào đó trước khi mọi người mệt mỏi chờ đợi. Nó luôn luôn là một sự thỏa hiệp.
aaaaaaaaaaaa

@Pseudonymous, thực tế là nhiều lần trình biên dịch sẽ phải giải quyết vấn đề tạm dừng (hoặc các vấn đề khó NP rất khó chịu) là một trong những lý do khiến các trình soạn thảo mất thời gian khi cho rằng hành vi không xác định không xảy ra (như các vòng lặp vô hạn và như vậy ).
vonbrand

15

Trên thực tế, một số ngôn ngữ (như C ++, Lisp và D) hoàn thành Turing tại thời điểm biên dịch, do đó, việc biên dịch chúng là không thể nói chung. Đối với C ++, điều này là do khởi tạo mẫu đệ quy. Đối với Lisp và D, bạn có thể thực thi hầu hết mọi mã trong thời gian biên dịch, vì vậy bạn có thể ném trình biên dịch vào một vòng lặp vô hạn nếu bạn muốn.


3
Các hệ thống loại của Haskell (có phần mở rộng) và Scala cũng hoàn thành Turing, nghĩa là việc kiểm tra loại có thể mất một lượng thời gian vô hạn. Scala bây giờ cũng có macro Turing-Complete trên đầu trang.
Jörg W Mittag

5

Từ kinh nghiệm thực tế của tôi với trình biên dịch C #, tôi có thể nói rằng đối với các chương trình nhất định, kích thước của nhị phân đầu ra tăng theo cấp số nhân đối với kích thước của nguồn đầu vào (điều này thực sự được yêu cầu bởi thông số C # và không thể giảm) ít nhất phải theo cấp số nhân.

Nhiệm vụ giải quyết quá tải chung trong C # được biết đến là NP-hard (và độ phức tạp thực hiện thực tế ít nhất là theo cấp số nhân).

Việc xử lý các nhận xét tài liệu XML trong các nguồn C # cũng yêu cầu đánh giá các biểu thức XPath 1.0 tùy ý tại thời gian biên dịch, đó cũng là hàm mũ, AFAIK.


Điều gì làm cho nhị phân C # nổ tung theo cách đó? Nghe có vẻ như là một lỗi ngôn ngữ đối với tôi ...
vonbrand

1
Đó là cách các loại chung được mã hóa trong siêu dữ liệu. class X<A,B,C,D,E> { class Y : X<Y,Y,Y,Y,Y> { Y.Y.Y.Y.Y.Y.Y.Y.Y y; } }
Vladimir Reshetnikov

-2

Đo lường nó với các cơ sở mã thực tế, chẳng hạn như một tập hợp các dự án nguồn mở. Nếu bạn vẽ kết quả dưới dạng (codeSize, finishTime), thì bạn có thể vẽ các biểu đồ đó. Nếu dữ liệu của bạn f (x) = y là O (n), thì âm mưu g = f (x) / x sẽ cung cấp cho bạn một đường thẳng sau khi dữ liệu bắt đầu lớn.

Lô f (x) / x, f (x) / lg (x), f (x) / (x * lg (x)), f (x) / (x * x), v.v ... Đồ thị sẽ lặn tắt về không, tăng mà không bị ràng buộc, hoặc san phẳng. Ý tưởng này có ích cho các tình huống như đo thời gian chèn bắt đầu từ cơ sở dữ liệu trống (nghĩa là: tìm kiếm "rò rỉ hiệu suất" trong một khoảng thời gian dài.).


1
Đo lường thực nghiệm về thời gian chạy không thiết lập độ phức tạp tính toán. Đầu tiên, độ phức tạp tính toán được thể hiện phổ biến nhất theo thời gian chạy trường hợp xấu nhất. Thứ hai, ngay cả khi bạn muốn đo một số trường hợp trung bình, bạn cần phải xác định rằng đầu vào của bạn là "trung bình" theo nghĩa đó.
David Richerby

Chắc chắn đó chỉ là ước tính. Nhưng các thử nghiệm thực nghiệm đơn giản với nhiều dữ liệu thực (mỗi cam kết cho một loạt các git repos) có thể đánh bại một mô hình cẩn thận. Trong mọi trường hợp, nếu một hàm thực sự là O (n ^ 3) và bạn vẽ đồ thị f (n) / (n n n), bạn sẽ nhận được một dòng nhiễu với độ dốc gần bằng không. Nếu bạn chỉ vẽ O (n ^ 3) / (n * n), bạn sẽ thấy nó tăng tuyến tính. Nó thực sự rõ ràng nếu bạn đánh giá quá cao và xem dòng nhanh chóng lặn xuống không.
Rob

1
Θ(viết sai rồiđăng nhậpviết sai rồi)Θ(viết sai rồi2)Θ(viết sai rồiđăng nhậpviết sai rồi)Θ(viết sai rồi2)

Tôi đồng ý rằng đó là những gì bạn cần biết nếu bạn lo lắng về việc từ chối dịch vụ từ kẻ tấn công cung cấp cho bạn đầu vào xấu, thực hiện một số phân tích cú pháp nhập liệu quan trọng theo thời gian thực. Hàm thực sự đo thời gian biên dịch sẽ rất ồn và trường hợp mà chúng ta quan tâm sẽ nằm trong kho lưu trữ mã thực.
Rob

1
Không. Câu hỏi hỏi về độ phức tạp thời gian của vấn đề. Điều đó thường được hiểu là thời gian chạy trong trường hợp xấu nhất, rõ ràng không phải là thời gian chạy mã trong kho. Các thử nghiệm mà bạn đề xuất đưa ra cách xử lý hợp lý về thời gian bạn có thể mong đợi trình biên dịch sẽ thực hiện một đoạn mã nhất định, đây là một điều tốt và hữu ích để biết. Nhưng họ nói với bạn hầu như không có gì về sự phức tạp tính toán của vấn đề.
David Richerby
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.