2 điểm bởi GN⁺ 2024-04-04 | 1 bình luận | Chia sẻ qua WhatsApp
  • Các máy tính thời kỳ đầu phải triển khai lời gọi hàm ngay cả khi không có stack và heap, và trình biên dịch quản lý trạng thái lời gọi bằng các biến toàn cục ẩn tương ứng với tham số, địa chỉ trả về và biến cục bộ
  • Bên gọi lưu đối số, đặt vị trí quay lại vào biến địa chỉ trả về, rồi nhảy đến điểm bắt đầu của hàm; sau khi tính toán, hàm nhảy trở lại địa chỉ đã lưu
  • Các biến cục bộ về mặt logic thực tế cũng dùng vùng lưu trữ toàn cục, nên dù nhìn bề ngoài giống hàm, cơ chế bên trong gần với bộ nhớ cố định và goto hơn
  • Một số ABI và bộ xử lý tối ưu hóa việc truyền đối số và xử lý địa chỉ quay lại bằng thanh ghi hoặc branch with link, nhưng các ràng buộc cơ bản vẫn còn nguyên
  • Vì địa chỉ trả về của cùng một hàm bị lời gọi mới ghi đè, không thể gọi đệ quy; các ngôn ngữ thời đó ứng phó bằng cách cấm đệ quy hoặc chỉ cho phép khi khai báo rõ ràng

Cách cấu thành lời gọi hàm khi không có stack

  • Trong môi trường máy tính thời kỳ đầu, không có stack hay heap như ngày nay chúng ta mặc nhiên coi là có
  • Cấp phát bộ nhớ động khi không có heap có thể được thay thế bằng bộ đệm kích thước cố định
    • Ngay cả khi xử lý dữ liệu có kích thước biến đổi, người ta cũng đặt trước một bộ đệm cố định đủ lớn
    • Nếu dữ liệu yêu cầu vượt quá dung lượng bộ đệm, chương trình kết thúc bằng lỗi nghiêm trọng
    • Cách triển khai thân thiện hơn cho phép thiết lập dung lượng tối đa khi biên dịch
    • Cách triển khai tinh vi hơn đặt một bộ cấp phát tùy chỉnh trên bộ đệm cố định để có thể dùng như allocatefree

Quy ước gọi dựa trên biến toàn cục ẩn

  • Để triển khai lời gọi hàm không dùng stack, trình biên dịch định nghĩa nhiều biến toàn cục ẩn cho mỗi hàm
    • Biến toàn cục cho từng tham số đầu vào
    • Biến toàn cục chứa địa chỉ trả về của hàm
    • Biến toàn cục tương ứng với các biến cục bộ
  • Mã gọi được thực thi theo trình tự sau
    • Lưu giá trị tham số vào biến toàn cục ẩn tương ứng
    • Ghi vị trí sẽ quay lại vào biến địa chỉ trả về của hàm
    • Nhảy bằng goto đến vị trí bắt đầu của hàm
  • Hàm đọc và ghi cả tham số lẫn biến cục bộ từ các biến toàn cục ẩn
  • Khi thực thi xong, hàm đặt giá trị trả về vào thanh ghi giá trị trả về, rồi nhảy đến địa chỉ được lưu trong biến địa chỉ trả về của hàm

Ví dụ mã giống C được chuyển thành mã dựa trên goto

  • Hàm ví dụ add_two_values(int a, int b) có thể được chuyển đổi thành các vùng lưu trữ sau khi không có stack
    • a2v_a, a2v_b là các biến toàn cục dùng để lưu đối số
    • a2v_c là biến toàn cục tương ứng với biến cục bộ c
    • a2v_retaddr là biến toàn cục dùng để lưu địa chỉ quay lại
  • Bên gọi sample() lưu lần lượt 314152718 vào các biến toàn cục đối số
  • Sau đó đặt vị trí resume vào a2v_retaddr và nhảy đến add_two_values
  • add_two_values lưu kết quả tính toán vào return_value_register, rồi quay lại qua a2v_retaddr
  • Bên gọi sau khi quay lại vị trí resume sẽ lưu giá trị trong thanh ghi giá trị trả về vào sample_x

Tối ưu hóa bằng thanh ghi và branch with link

  • Cùng cấu trúc này có thể được làm nhanh hơn ở cấp ABI bằng truyền qua thanh ghi
  • Nhiều bộ xử lý cung cấp một link register đặc biệt và lệnh branch with link
    • branch with link tự động lưu địa chỉ của lệnh ngay sau lệnh rẽ nhánh vào link register
    • Bên gọi có thể đặt hai đối số đầu tiên vào argument_register_1, argument_register_2
    • Hàm được gọi có thể chuyển các giá trị thanh ghi này vào biến toàn cục ẩn của chính nó để sử dụng
  • Địa chỉ trả về cũng có thể được lưu từ link_register vào biến địa chỉ trả về của hàm
  • Tối ưu hóa này vẫn giữ cấu trúc cơ bản cho phép gọi và trả về mà không cần stack

Vì sao đệ quy bị chặn

  • Ràng buộc cốt lõi của cách gọi này là không thể gọi đệ quy
  • Khi xảy ra lời gọi đệ quy, biến địa chỉ trả về của cùng một hàm bị ghi đè bằng địa chỉ trả về của lời gọi mới
  • Khi lời gọi bên ngoài kết thúc, vị trí ban đầu cần quay lại đã biến mất, khiến chương trình nhảy đến vị trí sai
  • Các ngôn ngữ lập trình thời đó tránh vấn đề này bằng cách không hỗ trợ đệ quy
  • Ban đầu FORTRAN thậm chí không hỗ trợ chương trình con; chương trình con được bổ sung vào năm 1958
  • Việc hỗ trợ đệ quy trong FORTRAN trở thành chuẩn vào năm 1991, và ngay cả khi đó cũng phải khai báo chương trình con là RECURSIVE

Mã tự sửa và lệnh chương trình con của các bộ xử lý thời kỳ đầu

  • Một số trình biên dịch dùng cách tinh vi hơn là mã tự sửa
    • Trường địa chỉ bên trong lệnh nhảy ở cuối hàm về thực chất đóng vai trò như biến địa chỉ trả về
  • Cách này không chỉ là một mẹo đơn giản mà có thể là nhu cầu thực tế
    • Một số bộ xử lý có thể không hỗ trợ nhảy gián tiếp
  • Sau khi tính hữu dụng của chương trình con được công nhận, nhiều bộ xử lý đã bổ sung lệnh gọi chuyên dụng
    • Lưu địa chỉ trả về vào word đầu tiên của chương trình con
    • Việc thực thi thực sự bắt đầu từ word thứ hai
    • Khi trả về, thực hiện nhảy gián tiếp qua nhãn bắt đầu của chương trình con
  • Trong ví dụ assembly, bsr add_two_values lưu địa chỉ trả về vào word đầu tiên của add_two_values, rồi bắt đầu thực thi từ lệnh thực sự sau nop dùng để hy sinh

1 bình luận

 
GN⁺ 2024-04-04
Các ý kiến trên Hacker News
  • Về chủ đề này, The Art of Computer Programming thật sự rất hay
    Nhìn bề ngoài có vẻ cũ kỹ, nhưng có rất nhiều thuật toán xử lý mảng hoặc cấu trúc dữ liệu thay đổi động từ thời trước khi có heap hay stack
    Cuốn sách dẫn dắt từng bước đến cả garbage collection và cách triển khai danh sách Lisp, và chứa đúng kiểu kiến thức bách khoa mà bạn kỳ vọng ở Knuth
    Ví dụ tôi đặc biệt thích là cách hai mảng chia sẻ động cùng một vùng không gian. Nếu để một mảng tăng về phía trước từ location#0, còn mảng thứ hai tăng lùi từ location#End, thì có thể chia sẻ hiệu quả vùng không gian được cấp phát tĩnh
    Cũng có thể mở rộng ra số lượng mảng tùy ý, nhưng đến mức đó thì dùng MallocRealloc có lẽ tốt hơn, và bản thân kỹ thuật ấy cũng khá gần với một routine kiểu malloc

    • Một số trình xử lý văn bản trên máy tính 8-bit hoạt động theo kiểu này. Tài liệu chiếm toàn bộ RAM khả dụng, phần văn bản trước con trỏ nằm ở đầu RAM, còn phần văn bản sau con trỏ nằm ở cuối RAM
      Chèn và dán thì không cần đẩy dữ liệu đi, nhưng khi duyệt thì cần. Dù vậy nó vẫn hoạt động tốt
    • Trong phần lớn kiến trúc tập lệnh và ABI, stack tăng từ địa chỉ cao xuống thấp, nên trong các hệ thống bộ nhớ nhỏ, đơn luồng, kỹ thuật này cho phép chia sẻ linh hoạt bộ nhớ giữa heap và stack
    • Cách cấp phát tài nguyên theo từng ứng dụng của MacOS cũ được giải thích đúng theo kiểu này. Mỗi app có yêu cầu RAM tối thiểu và RAM mong muốn, và khi chạy sẽ chiếm một slot bằng kích thước mong muốn
      Nếu không đủ như vậy thì nó nhận ít hơn mức mong muốn, còn nếu không lấy được cả mức tối thiểu thì chạy thất bại
      Tôi nhớ là hệ thống đặt heap và thư viện ở phía dưới của mảnh RAM vật lý đó, còn stack ở phía trên
      Khoảng System 8, một lớp ảo hóa được thêm vào khiến cách tiếp cận này bớt cần thiết hơn, và đến thời MacOS X thì giống các hệ thống khác, dùng bộ nhớ phân trang nên những trò khéo như vậy không còn cần nữa
      Dù vậy, nghĩ lại thời mà “một mẹo lạ” kiểu này trong Art of Computer Programming từng là cách cấp phát RAM cho nhiều app chạy đồng thời thì vẫn thấy thú vị
    • Một sự thật thú vị là Itanium có tổng cộng hai stack: một stack để push/pop thủ công và một stack khác xoay vòng qua register file
      Một cái tăng lên trên, cái kia tăng xuống dưới. Đó là một cấu trúc mê hoặc, nhưng cuối cùng không đạt được hiệu năng đã hứa hẹn
    • Định dạng đĩa của SQLite cũng dùng kỹ thuật mảng tương tự khi lưu nội dung page của nút lá B-tree cho bảng
      Trong một page kích thước cố định, mảng offset tăng về phía trước, còn mảng giá trị hàng có độ dài biến đổi thì tăng lùi từ cuối. Theo tôi hiểu, khi xóa hàng, mảng phía sau có thể xuất hiện lỗ trống
      Tài liệu có trích dẫn TAOCP về chính cấu trúc B-tree, nên nếu đây là nguồn cảm hứng trực tiếp thì cũng không có gì đáng ngạc nhiên
  • Việc đưa hàm đệ quy vào ALGOL từng gây khá nhiều tranh cãi, và vẫn là một câu chuyện thú vị: https://vanemden.wordpress.com/2014/06/18/how-recursion-got-...

  • Trình thông dịch Forth cho máy SUBLEQ (https://github.com/howerj/subleq) và trình thông dịch cho máy bit-serial (https://github.com/howerj/bit-serial) đã được viết, nhưng cả hai đều không có ngăn xếp gọi hàm cần thiết cho Forth
    SUBLEQ thậm chí không cho phép load/store gián tiếp, nên để làm bất cứ việc gì hơi phức tạp đều cần mã tự sửa đổi
    Cách tiếp cận là tạo một máy ảo có thể thực hiện các chức năng đó trên cả hai máy, rồi đưa cả cooperative multithreading vào
    Nếu cần heap thì viết bằng Forth, và cả tập word dấu phẩy động cũng viết bằng Forth. Nhiều MCU vẫn không có lệnh dấu phẩy động, và có thể xử lý bằng các lời gọi hàm phần mềm hiện thực chúng
    Các trình biên dịch khác không được nhắc đến, nhưng có lẽ cũng dùng cách tương tự. Một số trình thông dịch BASIC cũng hiện thực VM rồi nhắm tới nó, và P-Code cũng tương tự

    • TI-99/4A chỉ có 256 byte RAM chính mà CPU có thể truy cập trực tiếp, tức chỉ 128 word
      Phần lớn bộ nhớ hệ thống cơ bản là video RAM, và phải truy cập bằng một quy trình khá rườm rà là poke/peek các thanh ghi của chip video
      Chip video duy trì một con trỏ bộ nhớ hiện tại tự tăng, nên khi đọc hoặc ghi liên tiếp thì con trỏ tăng thêm 1, nhưng bản thân việc phần lớn bộ nhớ hệ thống chỉ có thể truy cập theo cách này đã khiến việc viết chương trình lớn trở nên khó khăn
      Vì vậy TI đã tạo ra một máy trừu tượng tên là GPL để làm cho việc truy cập video RAM này tự nhiên hơn. Tuy nhiên, vì nó được thông dịch chạy trên TMS9900 nên chậm hơn mã native, và CPU cũng chỉ có thể truy cập RAM của chip video vào những thời điểm chip không scan-out màn hình, chẳng hạn trong khoảng hồi ngang/dọc, nên còn chậm hơn nữa
      Mã BASIC và biến cũng đều nằm trong bộ nhớ video này, nên việc trình thông dịch BASIC của TI-99/4A được viết bằng gì cũng khá rõ. Nó hoàn toàn không nhanh
      Điểm thú vị là TMS9900 không có thanh ghi đa dụng thực sự. Các thanh ghi workspace WR0~WR15 nằm đâu đó trong bộ nhớ, và thanh ghi con trỏ workspace WP trỏ tới chúng
      Các thanh ghi vật lý của CPU chỉ có ba cái: PC, WP và thanh ghi trạng thái. Kết quả là có thể thực hiện một dạng register windowing rất sơ khai; khi rẽ nhánh bằng lệnh BLWP, một tập “thanh ghi” mới ở vị trí khác trong bộ nhớ được kích hoạt, và địa chỉ trả về được lưu trong workspace mới
      Dạo này tôi hay nói về TI-99/4A vì đang làm một dự án cá nhân là viết assembler cho dòng máy này
    • Tôi thấy những công trình đó khi đang học và đào sâu vào Forth và Subleq. Tôi thích đọc về cách tiếp cận này và muốn mua sách, nhưng Amazon nói là không được. Không biết có tái bản không
    • Tôi định nói về subleq, nhưng ở đó chỉ viết một “Hello world” thôi cũng thật sự khó
  • Nói rằng một số bộ xử lý lưu địa chỉ trả về vào word ngay trước lệnh đầu tiên của subroutine là đúng, và PDP-8 đã làm như vậy
    Sự tiến hóa của PDP-8 cũng có thể xem như hành trình hỗ trợ phần cứng cho đệ quy
    Ban đầu, lệnh JMS nhét địa chỉ trả về vào word đầu tiên của hàm. Cũng thường gặp trường hợp caller đặt tham số sau lệnh JMS, còn callee đọc tham số theo offset so với lệnh trả về, đồng thời tăng dần mỗi lần để địa chỉ trả về lại trỏ về vị trí mã
    Về sau, cách dùng một trong các vị trí tự tăng để tạo một ngăn xếp đơn giản trở nên khá phổ biến. PDP-8 có 8 vị trí bộ nhớ tự tăng mỗi khi được dùng làm con trỏ, và prologue/epilogue của hàm trực tiếp quản lý ngăn xếp này, cho phép đệ quy hoàn chỉnh
    Muộn hơn nữa, các hiện thực vi xử lý như Harris 6120 được bổ sung stack phần cứng, cải thiện hiệu năng

    • Librascope LGP-30 năm 1956 có lệnh R, tức lệnh lưu địa chỉ trả về
      Lệnh này lưu PC+1 đã được tăng sẵn vào phần địa chỉ lệnh của vị trí đích, và theo quy ước, đích đó là lệnh rẽ nhánh vô điều kiện ngay trước điểm bắt đầu subroutine
      Sau lệnh R là lệnh rẽ nhánh vô điều kiện U để đi tới subroutine đó
      Subroutine trả về bằng cách rẽ nhánh tới địa chỉ ngay trước nó, nơi chứa một lệnh rẽ nhánh vô điều kiện quay lại ngay sau điểm gọi
      Trừ khi dùng một quy ước gọi nâng cao hơn, đệ quy là không thể. Và mọi mã lệnh trong assembly language đều chỉ là một chữ cái
    • IBM 1800, IBM 1130 và nhiều máy cùng thời cũng như vậy. Những máy có đủ thanh ghi, như dòng Xerox Sigma, có thể tránh được tập quán này
  • Trong các chương trình viết cho AVR-8, dùng quy ước gọi C đôi khi cảm giác như điên rồ
    Nếu viết assembly, bạn có thể giữ các biến vòng lặp nội bộ trong file thanh ghi lớn, hoặc dùng các cách được mô tả trong bài
    Cách “tô màu” hàm trong những ứng dụng như vậy cũng hay. Nếu biết hàm đỏ và hàm xanh không bao giờ hoạt động đồng thời, bạn có thể tái sử dụng biến cục bộ hoặc tham số của chúng

    • Khi làm việc trong môi trường hạn chế, đặc biệt nếu đã quen với sự tiện lợi của hệ điều hành desktop, mức dùng stack của C có thể không trực quan
      Trong một dự án codebase vi điều khiển mà tôi từng tham gia, nhiều lập trình viên đã mất vài tuần truy vết các lỗi khó bắt ở nhiều subsystem
      Khi di chuyển mã, lỗi cũng di chuyển theo. Sau khi truy vết một chút và đặt bẫy, có thể tìm ra những vị trí mã mà call stack trở nên quá sâu và ghi đè lên các cấu trúc dữ liệu khác
  • Khi mới học lập trình, tôi đã bị buộc phải lập trình đúng theo kiểu này. Không phải thập niên 1970, mà là năm 2001
    Bởi trải nghiệm lập trình đầu tiên của tôi là “ngôn ngữ” scripting bán đồ họa do công cụ phát triển game RPG Maker 2000 cung cấp
    Nếu chưa từng thấy scripting của RM2K, hãy tưởng tượng một thứ pha trộn giữa Scratch và chế độ Emacs Paredit. Ví dụ: https://forums.rpgmakerweb.com/data/attachments/21/21958-f89...
    Trông giống văn bản, nhưng không thể chỉnh sửa như văn bản; chỉ có thể chỉnh sửa dưới dạng các khối kèm hộp thoại thuộc tính
    Dĩ nhiên ngôn ngữ scripting của RPG Maker cũng chẳng có thứ xịn sò như stack. Nếu cần một subroutine có thể tái sử dụng, bạn phải cấp phát các biến toàn cục bí mật để làm tham số, và không có tính reentrant
    Nhìn lại thì nếu đủ cố chấp, có lẽ đã có thể triển khai cả register lẫn runtime stack bên trong RPG Maker 2000
    Ban đầu nghe có vẻ dễ. Có thể tạo các “register” giả kiểu zero page của 6502, và cũng có thể tạo stack bằng truy cập biến gián tiếp (https://rpgmaker.net/tutorials/523/)
    Vấn đề là RM2K có tính đồng thời dưới dạng script “parallel process”. Nếu các parallel process dùng những trừu tượng này, các “thread” khác nhau sẽ ghi đè trạng thái của nhau lung tung
    Vì vậy cần nhiều zero page và stack cho mỗi “lõi ảo”, rồi phải cấp phát/gắn/schedule lõi ảo cho từng script song song. Nói cách khác, phải bằng cách nào đó khiến mỗi script có một stack pointer mà chỉ nó biết
    Muốn ổn định trước race condition thì thường cần thứ gì đó như mutex
    Nghĩ đến độ dai dẳng của các nhà phát triển game RPG Maker, tôi đoán hẳn đã có ai đó tìm ra cách đánh lừa một tính năng runtime nào đó để nó hoạt động như mutex, nhưng thật lòng tôi sợ đến mức không muốn biết họ thực sự đã làm gì

    • Tôi cũng bắt đầu với rpgmaker, và câu chuyện này thật sự gợi nhiều hoài niệm
      Tôi nhớ từng tải một game trên rpgmaker.net có triển khai custom battle system. Đó là một bản triển khai thay thế toàn bộ hệ thống chiến đấu tích hợp bằng những kỹ thuật giống như bạn mô tả
      Khi mở nó trong editor để xem cách hoạt động, tôi hoàn toàn choáng ngợp. Có hàng trăm “biến”, và nếu nhớ không nhầm thì chỉ cho phép i64, cùng hàng trăm “switch” nữa. Switch là boolean
      Khi đó tôi hoàn toàn chưa có khái niệm gì về stack, heap hay lời gọi hàm
      Tôi không thể tưởng tượng nổi đã phải bỏ ra bao nhiêu năng lượng để tạo ra rồi bảo trì/debug thứ đó
  • Nếu tôi nhớ đúng, khi viết chương trình BASIC trên ZX81, tôi đã viết theo kiểu gần như “không có stack”
    1 GOTO 30
    10 LET C = A + B
    20 RETURN
    30 LET A = 1
    40 LET B = 2
    50 GOSUB 10
    60 LET A = C
    70 LET B = 3
    80 GOSUB 10
    90 PRINT C
    RUN
    6
    Nói cách khác, tôi đang tự làm công việc mà bài viết nói là compiler làm. Số dòng là địa chỉ bộ nhớ, còn các biến ẩn thì không hề ẩn với tôi. Vì chính tôi là compiler
    Việc duy nhất interpreter làm cho tôi là lưu địa chỉ trả về của GOSUB
    Tuy vậy, đoạn code có thể sai cú pháp hoặc trí nhớ của tôi có thể bị méo mó. 40 năm là một quãng thời gian dài, nhưng ý tưởng tổng quát thì đúng
    Ngoài ra, bộ xử lý Z80 trong máy có chức năng quản lý stack. BASIC interpreter thật sự rất đơn giản, nhưng cũng có lý do bào chữa: chỉ có 1KB RAM và 8KB ROM chứa OS, interpreter, mọi thứ

    • Thứ đó ít nhất vẫn dùng call stack. GOSUB lưu số dòng hoặc một tham chiếu khác để RETURN tham chiếu tới, và nếu lồng các lời gọi GOSUB thì phải nhớ nhiều điểm trả về, nên cần một dạng stack nào đó
      Tuy nhiên trong một số BASIC, thay vì stack tổng quát, chỉ có một mảng cố định các con trỏ trả về và một chỉ số vị trí hiện tại, chẳng hạn độ sâu lời gọi bị cố định là 7. Với lập trình viên thì nó hoạt động như call stack
      Tất nhiên đó không phải là stack “đúng nghĩa” có biến cục bộ/tham số như người ta thường kỳ vọng khi nói đến stack
      Trong môi trường mặc định của BBC BASIC, có thể làm một demo thú vị cho thấy chuyện gì xảy ra khi gọi lồng nhau, kể cả đệ quy. Nếu đặt vị trí stack ở đỉnh bộ nhớ hiển thị và không vẽ gì ở đó, bạn có thể thấy stack lớn dần khi chương trình chạy
      Vì độ phân giải màn hình thấp, địa chỉ trả về 2 byte hiện thành 8 pixel to ở chế độ màn hình 1 hoặc 5. Ở chế độ 2 thì là 4 pixel nhưng có màu nhấp nháy nên kém hay hơn; còn ở chế độ 0, 3, 4, 6 thì là 16 pixel, nhưng nhìn ở mức bit khó nhận ra hơn so với mẫu lặp 8 màu
  • Trước khi có heap có thể mở rộng tùy ý, lập trình viên ít nhất cũng phải dùng một chút phán đoán kỹ thuật
    Vì họ phải xét đến phân bố xác suất của đầu vào và định cỡ phù hợp cho mọi vùng lưu trữ trung gian
    Vì thế mới có mục “BUGS AND LIMITATIONS”

    • Những cách làm cũ như vậy, tùy bạn đang làm gì, đến nay vẫn ở thì hiện tại. Trong hard real-time, người ta hầu như không dùng bộ nhớ động, chủ yếu vì thời gian cấp phát/giải phóng bộ nhớ không có tính quyết định
      Vì vậy mọi thứ được cấp phát tĩnh tại thời điểm biên dịch, và cần biết đầu vào sẽ tiêu thụ bao nhiêu bộ nhớ
      Nhưng việc biết cận trên của mức tiêu thụ bộ nhớ từng là điều bình thường ngay cả với lập trình viên ứng dụng. Vì chẳng ai muốn hết bộ nhớ cả
      Có vẻ ngày nay người ta cứ để mức dùng bộ nhớ theo kiểu YOLO
    • Trong lịch sử, một trong những mục tiêu lớn của GNU cũng là chuyện đó. Họ muốn loại bỏ các giới hạn nhân tạo trong các tiện ích cốt lõi
      Chẳng hạn đó là một cải thiện lớn so với các hạn chế kiểu độ dài lệnh tối đa của sed là hữu hạn và ngắn
    • Thật ra sai lầm nằm ở việc để con người cung cấp đầu vào cho chương trình máy tính
  • Tôi đã lập trình hàm quá lâu đến mức thật sự khó hình dung phải viết mã thế nào mà không dùng đệ quy
    Về mặt kỹ thuật, tôi biết cách chuyển thuật toán đệ quy thành thuật toán lặp, và cũng từng làm việc đó ở những nơi có ràng buộc tài nguyên lớn, nhưng tôi không thích
    Thường thì cách đệ quy đẹp hơn, và tôi cho rằng trong 99% trường hợp là đủ nhanh. Nếu trình biên dịch hỗ trợ đệ quy đuôi thì gần như 100%, nhưng với phần lớn các tác vụ thú vị hơn, dù sao cũng phải tự duy trì stack
    Thỉnh thoảng tôi cố ý làm những việc như vậy để học xem trước khi tôi ra đời người ta đã làm thế nào. Tôi thỉnh thoảng mày mò game Commodore 64, và cảm nhận rất rõ rằng việc giờ đây quen với phần cứng nhanh, rẻ và dễ dùng là một sự xa xỉ đến mức nào

    • Tập lệnh ngày nay chắc chắn hữu dụng hơn rất nhiều
      Muốn dùng đệ quy trên những cỗ máy cũ như vậy thì phải tự tạo cơ chế stack, mà ngay cả thế vẫn còn vấn đề phải xử lý vì về cơ bản không có cách nào dùng được ngoài lưu trữ toàn cục
      Tôi đã sống qua thời đó, nhưng không muốn khuyên ai trải nghiệm cả
  • Trong tính năng @let của Enhanced GNU Awk, các khối @let bên ngoài hàm, chẳng hạn trong khối BEGIN hoặc END, được để cho trình biên dịch cấp phát biến toàn cục bí mật
    Các biến này được tái sử dụng giữa các khối nhiều nhất có thể
    $ ./gawk --dump-variables 'BEGIN { @let (a, b, c = 1) { } }'
    $ cat awkvars.out
    $let0001: untyped variable
    $let0002: untyped variable
    $let0003: 1
    ARGC: 1
    ARGIND: 0
    ARGV: array, 1 elements
    BINMODE: 0
    [ .. snip many ]
    https://www.kylheku.com/cgit/egawk/about/

    • Trang web đó không hoạt động từ ISP của tôi. ping không được, mà nc -z 104.37.63.7 443 cũng không được
      Cập nhật: có vẻ hạ tầng bảo mật bị hỏng. Tôi cũng không biết đó là gì và cũng không dùng Twitter. Kiểm tra AS thì là Google Fiber
      Và tôi mong mọi người đừng doxx tôi