Tại sao các chương trình sử dụng ngăn xếp cuộc gọi, nếu các lệnh gọi hàm lồng nhau có thể được nội tuyến?


32

Tại sao không có trình biên dịch lấy một chương trình như thế này:

function a(b) { return b^2 };
function c(b) { return a(b) + 5 };

và chuyển đổi nó thành một chương trình như thế này:

function c(b) { return b^2 + 5 };

do đó loại bỏ sự cần thiết của máy tính để nhớ địa chỉ trả về của c (b)?

Tôi cho rằng dung lượng ổ cứng và RAM tăng cần thiết để lưu trữ chương trình và hỗ trợ quá trình biên dịch của nó (tương ứng) là lý do tại sao chúng tôi sử dụng ngăn xếp cuộc gọi. Đúng không?


30
Xem những gì sẽ xảy ra nếu bạn làm điều này trên một chương trình với bất kỳ kích thước có ý nghĩa. Đặc biệt, các chức năng được gọi từ nhiều nơi.
dùng253751

10
Ngoài ra, đôi khi trình biên dịch không biết hàm nào được gọi! Ví dụ ngớ ngẩn:window[prompt("Enter function name","")]()
user253751

26
Làm thế nào để bạn thực hiện function(a)b { if(b>0) return a(b-1); }mà không có một ngăn xếp?
pjc50

8
Đâu là mối quan hệ với lập trình chức năng?
mastov

14
@ pjc50: đó là đệ quy đuôi, vì vậy trình biên dịch sẽ dịch nó thành một vòng lặp có thể thay đổi b. Nhưng theo quan điểm, không phải tất cả các hàm đệ quy đều có thể loại bỏ đệ quy và ngay cả khi hàm có thể về nguyên tắc, trình biên dịch có thể không đủ thông minh để làm như vậy.
Steve Jessop

Câu trả lời:


75

Điều này được gọi là "nội tuyến" và nhiều trình biên dịch thực hiện điều này như một chiến lược tối ưu hóa trong trường hợp có ý nghĩa.

Trong ví dụ cụ thể của bạn, tối ưu hóa này sẽ tiết kiệm cả không gian và thời gian thực hiện. Nhưng nếu hàm được gọi ở nhiều nơi trong chương trình (không phổ biến!), Nó sẽ tăng kích thước mã, vì vậy chiến lược trở nên đáng ngờ hơn. (Và tất nhiên nếu một hàm gọi chính nó trực tiếp hoặc gián tiếp thì không thể thực hiện được nội tuyến, từ đó mã sẽ trở nên vô hạn về kích thước.)

Và rõ ràng là chỉ có thể cho các chức năng "riêng tư". Các chức năng được hiển thị cho người gọi bên ngoài không thể được tối ưu hóa, ít nhất là không bằng ngôn ngữ có liên kết động.


7
@Blrfl: Trình biên dịch hiện đại thực sự không cần định nghĩa trong tiêu đề nữa; họ có thể nội tuyến trên các Đơn vị dịch thuật. Điều này không đòi hỏi một liên kết tốt, mặc dù. Các định nghĩa trong các tệp tiêu đề là một cách giải quyết cho các liên kết câm.
MSalters

3
"Các chức năng được hiển thị cho người gọi bên ngoài không thể được tối ưu hóa" - chức năng phải tồn tại, nhưng bất kỳ trang web cuộc gọi nào cho nó (trong mã của riêng bạn hoặc nếu chúng có nguồn, người gọi bên ngoài ') có thể được nội tuyến.
Random832

14
Wow, 28 câu trả lời cho một câu trả lời thậm chí không đề cập đến lý do tại sao nội tuyến mọi thứ là không thể: Đệ quy.
mastov

3
@R ..: LTO là Tối ưu hóa thời gian LINK, không phải tối ưu hóa thời gian LOAD.
MSalters

2
@immibis: Nhưng nếu điều đó ngăn xếp rõ ràng được giới thiệu bởi trình biên dịch, sau đó ngăn xếp các cuộc gọi stack.
user2357112 hỗ trợ Monica

51

Có hai phần cho câu hỏi của bạn: Tại sao có nhiều chức năng (thay vì thay thế các cuộc gọi chức năng bằng định nghĩa của chúng) và tại sao thực hiện các chức năng đó với ngăn xếp cuộc gọi thay vì phân bổ tĩnh dữ liệu của chúng ở một nơi khác?

Lý do đầu tiên là đệ quy. Không chỉ là kiểu "oh hãy thực hiện một cuộc gọi chức năng mới cho mỗi mục duy nhất trong danh sách này", cũng là loại khiêm tốn mà bạn có thể có hai cuộc gọi của một chức năng hoạt động cùng một lúc, với nhiều chức năng khác ở giữa chúng. Bạn cần đặt các biến cục bộ trên một ngăn xếp để hỗ trợ điều này và bạn không thể nói chung các hàm đệ quy nội tuyến.

Sau đó, có một vấn đề đối với các thư viện: Bạn không biết các hàm nào sẽ được gọi từ đâu và tần suất như thế nào, do đó, một "thư viện" không bao giờ thực sự được biên dịch, chỉ được chuyển đến tất cả các máy khách ở một số định dạng cấp cao thuận tiện. nội tuyến vào ứng dụng. Ngoài các vấn đề khác với điều này, bạn hoàn toàn mất liên kết động với tất cả các lợi thế của nó.

Ngoài ra, có nhiều lý do để không thực hiện chức năng nội tuyến ngay cả khi bạn có thể:

  1. Nó không nhất thiết phải nhanh hơn. Thiết lập khung ngăn xếp và xé nó có thể là một tá các lệnh đơn chu kỳ, đối với nhiều hàm lớn hoặc vòng lặp thậm chí không bằng 0,1% thời gian thực hiện của chúng.
  2. Nó có thể chậm hơn. Sao chép mã có chi phí, ví dụ, nó sẽ tạo thêm áp lực trong bộ đệm hướng dẫn.
  3. Một số chức năng rất lớn và được gọi từ nhiều nơi, nội tuyến chúng ở khắp mọi nơi làm tăng nhị phân vượt xa những gì hợp lý.
  4. Trình biên dịch thường có một thời gian khó khăn với các chức năng rất lớn. Mọi thứ khác đều bằng nhau, một hàm có kích thước 2 * N mất hơn 2 * T thời gian trong đó một hàm có kích thước N mất thời gian T.

1
Tôi ngạc nhiên bởi điểm 4. lý do cho việc này là gì?
JacquesB

12
@JacquesB Nhiều thuật toán tối ưu hóa là bậc hai, khối hoặc thậm chí là NP-đầy đủ về mặt kỹ thuật. Ví dụ chính tắc là phân bổ thanh ghi, được hoàn thành NP bằng cách tương tự với tô màu đồ thị. . n log n thời gian với n khối cơ bản).

2
"Bạn thực sự có hai câu hỏi ở đây" Không, tôi không. Chỉ một - tại sao không coi một lệnh gọi hàm như là một trình giữ chỗ mà trình biên dịch có thể, nói, thay thế bằng mã của hàm được gọi?
moonman239

4
@ moonman239 Sau đó, từ ngữ của bạn đã ném tôi đi. Tuy nhiên, câu hỏi của bạn có thể được phân tách khi tôi trả lời và tôi nghĩ đó là một viễn cảnh hữu ích.

16

Ngăn xếp cho phép chúng tôi bỏ qua các giới hạn áp đặt bởi số lượng đăng ký hữu hạn.

Hãy tưởng tượng có chính xác 26 "thanh ghi az" toàn cầu (hoặc thậm chí chỉ có các thanh ghi có kích thước 7 byte của chip 8080) Và mọi chức năng bạn viết trong ứng dụng này đều chia sẻ danh sách phẳng này.

Một khởi đầu ngây thơ sẽ là phân bổ một vài thanh ghi đầu tiên cho hàm đầu tiên và biết rằng nó chỉ mất 3, bắt đầu bằng "d" cho hàm thứ hai ... Bạn hết nhanh chóng.

Thay vào đó, nếu bạn có một băng ẩn dụ, như máy turing, bạn có thể có mỗi chức năng bắt đầu "gọi một chức năng khác" bằng cách lưu tất cả các biến mà nó sử dụng và chuyển tiếp () băng, và sau đó chức năng callee có thể trộn lẫn với nhiều đăng ký như nó muốn. Khi callee kết thúc, nó sẽ trả lại quyền điều khiển cho hàm cha, người biết nơi để ngắt đầu ra của callee khi cần, sau đó phát băng ngược lại để khôi phục trạng thái của nó.

Khung cuộc gọi cơ bản của bạn chỉ là như vậy, và được tạo và loại bỏ bởi các chuỗi mã máy được tiêu chuẩn hóa mà trình biên dịch đưa vào xung quanh các chuyển đổi từ chức năng này sang chức năng khác. (Đã lâu rồi tôi mới phải nhớ các khung ngăn xếp C của mình, nhưng bạn có thể đọc các cách khác nhau về nhiệm vụ của những người bỏ những gì tại X86_calling_conventions .)

(đệ quy là tuyệt vời, nhưng nếu bạn đã từng phải sắp xếp các thanh ghi mà không có ngăn xếp, thì bạn thực sự đánh giá cao các ngăn xếp.)


Tôi cho rằng dung lượng ổ cứng và RAM tăng cần thiết để lưu trữ chương trình và hỗ trợ quá trình biên dịch của nó (tương ứng) là lý do tại sao chúng tôi sử dụng ngăn xếp cuộc gọi. Đúng không?

Mặc dù chúng ta có thể nội tuyến nhiều hơn trong những ngày này, ("tốc độ nhanh hơn" luôn luôn tốt; "ít kb lắp ráp hơn" có nghĩa là rất ít trong thế giới của các luồng video) Hạn chế chính là ở khả năng của trình biên dịch trong việc làm phẳng các loại mẫu mã nhất định.

Ví dụ: các đối tượng đa hình - nếu bạn không biết một và chỉ một loại đối tượng bạn sẽ được trao, bạn không thể làm phẳng; bạn phải xem vtable các tính năng của đối tượng và gọi qua con trỏ đó ... tầm thường phải làm trong thời gian chạy, không thể nội tuyến trong thời gian biên dịch.

Một chuỗi công cụ hiện đại có thể vui vẻ nội tuyến một hàm được định nghĩa đa hình khi nó đã làm phẳng đủ số người gọi để biết chính xác hương vị của obj là gì:

class Base {
    public: void act() = 0;
};
class Child1: public Base {
    public: void act() {};
};
void ActOn(Base* something) {
    something->act();
}
void InlineMe() {
    Child1 thingamabob;
    ActOn(&thingamabob);
}

ở trên, trình biên dịch có thể chọn giữ đúng nội tuyến, từ InlineMe cho đến bất kỳ hành động bên trong nào (), cũng không cần phải chạm vào bất kỳ vtables nào khi chạy.

Nhưng bất kỳ sự không chắc chắn trong hương vị của đối tượng sẽ để lại nó như là một cuộc gọi đến một chức năng riêng biệt, ngay cả khi một số lời mời khác của cùng chức năng được đưa vào .


11

Các trường hợp tiếp cận không thể xử lý:

function fib(a) { if(a>2) return fib(a-1)+fib(a-2); else return 1; }

function many(a) { for(i = 1 to a) { b(i); };}

những ngôn ngữ và nền tảng với ngăn xếp hạn chế hoặc không gọi. Bộ vi xử lý PIC có một ngăn xếp phần cứng giới hạn trong khoảng từ 2 đến 32 mục . Điều này tạo ra các hạn chế thiết kế.

Lệnh đệ quy của COBOL: https://stackoverflow.com/questions/27806812/in-cobol-is-it-possible-to-recursively-call-a-par Đoạn

Áp dụng lệnh cấm đệ quy không có nghĩa là bạn có thể biểu diễn toàn bộ sơ đồ của chương trình một cách tĩnh như một DAG. Trình biên dịch của bạn sau đó có thể phát ra một bản sao của một hàm cho mỗi nơi mà nó được gọi với một bước nhảy cố định thay vì trả về. Không yêu cầu ngăn xếp, chỉ cần thêm không gian chương trình, có khả năng khá nhiều cho các hệ thống phức tạp. Nhưng đối với các hệ thống nhúng nhỏ, điều này có nghĩa là bạn có thể đảm bảo không bị tràn ngăn xếp khi chạy, đây sẽ là tin xấu cho lò phản ứng hạt nhân / điều khiển phản lực / điều khiển bướm ga của xe hơi, v.v.


12
Ví dụ đầu tiên của bạn là đệ quy cơ bản và bạn đã đúng ở đó. Nhưng ví dụ thứ hai của bạn dường như là một vòng lặp for gọi hàm khác. Trong chức năng lót khác với việc không điều khiển một vòng lặp; chức năng có thể được xếp hàng mà không hủy vòng lặp. Hoặc tôi đã bỏ lỡ một số chi tiết tinh tế?
jpmc26

1
Nếu ví dụ đầu tiên của bạn có nghĩa là xác định chuỗi Fibonacci, thì đó là sai. (Đó là thiếu một fibcuộc gọi.)
Paŭlo Ebermann

1
Mặc dù cấm đệ quy không có nghĩa là toàn bộ biểu đồ cuộc gọi có thể được biểu diễn dưới dạng DAG, điều đó không có nghĩa là người ta có thể liệt kê danh sách đầy đủ các chuỗi cuộc gọi lồng nhau trong một khoảng trống hợp lý. Trong một dự án của tôi cho một vi điều khiển có không gian mã 128KB, tôi đã mắc lỗi khi yêu cầu một biểu đồ cuộc gọi bao gồm tất cả các chức năng có thể ảnh hưởng đến yêu cầu RAM tham số tối đa và biểu đồ cuộc gọi đó đã vượt quá một gig. Một biểu đồ cuộc gọi hoàn chỉnh sẽ còn dài hơn nữa và đó là chương trình phù hợp với 128K không gian mã.
supercat

8

Bạn muốn hàm nội tuyến và hầu hết các trình biên dịch ( tối ưu hóa ) đang làm điều đó.

Chú ý rằng nội tuyến đòi hỏi sự gọi là chức năng được biết đến (và chỉ có hiệu lực nếu điều đó được gọi là chức năng không phải là quá lớn), vì khái niệm nó được thay thế cuộc gọi bằng các viết lại của gọi functgion. Vì vậy, bạn thường không thể định tuyến một hàm chưa biết (ví dụ: con trỏ hàm - và bao gồm các hàm từ các thư viện chia sẻ được liên kết động - có thể hiển thị dưới dạng phương thức ảo trong một số vtable ; nhưng một số trình biên dịch đôi khi có thể tối ưu hóa các kỹ thuật ảo hóa thông qua ). Tất nhiên không phải lúc nào cũng có thể thực hiện các hàm đệ quy nội tuyến (một số trình biên dịch thông minh có thể sử dụng đánh giá một phần và trong một số trường hợp có thể thực hiện các hàm đệ quy nội tuyến).

Cũng lưu ý rằng nội tuyến, ngay cả khi có thể dễ dàng, không phải lúc nào cũng hiệu quả: bạn (thực ra là trình biên dịch của bạn) có thể tăng kích thước mã đến mức mà bộ đệm CPU (hoặc bộ dự báo nhánh ) sẽ hoạt động kém hiệu quả hơn và điều đó sẽ khiến chương trình của bạn chạy chậm hơn

Tôi hơi tập trung vào phong cách lập trình chức năng , vì bạn đã gắn thẻ qestion của mình như vậy.

Lưu ý rằng bạn không cần phải có bất kỳ ngăn xếp cuộc gọi nào (ít nhất là theo nghĩa máy của biểu thức "ngăn xếp cuộc gọi"). Bạn chỉ có thể sử dụng đống.

Vì vậy, hãy xem các phần tiếp theo và đọc thêm về kiểu chuyển tiếp tiếp tục (CPS) và chuyển đổi CPS (theo trực giác, bạn có thể sử dụng các bao đóng liên tục như các "khung gọi" được phân bổ trong heap và chúng giống như một mô phỏng cuộc gọi; sau đó bạn cần một người thu gom rác hiệu quả ).

Andrew Appel đã viết một cuốn sách Biên dịch với Continuations và một bộ sưu tập rác giấy cũ có thể nhanh hơn phân bổ ngăn xếp . Xem thêm Tài liệu của A.Kennedy (ICFP2007) Biên soạn với Continuations, Tiếp tục

Tôi cũng khuyên bạn nên đọc cuốn sách Lisp In Small Pieces của Queinnec , trong đó có một số chương liên quan đến việc tiếp tục và biên soạn.

Cũng lưu ý rằng một số ngôn ngữ (ví dụ Brainfuck ) hoặc máy trừu tượng (ví dụ OISC , RAM ) không có bất kỳ thiết bị gọi nào nhưng vẫn hoàn chỉnh Turing , vì vậy bạn không (về lý thuyết) thậm chí cần bất kỳ cơ chế gọi chức năng nào, ngay cả khi nó vô cùng tiện lợi BTW, một số kiến trúc tập lệnh cũ (ví dụ: IBM / 370 ) thậm chí không có ngăn xếp cuộc gọi phần cứng hoặc hướng dẫn máy gọi đẩy (IBM / 370 chỉ có hướng dẫn máy Chi nhánh và Liên kết )

Cuối cùng, nếu toàn bộ chương trình của bạn (bao gồm tất cả các thư viện cần thiết) không có bất kỳ đệ quy nào, bạn có thể lưu địa chỉ trả về (và các biến "cục bộ", thực sự trở thành tĩnh) của từng hàm trong các vị trí tĩnh. Các trình biên dịch Fortran77 cũ đã làm điều đó vào đầu những năm 1980 (vì vậy các chương trình được biên dịch không sử dụng bất kỳ ngăn xếp cuộc gọi nào vào thời điểm đó).


2
Điều rất đáng tranh luận là CPS không có "ngăn xếp cuộc gọi". Nó không nằm trong ngăn xếp , vùng bí ẩn của RAM thông thường có một chút hỗ trợ phần cứng thông qua %esp, v.v., nhưng nó vẫn giữ sổ sách tương đương trên ngăn xếp spaghetti được đặt tên khéo léo trong vùng RAM khác. Địa chỉ trả lại, đặc biệt, về cơ bản được mã hóa trong phần tiếp theo. Và tất nhiên, việc tiếp tục không nhanh hơn (và dường như đây là điều mà OP đang nhận được) hơn là không thực hiện bất kỳ cuộc gọi nào thông qua nội tuyến.

Các giấy tờ cũ của Appel đã tuyên bố (và được chứng minh bằng điểm chuẩn) rằng CPS có thể nhanh như có một ngăn xếp cuộc gọi.
Stilenkevitch Basile

Tôi hoài nghi về điều đó nhưng bất kể đó không phải là những gì tôi tuyên bố.

1
Trên thực tế, đây là trên máy trạm MIPS cuối những năm 1980. Có lẽ, hệ thống phân cấp bộ đệm trên các PC hiện tại sẽ làm cho hiệu suất hơi khác nhau. Đã có một số bài viết phân tích các tuyên bố của Appel (và thực tế, trên các máy hiện tại, việc phân bổ ngăn xếp có thể nhanh hơn một chút - một vài phần trăm - so với việc thu gom rác được làm cẩn thận)
Basile Starynkevitch

1
@Gilles: Nhiều lõi ARM mới hơn như Cortex M0 và M3 (và có lẽ các lõi khác như M4) có hỗ trợ ngăn xếp phần cứng cho những việc như xử lý ngắt. Hơn nữa, tập lệnh Thumb bao gồm một tập hợp con giới hạn của các lệnh STRM / STRM bao gồm STRMDB R13 với bất kỳ sự kết hợp nào của R0-R7 với / không có LR và LDRMIA R13 của bất kỳ sự kết hợp nào của R0-R7 với / không có PC, xử lý hiệu quả R13 như một con trỏ ngăn xếp.
supercat

8

Nội tuyến (thay thế các cuộc gọi chức năng bằng chức năng tương đương) hoạt động tốt như một chiến lược tối ưu hóa cho các chức năng đơn giản nhỏ. Chi phí chung của một cuộc gọi chức năng có thể được giao dịch một cách hiệu quả cho một hình phạt nhỏ trong quy mô chương trình được thêm vào (hoặc trong một số trường hợp, không có hình phạt nào cả).

Tuy nhiên, các chức năng lớn lần lượt gọi các chức năng khác có thể dẫn đến một vụ nổ lớn về quy mô chương trình nếu mọi thứ được nội tuyến.

Toàn bộ quan điểm của các chức năng có thể gọi là để tạo điều kiện sử dụng lại hiệu quả, không chỉ bởi người lập trình, mà còn bởi chính máy và bao gồm các thuộc tính như bộ nhớ hợp lý hoặc dấu chân trên đĩa.

Đối với những gì nó có giá trị: bạn có thể có các chức năng có thể gọi mà không cần ngăn xếp cuộc gọi. Ví dụ: Hệ thống IBM / 360. Khi lập trình bằng các ngôn ngữ như FORTRAN trên phần cứng đó, bộ đếm chương trình (địa chỉ trả về) sẽ được lưu vào một phần nhỏ của bộ nhớ dành riêng ngay trước điểm nhập chức năng. Nó cho phép các hàm có thể sử dụng lại, nhưng không cho phép mã đệ quy hoặc mã đa luồng (một nỗ lực trong một cuộc gọi đệ quy hoặc tái ký sẽ dẫn đến một địa chỉ trả lại được lưu trước đó bị ghi đè).

Như được giải thích bởi các câu trả lời khác, ngăn xếp là những điều tốt. Chúng tạo điều kiện cho đệ quy và các cuộc gọi đa luồng. Mặc dù bất kỳ thuật toán nào được mã hóa để sử dụng đệ quy đều có thể được mã hóa mà không cần dựa vào đệ quy, kết quả có thể phức tạp hơn, khó bảo trì hơn và có thể kém hiệu quả hơn. Tôi không chắc chắn một kiến ​​trúc không có ngăn xếp có thể hỗ trợ đa luồ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.