Đâu là lựa chọn thay thế cho việc sử dụng ngăn xếp để biểu diễn ngữ nghĩa của hàm gọi?


19

Chúng ta đều biết và yêu thích rằng các lệnh gọi hàm thường được thực hiện bằng cách sử dụng ngăn xếp; Có khung, địa chỉ trả về, tham số, toàn bộ lô.

Tuy nhiên, ngăn xếp là một chi tiết triển khai: các quy ước gọi có thể thực hiện những việc khác nhau (ví dụ x86 fastcall sử dụng (một số) thanh ghi, MIPS và người theo dõi sử dụng cửa sổ đăng ký, v.v.) và tối ưu hóa có thể thực hiện ngay cả những việc khác (nội tuyến, bỏ sót con trỏ khung, tối ưu hóa cuộc gọi đuôi ..).

Chắc chắn, sự hiện diện của hướng dẫn ngăn xếp thuận tiện trên nhiều máy (VM như JVM và CLR, nhưng cả các máy thực như x86 với PUSH / POP, v.v.) giúp thuận tiện sử dụng cho các cuộc gọi chức năng, nhưng trong một số trường hợp có thể để lập trình theo cách không cần đến ngăn xếp cuộc gọi (Tôi đang nghĩ về Phong cách chuyển tiếp liên tục ở đây hoặc Diễn viên trong hệ thống chuyển tin nhắn)

Vì vậy, tôi đã bắt đầu tự hỏi: có thể thực hiện chức năng gọi ngữ nghĩa mà không cần ngăn xếp hay tốt hơn là sử dụng cấu trúc dữ liệu khác (hàng đợi, có lẽ hoặc bản đồ liên kết?)
Tất nhiên, tôi hiểu rằng một ngăn xếp rất thuận tiện (có một lý do tại sao nó có mặt ở khắp nơi) nhưng gần đây tôi tình cờ gặp một triển khai khiến tôi tự hỏi ..

Có ai trong số các bạn biết nếu nó đã từng được thực hiện trong bất kỳ ngôn ngữ / máy / máy ảo nào không, và nếu có thì đó là những khác biệt và thiếu sót nổi bật?

EDIT: Cảm giác ruột của tôi là các phương pháp tính toán phụ khác nhau có thể sử dụng các cấu trúc dữ liệu khác nhau. Ví dụ, tính toán lambda không dựa trên ngăn xếp (ý tưởng về ứng dụng hàm được thu thập bằng cách giảm) nhưng tôi đã xem xét một ngôn ngữ / máy / ví dụ thực tế. Đó là lý do tại sao tôi hỏi ...


Clean sử dụng biểu đồ và máy viết lại biểu đồ, lần lượt được triển khai bằng máy ba ngăn, nhưng có nội dung khác với thông thường.

Đối với các máy ảo, một danh sách liên kết có thể được sử dụng. Mỗi nút của danh sách là một khung. Do ngăn xếp phần cứng được VM sử dụng, điều này cho phép các khung tồn tại trên heap mà không cần chi phí hoạt động realloc().
Shawnhcorey

Câu trả lời:


19

Tùy thuộc vào ngôn ngữ, có thể không cần sử dụng ngăn xếp cuộc gọi. Ngăn xếp cuộc gọi chỉ cần thiết trong các ngôn ngữ cho phép đệ quy hoặc đệ quy lẫn nhau. Nếu ngôn ngữ không cho phép đệ quy, thì chỉ một lệnh gọi của bất kỳ thủ tục nào có thể được kích hoạt bất cứ lúc nào và các biến cục bộ cho thủ tục đó có thể được phân bổ tĩnh. Các ngôn ngữ như vậy phải cung cấp cho việc thay đổi ngữ cảnh, để xử lý ngắt, nhưng điều này vẫn không yêu cầu ngăn xếp.

Tham khảo FORTRAN IV (và trước đó) và các phiên bản đầu tiên của COBOL để biết ví dụ về các ngôn ngữ không yêu cầu ngăn xếp cuộc gọi.

Tham khảo Dữ liệu điều khiển 6600 (và các máy Dữ liệu điều khiển trước đó) để biết ví dụ về siêu máy tính sớm rất thành công không hỗ trợ phần cứng trực tiếp cho ngăn xếp cuộc gọi. Tham khảo PDP-8 để biết ví dụ về một máy tính mini đầu tiên rất thành công không hỗ trợ ngăn xếp cuộc gọi.

Theo như tôi biết, các máy xếp chồng B5000 của Burroughs là những máy đầu tiên có ngăn xếp cuộc gọi phần cứng. Các máy B5000 được thiết kế từ đầu để chạy ALGOL, cần phải đệ quy. Họ cũng có một trong những kiến ​​trúc dựa trên mô tả đầu tiên, đặt nền móng cho kiến ​​trúc khả năng.

Theo như tôi biết, chính PDP-6 (đã phát triển thành DEC-10) đã phổ biến phần cứng ngăn xếp cuộc gọi, khi cộng đồng hacker tại MIT nhận giao hàng và phát hiện ra rằng hoạt động của PUSHJ (Đẩy trả lại địa chỉ và nhảy) cho phép giảm thói quen in thập phân từ 50 hướng dẫn xuống 10.

Hàm cơ bản nhất gọi ngữ nghĩa trong một ngôn ngữ cho phép đệ quy yêu cầu các khả năng phù hợp độc đáo với một ngăn xếp. Nếu đó là tất cả những gì bạn cần, thì một ngăn xếp cơ bản là một kết hợp đơn giản, tốt. Nếu bạn cần nhiều hơn thế, thì cấu trúc dữ liệu của bạn phải làm nhiều hơn.

Ví dụ tốt nhất về việc cần nhiều hơn mà tôi đã gặp là "sự tiếp tục", khả năng đình chỉ một tính toán ở giữa, lưu nó dưới dạng bong bóng trạng thái đóng băng và bắn lại sau đó, có thể nhiều lần. Continuations trở nên phổ biến trong phương ngữ SchIS của LISP, như một cách để thực hiện, trong số những thứ khác, thoát lỗi. Việc tiếp tục yêu cầu khả năng chụp nhanh môi trường thực thi hiện tại và tái tạo nó sau đó, và một ngăn xếp có phần bất tiện cho điều đó.

"Cấu trúc và giải thích các chương trình máy tính" của Abelson & Sussman đi sâu vào một số chi tiết về các phần tiếp theo.


2
Đó là một cái nhìn sâu sắc lịch sử, cảm ơn bạn! Khi tôi hỏi câu hỏi của mình, tôi thực sự có những suy nghĩ liên tục, đặc biệt là phong cách tiếp tục (CPS). Trong trường hợp đó, một ngăn xếp không chỉ bất tiện, mà có lẽ không cần thiết: bạn không cần phải nhớ nơi để trở về, bạn cung cấp một điểm để tiếp tục thực hiện. Tôi tự hỏi nếu các cách tiếp cận không chồng khác phổ biến, và bạn đã đưa ra một số cách tiếp cận rất tốt mà tôi không biết.
Lorenzo Dematté

Hơi liên quan: bạn chỉ ra chính xác "nếu ngôn ngữ không cho phép đệ quy". Điều gì về ngôn ngữ với đệ quy, trong các chức năng cụ thể không phải là đệ quy đuôi? Họ có cần một ngăn xếp "theo thiết kế" không?
Lorenzo Dematté

"Ngăn xếp cuộc gọi chỉ cần thiết trong các ngôn ngữ cho phép đệ quy hoặc đệ quy lẫn nhau" - không. Nếu một hàm có thể được gọi từ nhiều nơi (ví dụ cả hai foobarcó thể gọi baz), thì hàm đó cần biết những gì cần trả về. Nếu bạn lồng thông tin "ai trở về" này, thì bạn sẽ có một ngăn xếp. Không quan trọng bạn gọi nó là gì hay nếu nó được hỗ trợ bởi phần cứng của CPU hoặc thứ gì đó bạn mô phỏng trong phần mềm (hoặc ngay cả khi đó là danh sách được liên kết của các mục được phân bổ tĩnh), thì đó vẫn là một ngăn xếp.
Brendan

@Brendan không nhất thiết (ít nhất, đó là toàn bộ mục đích câu hỏi của tôi). "Nơi để trở về" hoặc tốt hơn "đi đâu tiếp theo" có cần phải là một ngăn xếp, tức là cấu trúc LIFO không? Nó có thể là một cái cây, một bản đồ, một hàng đợi hay cái gì khác?
Lorenzo Dematté

Ví dụ, cảm xúc ruột của tôi là CPS chỉ cần một cái cây, nhưng tôi không chắc, tôi cũng không biết nhìn vào đâu. Đó là lý do tại sao tôi hỏi ..
Lorenzo Dematté

6

Không thể thực hiện ngữ nghĩa gọi hàm mà không sử dụng một số loại ngăn xếp. Bạn chỉ có thể chơi các trò chơi chữ (ví dụ: sử dụng một tên khác cho nó, như "bộ đệm trả về FILO").

Có thể sử dụng một cái gì đó không thực hiện chức năng gọi ngữ nghĩa (ví dụ: kiểu truyền tiếp tục, các tác nhân), và sau đó xây dựng ngữ nghĩa gọi hàm trên đầu trang; nhưng điều này có nghĩa là thêm một số loại cấu trúc dữ liệu để theo dõi nơi điều khiển được truyền khi hàm trả về và cấu trúc dữ liệu đó sẽ là một loại ngăn xếp (hoặc ngăn xếp có tên / mô tả khác).

Hãy tưởng tượng bạn có nhiều chức năng mà tất cả có thể gọi nhau. Trong thời gian chạy, mỗi chức năng phải biết trở về nơi khi chức năng thoát. Nếu firstcuộc gọi secondthì bạn có:

second returns to somewhere in first

Sau đó, nếu bạn secondgọi third:

third returns to somewhere in second
second returns to somewhere in first

Sau đó, nếu bạn thirdgọi fourth:

fourth returns to somewhere in third
third returns to somewhere in second
second returns to somewhere in first

Khi mỗi chức năng được gọi, nhiều thông tin "nơi trở về" phải được lưu trữ ở đâu đó.

Nếu một hàm trả về, thì thông tin "nơi trả về" của nó được sử dụng và không còn cần thiết nữa. Ví dụ: nếu fourthquay trở lại một nơi nào thirdđó thì lượng thông tin "nơi quay lại" sẽ trở thành:

third returns to somewhere in second
second returns to somewhere in first

Về cơ bản; "ngữ nghĩa gọi hàm" ngụ ý rằng:

  • bạn phải có thông tin "nơi để trở về"
  • lượng thông tin tăng lên khi các hàm được gọi và giảm đi khi các hàm quay trở lại
  • phần đầu tiên của thông tin "nơi trở về" được lưu trữ sẽ là phần cuối cùng của thông tin "nơi trả lại" bị loại bỏ

Điều này mô tả bộ đệm FILO / LIFO hoặc ngăn xếp.

Nếu bạn cố gắng sử dụng một loại cây, thì mỗi nút trong cây sẽ không bao giờ có nhiều hơn một con. Lưu ý: một nút có nhiều con chỉ có thể xảy ra nếu một hàm gọi 2 hoặc nhiều hàm cùng một lúc , yêu cầu một số loại đồng thời (ví dụ: luồng, fork (), v.v.) và nó sẽ không phải là "ngữ nghĩa gọi hàm". Nếu mỗi nút trong cây sẽ không bao giờ có nhiều hơn một con; sau đó "cây" đó sẽ chỉ được sử dụng làm bộ đệm FILO / LIFO hoặc ngăn xếp; và bởi vì nó chỉ được sử dụng làm bộ đệm FILO / LIFO hoặc một ngăn xếp nên công bằng khi cho rằng "cây" là một ngăn xếp (và sự khác biệt duy nhất là trò chơi chữ và / hoặc chi tiết triển khai).

Điều tương tự cũng áp dụng cho bất kỳ cấu trúc dữ liệu nào khác có thể được sử dụng để thực hiện "ngữ nghĩa gọi hàm" - nó sẽ được sử dụng như một ngăn xếp (và sự khác biệt duy nhất là trò chơi chữ và / hoặc chi tiết triển khai); trừ khi nó phá vỡ "chức năng gọi ngữ nghĩa". Lưu ý: Tôi sẽ cung cấp các ví dụ cho các cấu trúc dữ liệu khác nếu tôi có thể, nhưng tôi không thể nghĩ ra bất kỳ cấu trúc nào khác hơi hợp lý.

Tất nhiên làm thế nào một ngăn xếp được thực hiện là một chi tiết thực hiện. Nó có thể là một vùng bộ nhớ (nơi bạn theo dõi "đỉnh ngăn xếp hiện tại"), nó có thể là một loại danh sách được liên kết (nơi bạn theo dõi "mục hiện tại trong danh sách") hoặc có thể được thực hiện trong một số cách khác. Nó cũng không quan trọng nếu phần cứng có hỗ trợ tích hợp hay không.

Lưu ý: Nếu chỉ một lời mời của bất kỳ thủ tục nào có thể được kích hoạt bất cứ lúc nào; sau đó bạn có thể phân bổ tĩnh không gian cho thông tin "nơi quay lại". Đây vẫn là một ngăn xếp (ví dụ: một danh sách được liên kết của các mục được phân bổ tĩnh được sử dụng theo cách FILO / LIFO).

Cũng lưu ý rằng có một số điều không tuân theo "ngữ nghĩa gọi hàm". Những điều này bao gồm "ngữ nghĩa có khả năng rất khác nhau" (ví dụ: tiếp tục vượt qua, mô hình diễn viên); và cũng bao gồm các phần mở rộng phổ biến cho "ngữ nghĩa gọi hàm" như đồng thời (luồng, sợi, bất cứ thứ gì), setjmp/ longjmp, xử lý ngoại lệ, v.v.


Theo định nghĩa, ngăn xếp là một bộ sưu tập LIFO: Cuối cùng, trước hết. Một hàng đợi là một bộ sưu tập FIFO.
John R. Strohm

Vì vậy, một ngăn xếp là cấu trúc dữ liệu duy nhất được chấp nhận? Nếu vậy, tại sao?
Lorenzo Dematté

@ JohnR.Strohm: Đã sửa :-)
Brendan

1
Đối với các ngôn ngữ không có đệ quy (trực tiếp hoặc đột biến), có thể phân bổ tĩnh cho mỗi phương thức một biến sẽ xác định vị trí mà phương thức đó được gọi lần cuối. Nếu trình liên kết nhận thức được những điều như vậy, nó có thể phân bổ các biến như vậy theo cách không tệ hơn những gì một ngăn xếp sẽ làm nếu mọi đường dẫn thực thi tĩnh có thể được thực hiện .
supercat

4

Ngôn ngữ kết hợp đồ chơi XY sử dụng hàng đợi cuộc gọi và ngăn xếp dữ liệu để thực hiện.

Mỗi bước tính toán chỉ đơn giản liên quan đến việc sắp xếp từ tiếp theo sẽ được thực thi và trong trường hợp nội dung, cung cấp cho chức năng bên trong của ngăn xếp dữ liệu và hàng đợi cuộc gọi dưới dạng đối số hoặc với userdefs, đẩy các từ ghép thành từ trước hàng đợi.

Vì vậy, nếu chúng ta có một chức năng để nhân đôi yếu tố hàng đầu:

; double dup + ;
// defines 'double' to be composed of 'dup' followed by '+'
// dup duplicates the top element of the data stack
// + pops the top two elements and push their sum

Sau đó, các hàm soạn thảo +dupcó các chữ ký kiểu ngăn xếp / hàng đợi sau đây:

// X is arbitraty stack, Y is arbitrary queue, ^ is concatenation
+      [X^a^b Y] -> [X^(a + b) Y]
dup    [X^a Y] -> [X^a^a Y]

Và nghịch lý thay, doublesẽ như thế này:

double [X Y] -> [X dup^+^Y]

Vì vậy, theo một nghĩa nào đó, XY là không có chồng.


Ồ cảm ơn nhé! Tôi sẽ xem xét điều đó ... không chắc nó thực sự áp dụng cho các cuộc gọi chức năng nhưng dù sao cũng đáng xem
Lorenzo Dematté

1
@ Karl Damgaard Asmussen "đẩy các từ soạn nó ra phía trước hàng đợi" "đẩy phía trước" Không phải là một chồng sao?

@ guesttttttt222222222 không thực sự. Một Callstack lưu trữ các con trỏ trả về và khi hàm trả về, callstack được bật lên. Hàng đợi thực thi chỉ lưu trữ các con trỏ tới các hàm và khi thực hiện hàm tiếp theo, nó được mở rộng thành định nghĩa của nó và được đẩy lên phía trước hàng đợi. Trong XY, hàng đợi thực thi thực sự là một deque, vì có các hoạt động cũng hoạt động ở mặt sau của hàng đợi thực thi.
Karl Damgaard Asmussen
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.