การเรียกซับรูทีนในโลกยุคโบราณ: ก่อนที่คอมพิวเตอร์จะมีสแต็กหรือฮีป
(devblogs.microsoft.com)- คอมพิวเตอร์ยุคแรกต้องทำให้การเรียกฟังก์ชันทำงานได้ แม้ไม่มีสแต็กและฮีป และคอมไพเลอร์จัดการสถานะการเรียกด้วยตัวแปรโกลบอลแฝงที่สอดคล้องกับพารามิเตอร์ ที่อยู่สำหรับคืนกลับ และตัวแปรโลคัล
- ผู้เรียกจะเก็บอาร์กิวเมนต์ ใส่ตำแหน่งที่จะกลับไปไว้ใน ตัวแปรที่อยู่สำหรับคืนกลับ แล้วกระโดดไปยังจุดเริ่มต้นของฟังก์ชัน จากนั้นฟังก์ชันคำนวณเสร็จแล้วกระโดดกลับไปยังที่อยู่ที่เก็บไว้
- แม้ตัวแปรโลคัลในเชิงตรรกะจะดูเหมือนเป็นของฟังก์ชัน แต่จริง ๆ แล้วใช้พื้นที่เก็บข้อมูลแบบโกลบอล ดังนั้นภายนอกดูเหมือนฟังก์ชัน แต่การทำงานภายในใกล้เคียงกับหน่วยความจำแบบคงที่และ
goto - ABI และโปรเซสเซอร์บางส่วนปรับให้การส่งอาร์กิวเมนต์และการจัดการที่อยู่สำหรับคืนกลับเร็วขึ้นด้วย รีจิสเตอร์ หรือ
branch with linkแต่ข้อจำกัดพื้นฐานยังคงอยู่ - เนื่องจากที่อยู่สำหรับคืนกลับของฟังก์ชันเดียวกันจะถูกการเรียกใหม่เขียนทับ จึงไม่สามารถเรียกแบบรีเคอร์ซีฟได้ และภาษาสมัยนั้นรับมือด้วยการห้ามรีเคอร์ชันหรืออนุญาตเฉพาะเมื่อระบุอย่างชัดเจน
วิธีประกอบการเรียกฟังก์ชันโดยไม่มีสแต็ก
- ในสภาพแวดล้อมคอมพิวเตอร์ยุคแรก ไม่มี สแต็ก หรือ ฮีป อย่างที่เราถือว่าเป็นเรื่องปกติในปัจจุบัน
- การจัดสรรหน่วยความจำแบบไดนามิกที่ไม่มีฮีปสามารถแทนด้วยบัฟเฟอร์ขนาดคงที่ได้
- แม้เมื่อต้องจัดการข้อมูลขนาดแปรผัน ก็จองบัฟเฟอร์คงที่ที่ใหญ่พอไว้ล่วงหน้า
- หากข้อมูลที่ร้องขอเกินความจุของบัฟเฟอร์ โปรแกรมจะจบการทำงานด้วยข้อผิดพลาดร้ายแรง
- การใช้งานที่เป็นมิตรกว่านั้นเปิดให้ตั้งค่าความจุสูงสุดตอนคอมไพล์ได้
- การใช้งานที่ซับซ้อนกว่านั้นวางตัวจัดสรรแบบกำหนดเองบนบัฟเฟอร์คงที่ และทำให้ใช้งานได้คล้าย
allocateและfree
ข้อตกลงการเรียกที่อิงตัวแปรโกลบอลแฝง
- เพื่อทำให้การเรียกฟังก์ชันทำงานได้โดยไม่มีสแต็ก คอมไพเลอร์จะนิยาม ตัวแปรโกลบอลแฝง หลายตัวสำหรับแต่ละฟังก์ชัน
- ตัวแปรโกลบอลสำหรับพารามิเตอร์อินพุตแต่ละตัว
- ตัวแปรโกลบอลที่เก็บที่อยู่สำหรับคืนกลับของฟังก์ชัน
- ตัวแปรโกลบอลที่สอดคล้องกับตัวแปรโลคัล
- โค้ดฝั่งผู้เรียกจะทำงานตามลำดับต่อไปนี้
- เก็บค่าพารามิเตอร์ลงในตัวแปรโกลบอลแฝงที่เกี่ยวข้อง
- บันทึกตำแหน่งที่จะกลับไปไว้ในตัวแปรที่อยู่สำหรับคืนกลับของฟังก์ชัน
- กระโดดด้วย
gotoไปยังตำแหน่งเริ่มต้นของฟังก์ชัน
- ฟังก์ชันจะอ่านและเขียนทั้งพารามิเตอร์และตัวแปรโลคัลจากตัวแปรโกลบอลแฝง
- เมื่อทำงานเสร็จ จะใส่ค่าที่คืนกลับลงในรีจิสเตอร์ค่าคืนกลับ แล้วกระโดดไปยังที่อยู่ที่เก็บไว้ในตัวแปรที่อยู่สำหรับคืนกลับของฟังก์ชัน
ตัวอย่างโค้ดคล้าย C ที่ถูกแปลงเป็นโค้ดแบบ goto
- ฟังก์ชันตัวอย่าง
add_two_values(int a, int b)อาจถูกแปลงเป็นพื้นที่เก็บข้อมูลต่อไปนี้เมื่อไม่มีสแต็กa2v_a,a2v_bเป็นตัวแปรโกลบอลสำหรับเก็บอาร์กิวเมนต์a2v_cเป็นตัวแปรโกลบอลที่สอดคล้องกับตัวแปรโลคัลca2v_retaddrเป็นตัวแปรโกลบอลสำหรับเก็บที่อยู่ที่จะกลับไป
- ผู้เรียก
sample()จะเก็บ31415และ2718ลงในตัวแปรโกลบอลของอาร์กิวเมนต์แต่ละตัว - จากนั้นใส่ตำแหน่ง
resumeลงในa2v_retaddrแล้วกระโดดไปยังadd_two_values add_two_valuesเก็บผลลัพธ์การคำนวณไว้ในreturn_value_registerแล้วกลับไปยังa2v_retaddr- เมื่อกลับมาที่ตำแหน่ง
resumeผู้เรียกจะเก็บค่าจากรีจิสเตอร์ค่าคืนกลับลงในsample_x
การปรับให้เหมาะสมด้วยรีจิสเตอร์และ branch with link
- โครงสร้างเดียวกันสามารถทำให้เร็วขึ้นในระดับ ABI ด้วย การส่งผ่านรีจิสเตอร์
- โปรเซสเซอร์จำนวนมากมี
link registerพิเศษและคำสั่งbranch with linkbranch with linkจะบันทึกที่อยู่ของคำสั่งถัดจากคำสั่ง branch ลงใน link register โดยอัตโนมัติ- ผู้เรียกสามารถใส่อาร์กิวเมนต์สองตัวแรกลงใน
argument_register_1,argument_register_2 - ฟังก์ชันที่ถูกเรียกสามารถย้ายค่าจากรีจิสเตอร์เหล่านี้ไปยังตัวแปรโกลบอลแฝงของตัวเองเพื่อใช้งานได้
- ที่อยู่สำหรับคืนกลับก็สามารถบันทึกจาก
link_registerไปยังตัวแปรที่อยู่สำหรับคืนกลับของฟังก์ชันได้ - การปรับให้เหมาะสมนี้ยังคงโครงสร้างพื้นฐานที่สามารถเรียกและคืนกลับได้โดยไม่มีสแต็กไว้
เหตุผลที่รีเคอร์ชันถูกปิดกั้น
- ข้อจำกัดหลักของวิธีเรียกนี้คือ ไม่สามารถเรียกแบบรีเคอร์ซีฟได้
- เมื่อเกิดการเรียกแบบรีเคอร์ซีฟ ตัวแปรที่อยู่สำหรับคืนกลับของฟังก์ชันเดียวกันจะถูกเขียนทับด้วยที่อยู่สำหรับคืนกลับของการเรียกใหม่
- เมื่อการเรียกชั้นนอกจบลง ตำแหน่งเดิมที่ควรกลับไปจะหายไป ทำให้กระโดดไปยังตำแหน่งที่ผิด
- ภาษาการเขียนโปรแกรมในเวลานั้นหลีกเลี่ยงปัญหานี้ด้วยการไม่รองรับรีเคอร์ชัน
- FORTRAN ในตอนแรกยังไม่รองรับแม้แต่ซับรูทีน และซับรูทีนถูกเพิ่มเข้ามาในปี 1958
- การรองรับรีเคอร์ชันใน FORTRAN กลายเป็นมาตรฐานในปี 1991 และแม้ในตอนนั้นก็ยังต้องระบุซับรูทีนเป็น
RECURSIVEอย่างชัดเจน
โค้ดที่แก้ไขตัวเองและคำสั่งซับรูทีนของโปรเซสเซอร์ยุคแรก
- คอมไพเลอร์บางตัวใช้ โค้ดที่แก้ไขตัวเอง อย่างแยบยลกว่า
- ฟิลด์ที่อยู่ภายในคำสั่งกระโดดท้ายฟังก์ชันทำหน้าที่เสมือนตัวแปรที่อยู่สำหรับคืนกลับ
- วิธีนี้ไม่ใช่แค่ทริกง่าย ๆ แต่อาจเป็นความจำเป็นในทางปฏิบัติ
- โปรเซสเซอร์บางรุ่นอาจไม่รองรับ indirect jump
- หลังจากความเป็นประโยชน์ของซับรูทีนได้รับการยอมรับ โปรเซสเซอร์หลายรุ่นก็เพิ่มคำสั่งเรียกเฉพาะทาง
- บันทึกที่อยู่สำหรับคืนกลับไว้ใน word แรกของซับรูทีน
- การทำงานจริงเริ่มจาก word ที่สอง
- เมื่อคืนกลับ จะทำ indirect jump ผ่านลาเบลเริ่มต้นของซับรูทีน
- ในแอสเซมบลีตัวอย่าง
bsr add_two_valuesจะบันทึกที่อยู่สำหรับคืนกลับไว้ใน word แรกของadd_two_valuesแล้วเริ่มรันจากคำสั่งจริงถัดจากnopที่ใช้เป็นตัวเสียสละ
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ในหัวข้อนี้ The Art of Computer Programming ดีมากจริง ๆ
มองเผิน ๆ อาจดูเก่า แต่มีอัลกอริทึมจำนวนมหาศาลสำหรับจัดการอาร์เรย์หรือโครงสร้างข้อมูลที่เปลี่ยนแปลงแบบไดนามิกในยุคก่อนมี heap หรือ stack
หนังสือค่อย ๆ ไล่ไปถึง garbage collection และการ implement รายการของ Lisp และอัดแน่นด้วยความรู้แบบสารานุกรมอย่างที่คาดหวังจาก Knuth
ตัวอย่างที่ชอบเป็นพิเศษคือวิธีให้อาร์เรย์สองชุดแชร์พื้นที่เดียวกันแบบไดนามิก ให้อาร์เรย์หนึ่งโตไปข้างหน้าจาก
location#0และให้อาร์เรย์ที่สองโตย้อนกลับจากlocation#Endก็จะแบ่งใช้พื้นที่ที่จัดสรรแบบ static ได้อย่างมีประสิทธิภาพสามารถขยายไปเป็นอาร์เรย์จำนวนเท่าใดก็ได้ แต่ถึงจุดนั้นแค่ใช้
MallocกับReallocน่าจะดีกว่า และเทคนิคนั้นเองก็ใกล้เคียงกับรูทีนที่คล้าย malloc อยู่มากการแทรกและการวางไม่ต้องเลื่อนข้อมูลออกไป แต่การนำทางจำเป็นต้องทำ ถึงอย่างนั้นก็ทำงานได้ดี
ถ้าไม่มีมากพอก็จะจัดให้น้อยกว่าค่าที่ต้องการ และถ้าได้ไม่ถึงค่าขั้นต่ำก็จะรันไม่สำเร็จ
เท่าที่จำได้ ระบบจะวาง heap และไลบรารีไว้ด้านล่างของชิ้น RAM จริงนั้น และวาง stack ไว้ด้านบน
พอราว ๆ System 8 มีการเพิ่มเลเยอร์ virtualization เข้ามา วิธีนี้ก็จำเป็นน้อยลง และในยุค MacOS X ก็ใช้หน่วยความจำแบบ paging เหมือนระบบอื่น ๆ ทำให้ไม่ต้องใช้ท่าพลิกแพลงแบบนี้อีก
แต่ก็ยังน่าสนุกเมื่อคิดว่า “ทริกแปลก ๆ เพียงหนึ่งเดียว” แบบนี้จาก Art of Computer Programming เคยเป็นวิธีจัดสรร RAM ให้หลายแอปที่รันพร้อมกันในยุคหนึ่ง
ชุดหนึ่งโตขึ้น อีกชุดหนึ่งโตลง เป็นโครงสร้างที่น่าหลงใหล แต่สุดท้ายก็ไม่สามารถให้ประสิทธิภาพตามที่สัญญาไว้ได้
ภายในหน้าขนาดคงที่ อาร์เรย์ของ offset จะโตไปข้างหน้า ส่วนอาร์เรย์ของค่าบรรทัดที่มีความยาวแปรผันจะโตย้อนกลับจากท้ายหน้า เข้าใจว่าเมื่อ delete row แล้ว อาร์เรย์ด้านหลังอาจเกิดช่องว่างได้
เอกสารอ้างถึง TAOCP เกี่ยวกับโครงสร้าง B-tree เอง ดังนั้นถ้าจะเป็นแรงบันดาลใจโดยตรงก็ไม่น่าแปลกใจ
การใส่ฟังก์ชัน recursive เข้าไปใน ALGOL เป็นเรื่องที่ถกเถียงกันพอสมควร และยังคงเป็นเรื่องเล่าน่าสนุก: https://vanemden.wordpress.com/2014/06/18/how-recursion-got-...
How recursion got into programming: intrigue, betrayal, and advanced semantics - https://news.ycombinator.com/item?id=33123916 - ตุลาคม 2022, 8 ความคิดเห็น
How Recursion Got into Programming (2014) - https://news.ycombinator.com/item?id=23061881 - พฤษภาคม 2020, 47 ความคิดเห็น
How recursion got into Algol 60: a comedy of errors - https://news.ycombinator.com/item?id=10131664 - สิงหาคม 2015, 124 ความคิดเห็น
How recursion got into programming: a comedy of errors - https://news.ycombinator.com/item?id=8073361 - กรกฎาคม 2014, 108 ความคิดเห็น
อินเทอร์พรีเตอร์ Forth สำหรับเครื่อง SUBLEQ(https://github.com/howerj/subleq) และอินเทอร์พรีเตอร์สำหรับเครื่องแบบบิตอนุกรม(https://github.com/howerj/bit-serial) ถูกเขียนขึ้น โดยทั้งสองไม่มีสแตกสำหรับเรียกฟังก์ชันที่ Forth ต้องใช้
SUBLEQ ไม่อนุญาตแม้แต่การโหลด/บันทึกแบบอ้อม ดังนั้นถ้าจะทำอะไรที่ซับซ้อนแม้เพียงเล็กน้อยก็ต้องใช้โค้ดที่แก้ไขตัวเอง
แนวทางคือสร้าง เครื่องเสมือน ที่สามารถทำหน้าที่เหล่านั้นได้บนทั้งสองเครื่อง และใส่ cooperative multithreading เข้าไปด้วย
ถ้าต้องการฮีปก็เขียนด้วย Forth และชุดคำของ floating-point ก็เขียนด้วย Forth เช่นกัน MCU หลายตัวยังคงไม่มีคำสั่ง floating-point และสามารถจัดการได้ด้วยการเรียกฟังก์ชันซอฟต์แวร์ที่ implement สิ่งนี้
คอมไพเลอร์อื่น ๆ แม้ไม่ได้ถูกกล่าวถึงก็น่าจะใช้แนวทางคล้ายกัน อินเทอร์พรีเตอร์ BASIC บางตัวก็ implement VM แล้วตั้งเป้าไปที่ VM นั้น และ P-Code ก็คล้ายกัน
หน่วยความจำระบบส่วนใหญ่โดยพื้นฐานเป็นวิดีโอ RAM และต้องเข้าถึงผ่านขั้นตอนที่ค่อนข้างยุ่งยากด้วยการ
poke/peekรีจิสเตอร์ของชิปวิดีโอชิปวิดีโอจะรักษาพอยน์เตอร์หน่วยความจำปัจจุบันที่เพิ่มค่าอัตโนมัติไว้ ทำให้เมื่ออ่านหรือเขียนต่อเนื่อง พอยน์เตอร์จะเพิ่มขึ้นทีละ 1 แต่ข้อเท็จจริงที่ว่าหน่วยความจำระบบส่วนใหญ่เข้าถึงได้ด้วยวิธีนี้เท่านั้นเองก็ทำให้การเขียนโปรแกรมขนาดใหญ่ยากขึ้นมาก
ดังนั้น TI จึงสร้างเครื่องนามธรรมชื่อ GPL เพื่อทำให้การเข้าถึงวิดีโอ RAM นี้เป็นธรรมชาติมากขึ้น อย่างไรก็ตามมันถูกตีความและรันบน TMS9900 จึงช้ากว่าโค้ดเนทีฟ และยังช้าลงอีกเพราะ CPU เข้าถึง RAM ของชิปวิดีโอได้เฉพาะช่วงที่ชิปไม่ได้สแกนภาพออกหน้าจอ เช่น ช่วง horizontal/vertical retrace เท่านั้น
โค้ด BASIC และตัวแปรทั้งหมดก็อยู่ในวิดีโอเมโมรีนี้ ดังนั้นจึงเดาได้ไม่ยากว่าอินเทอร์พรีเตอร์ BASIC ของ TI-99/4A เขียนด้วยอะไร มันไม่ได้เร็วเลย
จุดที่น่าสนใจคือ TMS9900 ไม่มีรีจิสเตอร์ใช้งานทั่วไปจริง ๆ รีจิสเตอร์ workspace WR0~WR15 อยู่ที่ใดที่หนึ่งในหน่วยความจำ และรีจิสเตอร์ workspace pointer WP จะชี้ไปยังชุดนั้น
รีจิสเตอร์จริงทางกายภาพของ CPU มีเพียงสามตัวคือ PC, WP และ status register ผลก็คือสามารถทำ register windowing แบบดิบมาก ๆ ได้ และเมื่อใช้คำสั่ง
BLWPเพื่อกระโดด ชุด “รีจิสเตอร์” ใหม่ที่อยู่คนละตำแหน่งในหน่วยความจำจะถูกเปิดใช้งาน และ return address จะถูกบันทึกไว้ใน workspace ใหม่ที่ช่วงนี้พูดถึง TI-99/4A บ่อยก็เพราะกำลังทำ assembler สำหรับเครื่องรุ่นนี้เป็นโปรเจกต์ส่วนตัว
คำพูดที่ว่าโปรเซสเซอร์บางตัวบันทึก return address ไว้ในเวิร์ดก่อนหน้าคำสั่งแรกของซับรูทีนนั้นถูกต้อง และ PDP-8 ก็ทำแบบนั้น
วิวัฒนาการของ PDP-8 อาจมองได้ว่าเป็นการเดินทางของการรองรับ recursion ในฮาร์ดแวร์
ตอนแรกคำสั่ง
JMSจะฝัง return address ไว้ในเวิร์ดแรกของฟังก์ชัน มักมีกรณีที่ผู้เรียกวางอาร์กิวเมนต์ไว้หลังคำสั่งJMSและผู้ถูกเรียกอ่านอาร์กิวเมนต์ด้วยออฟเซ็ตจากคำสั่ง return พร้อมกับเพิ่มค่าไปเรื่อย ๆ เพื่อให้ return address กลับไปชี้ตำแหน่งโค้ดอีกครั้งต่อมาวิธีที่ค่อนข้างแพร่หลายคือใช้ตำแหน่ง auto-increment ตำแหน่งหนึ่งเพื่อสร้างสแตกอย่างง่าย PDP-8 มีตำแหน่งหน่วยความจำ 8 ตำแหน่งที่เพิ่มค่าทุกครั้งเมื่อใช้เป็นพอยน์เตอร์ และ prologue/epilogue ของฟังก์ชันจะจัดการสแตกนี้เอง ทำให้ทำ recursion ได้เต็มรูปแบบ
ภายหลังยิ่งกว่านั้น ใน implementation แบบไมโครโปรเซสเซอร์อย่าง Harris 6120 มีการเพิ่มฮาร์ดแวร์สแตกเข้ามา ทำให้ประสิทธิภาพดีขึ้น
Rหรือคำสั่งบันทึก return addressคำสั่งนี้จะบันทึก
PC+1ที่ถูกเพิ่มค่าแล้วลงในส่วนที่อยู่ของคำสั่งที่ตำแหน่งเป้าหมาย และตามธรรมเนียม เป้าหมายนั้นคือคำสั่งกระโดดแบบไม่มีเงื่อนไขที่อยู่ก่อนจุดเริ่มซับรูทีนทันทีหลังคำสั่ง
Rจะวางคำสั่งUซึ่งเป็นคำสั่งกระโดดแบบไม่มีเงื่อนไขไปยังซับรูทีนนั้นซับรูทีนจะ return ด้วยการกระโดดไปยังที่อยู่ก่อนหน้าตัวเอง และตรงนั้นมีคำสั่งกระโดดแบบไม่มีเงื่อนไขกลับไปยังตำแหน่งถัดจากจุดเรียกพอดี
หากไม่ใช้ calling convention ที่พัฒนากว่านี้ recursion ก็เป็นไปไม่ได้ และ opcode ของทุกคำสั่งในภาษา assembly เป็นตัวอักษรหนึ่งตัว
ในโปรแกรมที่เขียนสำหรับ AVR-8 บางครั้งการใช้ C calling convention ให้ความรู้สึกเหมือนบ้าไปแล้ว
ถ้าใช้ assembly ก็สามารถเก็บตัวแปรลูปภายในไว้ใน register file ขนาดใหญ่ได้ตลอด หรือไม่ก็ใช้วิธีที่อธิบายในบทความได้
วิธี “ระบายสี” ฟังก์ชันในแอปแบบนี้ก็ดีเช่นกัน ถ้ารู้ว่าฟังก์ชันสีแดงกับฟังก์ชันสีเขียวจะไม่ active พร้อมกัน ก็สามารถนำตัวแปร local หรือพารามิเตอร์ของทั้งสองมาใช้พื้นที่ซ้ำกันได้
ครั้งหนึ่งในโปรเจกต์ codebase สำหรับไมโครคอนโทรลเลอร์ที่ผมเข้าไปร่วม นักพัฒนาหลายคนใช้เวลาหลายสัปดาห์ตามหาบั๊กที่จับยากในหลาย subsystem
พอย้ายโค้ด บั๊กก็ย้ายตามไปด้วย หลังจากไล่ดูเล็กน้อยและวาง trap ไว้ ก็พบตำแหน่งโค้ดที่ call stack ลึกเกินไปจนเขียนทับโครงสร้างข้อมูลอื่นได้
ตอนเริ่มเรียนเขียนโปรแกรม ผมถูกบังคับให้เขียนแบบนี้เป๊ะ ๆ เลย ไม่ใช่ยุค 1970 แต่เป็น ปี 2001
เพราะประสบการณ์เขียนโปรแกรมครั้งแรกของผมคือ “ภาษา” สคริปต์แบบกึ่งกราฟิกที่เครื่องมือพัฒนาเกม RPG Maker 2000 มีให้
ถ้าไม่เคยเห็นการเขียนสคริปต์ของ RM2K ให้นึกถึงการผสมกันระหว่าง Scratch กับโหมด Emacs Paredit ตัวอย่าง: https://forums.rpgmakerweb.com/data/attachments/21/21958-f89...
มันดูเหมือนข้อความ แต่แก้ไขเหมือนข้อความไม่ได้ แก้ได้เฉพาะเป็นบล็อกที่มีไดอะล็อกคุณสมบัติประกอบ
แน่นอนว่าภาษาสคริปต์ของ RPG Maker ไม่มีของหรู ๆ อย่างสแตก ถ้าต้องการซับรูทีนที่นำกลับมาใช้ซ้ำได้ ก็ต้องจัดสรรตัวแปรโกลบอลลับ ๆ ไว้เป็นพารามิเตอร์ และไม่มี reentrancy
มองย้อนกลับไป ถ้าดื้อพอ ก็น่าจะสร้างทั้งรีจิสเตอร์และ runtime stack ภายใน RPG Maker 2000 ได้
ตอนแรกดูเหมือนง่าย เราสร้าง “รีจิสเตอร์” ปลอม ๆ แบบ zero page ของ 6502 ได้ และสร้างสแตกด้วยการเข้าถึงตัวแปรทางอ้อม (https://rpgmaker.net/tutorials/523/) ได้ด้วย
ปัญหาคือ RM2K มีภาวะพร้อมกันในรูปแบบสคริปต์ “parallel process” ถ้าโปรเซสคู่ขนานใช้ abstraction แบบนี้ “เธรด” ต่าง ๆ ก็จะเขียนทับสถานะของกันและกันมั่วไปหมด
ดังนั้นจึงต้องมี zero page และสแตกหลายชุดต่อ “คอร์เสมือน” แต่ละคอร์ และต้องจัดสรร/ผูก/กำหนดตารางเวลาให้คอร์เสมือนกับสคริปต์คู่ขนานแต่ละตัว พูดอีกอย่างคือต้องทำให้แต่ละสคริปต์มี stack pointer ที่รู้กันเฉพาะตัวมันเองให้ได้
ถ้าจะให้เสถียรแม้มี race condition ปกติก็ต้องมีอะไรอย่าง mutex
เมื่อคิดถึงความดื้อดึงของนักพัฒนาเกม RPG Maker แล้ว ผมว่าคงมีใครสักคนหาวิธีหลอกฟีเจอร์ runtime บางอย่างให้ทำงานเหมือน mutex ได้ แต่พูดตรง ๆ ผมกลัวจนไม่อยากรู้เลยว่าจริง ๆ เขาทำอะไรกัน
ผมจำได้ว่าเคยดาวน์โหลดเกมจาก rpgmaker.net ที่มี custom battle system อยู่ข้างใน เป็นการ implement ที่แทนที่ระบบต่อสู้ในตัวทั้งหมดด้วยเทคนิคทำนองที่คุณอธิบาย
ตอนเปิดดูใน editor ว่ามันทำงานยังไง ผมรู้สึกทึ่งจนรับไม่ไหว มี “ตัวแปร” หลายร้อยตัว และถ้าจำไม่ผิดอนุญาตแค่ i64 เท่านั้น อีกทั้งยังมี “สวิตช์” หลายร้อยตัวด้วย สวิตช์คือ boolean
ตอนนั้นผมยังไม่มีแนวคิดเรื่องสแตก ฮีป หรือการเรียกฟังก์ชันเลย
ผมนึกไม่ออกจริง ๆ ว่าต้องใช้พลังงานขนาดไหนในการสร้างและดูแล/ดีบักสิ่งนั้น
ถ้าจำไม่ผิด ตอนเขียน โปรแกรม BASIC บน ZX81 ผมเขียนในแบบที่ใกล้เคียงกับ “ไม่มีสแตก”
1 GOTO 3010 LET C = A + B20 RETURN30 LET A = 140 LET B = 250 GOSUB 1060 LET A = C70 LET B = 380 GOSUB 1090 PRINT CRUN6เท่ากับว่าผมทำสิ่งที่คอมไพเลอร์ทำในบทความด้วยตัวเอง เลขบรรทัดคือที่อยู่หน่วยความจำ และตัวแปรที่ซ่อนอยู่ก็ไม่ได้ซ่อนจากผม เพราะผมนั่นแหละคือคอมไพเลอร์
สิ่งเดียวที่ interpreter ทำให้คือเก็บที่อยู่สำหรับกลับของ
GOSUBแต่โค้ดอาจผิดไวยากรณ์ หรือความทรงจำผมอาจเพี้ยนไปก็ได้ 40 ปีเป็นเวลานาน แต่ไอเดียโดยรวมถูกต้อง
อีกอย่าง โปรเซสเซอร์ Z80 ในเครื่องก็มีฟังก์ชันจัดการสแตกอยู่ BASIC interpreter นั้นเรียบง่ายมากจริง ๆ แต่ก็มีข้อแก้ตัวอยู่ เพราะมี RAM แค่ 1KB และ ROM 8KB ที่ต้องใส่ทั้ง OS, interpreter และทุกอย่างไว้ในนั้น
GOSUBจะเก็บเลขบรรทัดหรือ reference อื่น ๆ ที่RETURNจะอ้างถึง และเมื่อซ้อนการเรียกGOSUBก็ต้องจำจุดกลับหลายจุด จึงต้องมีสแตกในรูปแบบใดรูปแบบหนึ่งเพียงแต่ BASIC บางตัวไม่ได้มีสแตกอเนกประสงค์ แต่มีแค่อาร์เรย์คงที่ของ return pointer กับดัชนีตำแหน่งปัจจุบัน เช่น กำหนดความลึกการเรียกไว้ตายตัวที่ 7 ระดับ จากมุมมองของโปรแกรมเมอร์ มันก็ทำงานเหมือน call stack
แน่นอนว่าไม่ใช่สแตก “ของจริง” ที่มี local variable/parameter อย่างที่คนส่วนใหญ่คาดหวังเมื่อพูดถึงสแตก
ในสภาพแวดล้อมพื้นฐานของ BBC BASIC มีเดโมสนุก ๆ ที่แสดงให้เห็นว่าเกิดอะไรขึ้นระหว่างการเรียกซ้อน รวมถึง recursion ได้ด้วย ถ้ากำหนดตำแหน่งสแตกไว้ที่ส่วนบนสุดของหน่วยความจำจอภาพ แล้วไม่วาดอะไรไว้ตรงนั้น ก็จะเห็นสแตกเติบโตระหว่างที่งานดำเนินไป
เพราะความละเอียดหน้าจอต่ำ return address ขนาด 2 ไบต์จึงมองเห็นเป็นพิกเซลหนา ๆ 8 พิกเซลในโหมดจอ 1 หรือ 5 ในโหมด 2 จะเป็น 4 พิกเซลแต่มีสีที่กะพริบเลยไม่ค่อยดี ส่วนในโหมด 0, 3, 4, 6 จะเป็น 16 พิกเซล แต่การดูระดับบิตนั้นแยกแยะยากกว่าการเห็นสี 8 สีวนซ้ำ
ก่อนจะมีฮีปที่ขยายได้ตามใจ โปรแกรมเมอร์อย่างน้อยก็ต้องใช้ วิจารณญาณทางวิศวกรรม อยู่บ้าง
เพราะต้องพิจารณาการกระจายเชิงความน่าจะเป็นของอินพุต และกำหนดขนาดพื้นที่เก็บข้อมูลชั่วคราวทั้งหมดให้เหมาะสม
ดังนั้นจึงเกิดหัวข้อ “BUGS AND LIMITATIONS” ขึ้นมา
ดังนั้นจึงจัดสรรทุกอย่างแบบ static ตอน compile time และต้องรู้ว่าอินพุตจะกินหน่วยความจำมากเท่าไร
แต่การรู้ขอบเขตบนของการใช้หน่วยความจำก็เคยเป็นเรื่องปกติสำหรับโปรแกรมเมอร์แอปพลิเคชันด้วย เพราะไม่มีใครอยากให้หน่วยความจำไม่พอเด็ดขาด
สมัยนี้เหมือนปล่อยการใช้หน่วยความจำแบบ YOLO กันไปเลยหรือเปล่า
เช่น เมื่อเทียบกับข้อจำกัดแบบความยาวคำสั่งสูงสุดของ sed ที่มีขนาดจำกัดและสั้น ก็ถือเป็นการปรับปรุงครั้งใหญ่
ผมเขียนโปรแกรมเชิงฟังก์ชันมานานเกินไป จนบอกตามตรงว่านึกไม่ออกจริง ๆ ว่าจะเขียนโค้ดยังไง โดยไม่ใช้รีเคอร์ชัน
ในเชิงเทคนิค ผมรู้วิธีเปลี่ยนอัลกอริทึมแบบรีเคอร์ซีฟให้เป็นแบบวนซ้ำ และเคยทำในที่ที่มีข้อจำกัดด้านทรัพยากรสูงด้วย แต่ก็ไม่ได้ชอบ
โดยทั่วไปฝั่งรีเคอร์ชันมักจะสวยกว่า และใน 99% ของกรณีก็ถือว่าเร็วพอ ถ้าคอมไพเลอร์รองรับ tail recursion ก็เกือบ 100% แต่สำหรับงานส่วนใหญ่ที่น่าสนใจกว่า ยังไงก็ต้องดูแลสแตกเองอยู่ดี
บางครั้งผมตั้งใจทำงานแบบนั้นเพื่อเรียนรู้ว่า ก่อนผมเกิดเขาทำกันยังไง ช่วงนี้ผมลองแงะ ๆ เกม Commodore 64 อยู่บ้าง แล้วก็ยิ่งรู้สึกชัดมากว่าทุกวันนี้เราโชคดีแค่ไหนที่คุ้นเคยกับฮาร์ดแวร์ที่เร็ว ถูก และใช้ง่าย
ถ้าจะทำรีเคอร์ชันบนเครื่องเก่า ๆ แบบนั้น ก็ต้องสร้างกลไกสแตกขึ้นมาเอง และถึงอย่างนั้นก็ยังเหลือปัญหาที่ต้องจัดการ เพราะโดยพื้นฐานแล้วไม่มีทางเลือกให้ใช้ นอกจากที่เก็บข้อมูลแบบโกลบอล
ผมเคยผ่านยุคนั้นมาแล้ว แต่ไม่อยากแนะนำให้ใครเลย
ในฟีเจอร์
@letของ Enhanced GNU Awk นั้น บล็อก@letที่อยู่นอกฟังก์ชัน เช่น ภายในบล็อกBEGINหรือENDถูกปล่อยให้คอมไพเลอร์จัดสรร ตัวแปรโกลบอลลับตัวแปรเหล่านี้จะถูกนำกลับมาใช้ซ้ำระหว่างบล็อกเท่าที่ทำได้
$ ./gawk --dump-variables 'BEGIN { @let (a, b, c = 1) { } }'$ cat awkvars.out$let0001: untyped variable$let0002: untyped variable$let0003: 1ARGC: 1ARGIND: 0ARGV: array, 1 elementsBINMODE: 0[ .. snip many ]https://www.kylheku.com/cgit/egawk/about/
pingก็ไม่ได้ และnc -z 104.37.63.7 443ก็ไม่ได้อัปเดต: ดูเหมือนโครงสร้างพื้นฐานด้านความปลอดภัยจะพัง ผมไม่รู้ด้วยซ้ำว่ามันคืออะไร และไม่ได้ใช้ Twitter ด้วย พอตรวจ AS แล้วเป็น Google Fiber
แล้วก็หวังว่าจะไม่มีใครขุดข้อมูลส่วนตัวของผมนะ