ฉันเข้าใจว่าตัวชี้แบบสแต็คคืออะไร แต่ใช้เพื่ออะไร


11

ตัวชี้สแต็กชี้ไปที่ด้านบนของสแต็กซึ่งเก็บข้อมูลเกี่ยวกับสิ่งที่เราเรียกว่า "LIFO" เพื่อขโมยความคล้ายคลึงของคนอื่นมันเหมือนกับกองอาหารที่คุณใส่และนำไปปรุงอาหารที่ด้านบน ตัวชี้สแต็ก OTOH ชี้ไปที่ "จาน" ด้านบนของสแต็ก อย่างน้อยมันก็เป็นจริงสำหรับ x86

แต่ทำไมคอมพิวเตอร์ / โปรแกรม "สนใจ" ตัวชี้สแต็คชี้ไปที่อะไร? กล่าวอีกนัยหนึ่งจุดประสงค์อะไรที่มีตัวชี้สแต็กและการรู้ว่าจะให้บริการที่ใด

คำอธิบายที่เข้าใจได้โดยโปรแกรมเมอร์ C จะได้รับการชื่นชม


เนื่องจากคุณไม่สามารถเห็นด้านบนของกองซ้อนใน RAM เหมือนที่คุณเห็นด้านบนของกองซ้อนของจาน
tkausl


8
คุณไม่ได้ใช้จานจากด้านล่างของสแต็ค คุณสามารถเพิ่มหนึ่งอยู่ด้านบนและคนอื่นใช้มันมาจากด้านบน คุณกำลังคิดถึงคิวที่นี่
Kilian Foth

@Snowman การแก้ไขของคุณดูเหมือนจะเปลี่ยนความหมายของคำถาม moonman239 คุณสามารถตรวจสอบได้หรือไม่ว่าการเปลี่ยนแปลงของ Snowman นั้นถูกต้องโดยเฉพาะการเพิ่ม "กองซ้อนนี้มีจุดประสงค์อะไรจริง ๆ
8bittree

1
@ 8bittree โปรดดูคำอธิบายการแก้ไข: ฉันคัดลอกคำถามตามที่ระบุในบรรทัดหัวเรื่องไปยังเนื้อหาของคำถาม แน่นอนว่าฉันเปิดรับความเป็นไปได้ที่ฉันจะเปลี่ยนแปลงบางสิ่งบางอย่างและผู้แต่งดั้งเดิมจะสามารถย้อนกลับหรือแก้ไขโพสต์ได้ตลอดเวลา

คำตอบ:


23

สแต็คนี้มีจุดประสงค์อะไรจริง ๆ ซึ่งต่างจากการอธิบายโครงสร้างของมัน

คุณมีคำตอบมากมายซึ่งอธิบายโครงสร้างของข้อมูลที่จัดเก็บไว้ในสแต็กอย่างถูกต้องซึ่งฉันทราบว่าตรงข้ามกับคำถามที่คุณถาม

วัตถุประสงค์ที่สแต็คทำหน้าที่เป็น: สแต็คเป็นส่วนหนึ่งของการทำให้เป็นจริงต่อเนื่องในภาษาโดยไม่ต้อง coroutines

มาแกะกล่องกันเถอะ

ต่อเนื่องจะใส่เพียงตอบคำถามว่า "สิ่งที่จะเกิดขึ้นต่อไปในโปรแกรมของฉัน" ในทุก ๆ จุดในโปรแกรมทุกสิ่งจะเกิดขึ้นต่อไป ตัวถูกดำเนินการสองตัวจะถูกคำนวณจากนั้นโปรแกรมจะดำเนินการต่อโดยการคำนวณผลรวมของพวกเขาจากนั้นโปรแกรมจะดำเนินการต่อโดยการกำหนดผลรวมให้กับตัวแปรแล้วจึง ... และต่อไป

การทำให้เป็นจริงเป็นเพียงคำศัพท์ที่ยอดเยี่ยมสำหรับการนำแนวคิดนามธรรมมาใช้อย่างเป็นรูปธรรม "จะเกิดอะไรขึ้นต่อไป?" เป็นแนวคิดที่เป็นนามธรรม วิธีการวางสแต็กเป็นส่วนหนึ่งของแนวคิดนามธรรมที่กลายเป็นเครื่องจริงที่คำนวณสิ่งต่างๆ

Coroutineเป็นฟังก์ชั่นที่จำได้ว่าอยู่ที่ไหนให้ผลการควบคุมไปยัง coroutine อีกระยะหนึ่งแล้วกลับมาทำงานต่อที่พวกมันทิ้งไว้ในภายหลัง แต่ไม่จำเป็นว่าจะต้องเกิดขึ้นทันทีหลังจาก coroutine ที่เพิ่งเรียกว่า คิดว่า "คืนผลตอบแทน" หรือ "รอ" ใน C # ซึ่งจะต้องจำไว้ว่าพวกเขาอยู่ที่ไหนเมื่อมีการร้องขอรายการถัดไปหรือการดำเนินการแบบอะซิงโครนัสเสร็จสมบูรณ์ ภาษาที่มี coroutines หรือฟีเจอร์ภาษาที่คล้ายกันนั้นต้องการโครงสร้างข้อมูลที่สูงกว่าสแต็กเพื่อที่จะใช้งานต่อเนื่อง

สแต็กนำมาใช้อย่างต่อเนื่องได้อย่างไร คำตอบอื่น ๆ พูดอย่างไร สแต็คสโตร์ (1) ค่าของตัวแปรและเทมเพลตที่อายุการใช้งานไม่สูงกว่าการเปิดใช้งานของวิธีการปัจจุบันและ (2) ที่อยู่ของรหัสการต่อเนื่องที่เกี่ยวข้องกับการเปิดใช้งานวิธีล่าสุด ในภาษาที่มีข้อยกเว้นการจัดการสแต็กอาจเก็บข้อมูลเกี่ยวกับ "ข้อผิดพลาดต่อเนื่อง" - นั่นคือสิ่งที่โปรแกรมจะทำต่อไปเมื่อเกิดสถานการณ์พิเศษ

ให้ฉันใช้โอกาสนี้เพื่อทราบว่าสแต็คไม่ได้บอกคุณว่า "ฉันมาจากไหน" - แม้ว่ามันมักจะถูกใช้เพื่อการดีบัก สแต็คจะบอกคุณว่าคุณจะไปต่อไปและสิ่งที่มีค่าของตัวแปรที่เปิดใช้งานนั้นจะได้รับเมื่อคุณได้รับมี ความจริงที่ว่าในภาษาที่ไม่มี coroutines ที่ที่คุณกำลังจะไปต่อไปนั้นเกือบจะเป็นจุดที่คุณมาจากการทำให้การดีบักแบบนี้ง่ายขึ้น แต่ไม่มีความต้องการที่คอมไพเลอร์เก็บข้อมูลเกี่ยวกับที่มาของการควบคุมหากสามารถหลีกเลี่ยงได้โดยไม่ทำเช่นนั้น การปรับการเรียกหางให้เหมาะสมเช่นทำลายข้อมูลเกี่ยวกับที่มาของโปรแกรมควบคุม

เหตุใดเราจึงใช้สแต็กเพื่อนำความต่อเนื่องมาใช้ในภาษาโดยไม่มี coroutines เนื่องจากลักษณะของการเปิดใช้งานวิธีการแบบซิงโครนัสคือรูปแบบของ "ระงับวิธีการปัจจุบันเปิดใช้งานวิธีอื่นดำเนินการวิธีการปัจจุบันที่รู้ผลลัพธ์ของวิธีการเปิดใช้งาน" เมื่อประกอบด้วยตัวเองในรูปแบบสแต็กของการเปิดใช้งาน การสร้างโครงสร้างข้อมูลที่ใช้ลักษณะการทำงานแบบกองซ้อนนี้มีราคาถูกและใช้งานง่าย ทำไมราคาถูกและง่ายมาก เนื่องจากชุดชิปได้รับการออกแบบมาโดยเฉพาะเพื่อให้การเขียนโปรแกรมประเภทนี้ง่ายสำหรับนักเขียนคอมไพเลอร์


โปรดทราบว่าข้อความอ้างอิงที่คุณอ้างอิงนั้นถูกเพิ่มเข้ามาในการแก้ไขโดยผู้ใช้รายอื่นและถูกแก้ไขแล้วทำให้คำตอบนี้ไม่ได้ตอบคำถาม
8bittree

2
ฉันค่อนข้างแน่ใจว่าคำอธิบายควรจะเพิ่มความชัดเจน ฉันไม่เชื่ออย่างสมบูรณ์ว่า "สแต็คเป็นส่วนหนึ่งของการปรับปรุงความต่อเนื่องในภาษาที่ไม่มี coroutines" มาใกล้เคียงกับที่ :-)

4

การใช้สแต็กส่วนใหญ่เป็นการจัดเก็บที่อยู่ผู้ส่งสำหรับฟังก์ชัน:

void a(){
    sub();
}
void b(){
    sub();
}
void sub() {
    //should i got back to a() or to b()?
}

และจากมุมมอง C นี่คือทั้งหมด จากมุมมองของคอมไพเลอร์:

  • อาร์กิวเมนต์ของฟังก์ชันทั้งหมดถูกส่งผ่านโดยการลงทะเบียน CPU - หากมีการลงทะเบียนไม่เพียงพออาร์กิวเมนต์จะถูกวางบนสแต็ก
  • หลังจากฟังก์ชั่นสิ้นสุดลง (ส่วนใหญ่) รีจิสเตอร์ควรมีค่าเหมือนกันก่อนที่จะเข้าไป - ดังนั้นรีจิสเตอร์ที่ใช้แล้วจะถูกสำรองบนสแต็ก

และจากมุมมองของระบบปฏิบัติการ: โปรแกรมสามารถถูกขัดจังหวะได้ตลอดเวลาดังนั้นหลังจากที่เราทำภารกิจของระบบเสร็จแล้วเราจะต้องเรียกคืนสถานะ CPU ดังนั้นให้เก็บทุกอย่างไว้ในกอง

ทั้งหมดนี้ทำงานได้เนื่องจากเราไม่สนใจว่าจะมีรายการอยู่ในสแต็คมากน้อยเพียงใดหรือมีกี่รายการที่คนอื่นจะเพิ่มในอนาคตเราเพียงแค่ต้องรู้ว่าเราย้ายตัวชี้สแต็คไปเท่าใดและกู้คืนหลังจากเสร็จสิ้น


1
ฉันคิดว่ามันแม่นยำมากกว่าที่จะบอกว่าอาร์กิวเมนต์นั้นถูกผลักลงบนสแต็ก แต่บ่อยครั้งที่การลงทะเบียนการปรับให้เหมาะสมใช้แทนโปรเซสเซอร์ที่มีการลงทะเบียนฟรีเพียงพอสำหรับงาน นั่นเป็นนิดหน่อย แต่ฉันคิดว่ามันเข้ากันได้ดีกว่าว่าภาษามีวิวัฒนาการมาในอดีตอย่างไร คอมไพเลอร์ C / C ++ ที่เก่าที่สุดไม่ได้ใช้การลงทะเบียนเลยสำหรับเรื่องนี้
Gort the Robot

4

LIFO กับ FIFO

LIFO ย่อมาจาก Last In, First Out เช่นเดียวกับรายการสุดท้ายที่ใส่เข้าไปในสแต็กคือรายการแรกที่นำออกจากสแต็ก

สิ่งที่คุณอธิบายด้วยความคล้ายคลึงกับอาหารของคุณ (ในการแก้ไขครั้งแรก ) คือคิวหรือ FIFO, เข้าก่อน, ออกก่อน

ความแตกต่างที่สำคัญระหว่างทั้งสองคือ LIFO / สแต็กผลัก (แทรก) และ pops (ลบ) จากปลายเดียวกันและ FIFO / คิวทำจากปลายตรงข้าม

// Both:

Push(a)
-> [a]
Push(b)
-> [a, b]
Push(c)
-> [a, b, c]

// Stack            // Queue
Pop()               Pop()
-> [a, b]           -> [b, c]

ตัวชี้สแต็ก

ลองมาดูสิ่งที่เกิดขึ้นภายใต้ฝากระโปรงของกอง นี่คือหน่วยความจำบางส่วนแต่ละกล่องมีที่อยู่:

...[ ][ ][ ][ ]...                       char* sp;
    ^- Stack Pointer (SP)

และมีตัวชี้สแต็คชี้ไปที่ด้านล่างของสแต็กที่ว่างเปล่าในปัจจุบัน (ไม่ว่าสแต็คจะโตขึ้นหรือโตขึ้นไม่เกี่ยวข้องโดยเฉพาะที่นี่ดังนั้นเราจะไม่สนใจสิ่งนั้น แต่แน่นอนในโลกแห่งความเป็นจริง และสิ่งที่ลบออกจาก SP)

งั้นลองผลักa, b, and cอีกครั้ง กราฟิกด้านซ้ายการทำงาน "ระดับสูง" ที่อยู่ตรงกลางโค้ดหลอก C-ish ด้านขวา:

...[a][ ][ ][ ]...        Push('a')      *sp = 'a';
    ^- SP
...[a][ ][ ][ ]...                       ++sp;
       ^- SP

...[a][b][ ][ ]...        Push('b')      *sp = 'b';
       ^- SP
...[a][b][ ][ ]...                       ++sp;
          ^- SP

...[a][b][c][ ]...        Push('c')      *sp = 'c';
          ^- SP
...[a][b][c][ ]...                       ++sp;
             ^- SP

อย่างที่คุณเห็นแต่ละครั้งที่เราpushใส่อาร์กิวเมนต์ในตำแหน่งที่ตัวชี้สแต็คกำลังชี้และปรับตัวชี้สแต็กให้ชี้ไปที่ตำแหน่งถัดไป

ตอนนี้ขอป๊อป:

...[a][b][c][ ]...        Pop()          --sp;
          ^- SP
...[a][b][c][ ]...                       return *sp; // returns 'c'
          ^- SP
...[a][b][c][ ]...        Pop()          --sp;
       ^- SP
...[a][b][c][ ]...                       return *sp; // returns 'b'
       ^- SP

Popอยู่ฝั่งตรงข้ามของpushมันปรับตัวชี้สแต็คไปยังจุดที่สถานที่ก่อนหน้านี้และลบรายการที่อยู่ที่นั่น (โดยปกติจะส่งกลับมาให้ใครก็ตามที่เรียกว่าpop)

คุณอาจสังเกตเห็นว่าbและcยังคงอยู่ในหน่วยความจำ ฉันแค่ต้องการรับรองว่าสิ่งเหล่านั้นไม่ใช่ตัวพิมพ์ผิด เราจะกลับไปที่ในไม่ช้า

ชีวิตที่ไม่มีตัวชี้สแต็ค

มาดูกันว่าจะเกิดอะไรขึ้นถ้าเราไม่มีตัวชี้สแต็ค เริ่มต้นด้วยการกดอีกครั้ง:

...[ ][ ][ ][ ]...
...[ ][ ][ ][ ]...        Push(a)        ? = 'a';

เอ่ออืม ... ถ้าเราไม่มีตัวชี้สแต็คเราก็ไม่สามารถย้ายบางสิ่งไปยังที่อยู่ที่ชี้ไป บางทีเราสามารถใช้ตัวชี้ที่ชี้ไปที่ฐานแทนด้านบน

...[ ][ ][ ][ ]...                       char* bp; // "base pointer"
    ^- bp                                bp = malloc(...);

...[a][ ][ ][ ]...        Push(a)        *bp = 'a';
    ^- bp
// No stack pointer, so no need to update it.
...[b][ ][ ][ ]...        Push(b)        *bp = 'b';
    ^- bp

เอ่อโอ้. เนื่องจากเราไม่สามารถเปลี่ยนค่าคงที่ของฐานสแต็กเราจึงเขียนทับaโดยการกดbไปยังตำแหน่งเดียวกัน

ทำไมเราไม่ติดตามจำนวนครั้งที่เราผลัก และเราจะต้องติดตามเวลาที่เราตอกหมุดด้วย

...[ ][ ][ ][ ]...                       char* bp; // "base pointer"
    ^- bp                                bp = malloc(...);
                                         int count = 0;

...[a][ ][ ][ ]...        Push(a)        bp[count] = 'a';
    ^- bp
...[a][ ][ ][ ]...                       ++count;
    ^- bp
...[a][b][ ][ ]...        Push(a)        bp[count] = 'b';
    ^- bp
...[a][b][ ][ ]...                       ++count;
    ^- bp
...[a][b][ ][ ]...        Pop()          --count;
    ^- bp
...[a][b][ ][ ]...                       return bp[count]; //returns b
    ^- bp

มันใช้งานได้ แต่จริง ๆ แล้วมันค่อนข้างคล้ายกับก่อนยกเว้น*pointerมีราคาถูกกว่าpointer[offset](ไม่มีเลขคณิตพิเศษ) ไม่พูดถึงมันพิมพ์น้อย ดูเหมือนว่าฉันจะสูญเสีย

ลองอีกครั้ง แทนที่จะใช้รูปแบบสตริง Pascal ในการค้นหาจุดสิ้นสุดของคอลเลกชันที่ใช้อาร์เรย์ (ติดตามจำนวนรายการที่อยู่ในคอลเลกชัน) ให้ลองใช้รูปแบบสตริง C (สแกนตั้งแต่ต้นจนจบ):

...[ ][ ][ ][ ]...                       char* bp; // "base pointer"
    ^- bp                                bp = malloc(...);

...[ ][ ][ ][ ]...        Push(a)        char* top = bp;
    ^- bp, top
                                         while(*top != 0) { ++top; }
...[ ][ ][ ][a]...                       *top = 'a';
    ^- bp    ^- top

...[ ][ ][ ][ ]...        Pop()          char* top = bp;
    ^- bp, top
                                         while(*top != 0) { ++top; }
...[ ][ ][ ][a]...                       --top;
    ^- bp       ^- top                   return *top; // returns '('

คุณอาจเดาปัญหาได้แล้วที่นี่ หน่วยความจำที่ไม่ได้กำหนดค่าเริ่มต้นนั้นไม่ได้รับประกันว่าจะเป็น 0 ดังนั้นเมื่อเราค้นหาตำแหน่งบนสุดaเราจะข้ามสถานที่ตั้งของหน่วยความจำที่ไม่ได้ใช้ซึ่งมีขยะสุ่มอยู่ในนั้น ในทำนองเดียวกันเมื่อเราสแกนไปที่ด้านบนสุดเราจะกระโดดได้ดีกว่าที่aเราผลักไปจนในที่สุดเราก็พบตำแหน่งหน่วยความจำอื่นที่เพิ่งเกิดขึ้น0และย้ายกลับไปและกลับขยะแบบสุ่มก่อนหน้านั้น

มันง่ายพอที่จะแก้ไขเราเพียงแค่ต้องเพิ่มการดำเนินการPushและPopเพื่อให้แน่ใจว่าด้านบนสุดของสแต็กนั้นได้รับการอัพเดตเสมอเพื่อทำเครื่องหมายด้วย a 0และเราต้องเริ่มต้นสแต็กด้วยตัวยุติดังกล่าว แน่นอนซึ่งหมายความว่าเราไม่สามารถมี0(หรือค่าใดก็ตามที่เราเลือกเป็นเทอร์มิเนเตอร์) เป็นค่าจริงในสแต็ก

ยิ่งไปกว่านั้นเรายังได้เปลี่ยนการดำเนินงานของ O (1) ไปเป็นการดำเนินการ O (n)

TL; DR

ตัวชี้สแต็ติดตามด้านบนของสแต็คที่ทั้งหมดของการกระทำที่เกิดขึ้น มีวิธีการกำจัดมัน ( bp[count]และtopยังคงเป็นตัวชี้สแต็ก) แต่ทั้งคู่จบลงด้วยความซับซ้อนและช้ากว่าการมีตัวชี้สแต็ก และไม่รู้ว่าส่วนบนสุดของสแต็คหมายความว่าคุณไม่สามารถใช้สแต็กได้

หมายเหตุ: ตัวชี้สแต็กชี้ไปที่ "ด้านล่าง" ของ runtime stack ใน x86 อาจเป็นความเข้าใจผิดที่เกี่ยวข้องกับ runtime stack ทั้งหมดที่คว่ำ กล่าวอีกนัยหนึ่งฐานของสแต็คจะถูกวางไว้ที่ที่อยู่หน่วยความจำสูงและส่วนปลายของสแต็กจะขยายลงไปยังที่อยู่หน่วยความจำที่ต่ำกว่า ตัวชี้สแต็กจะชี้ไปที่ปลายของสแต็กที่การกระทำทั้งหมดเกิดขึ้นเพียงปลายนั้นอยู่ที่หน่วยความจำต่ำกว่าฐานของสแต็ก


2

ตัวชี้สแต็กถูกใช้ (พร้อมตัวชี้เฟรม) สำหรับสแต็กการโทร (ตามลิงก์ไปยังวิกิพีเดียซึ่งมีภาพที่ดี)

สแตกการโทรประกอบด้วยเฟรมการโทรซึ่งมีที่อยู่ผู้ส่งตัวแปรท้องถิ่นและข้อมูลท้องถิ่นอื่น ๆ (โดยเฉพาะเนื้อหาที่หกของการลงทะเบียน; formals)

อ่านยังเกี่ยวกับการโทรหาง (บางสายหาง recursive ไม่จำเป็นต้องมีกรอบการเรียกร้องใด ๆ ) การจัดการข้อยกเว้น (เช่นsetjmp และ longjmpพวกเขาอาจเกี่ยวข้องกับการ popping เฟรมสแต็คหลายครั้ง) สัญญาณและการขัดจังหวะและต ดูเพิ่มเติมที่การเรียกการประชุมและแอปพลิเคชันอินเทอร์เฟซแบบไบนารี (ABIs) โดยเฉพาะอย่างยิ่งx86-64 ABI (ซึ่งกำหนดว่าอาร์กิวเมนต์บางอย่างที่เป็นทางการถูกส่งผ่านโดยรีจิสเตอร์)

นอกจากนี้ให้เขียนโค้ดฟังก์ชันแบบง่าย ๆ ใน C จากนั้นใช้gcc -Wall -O -S -fverbose-asm ในการคอมไพล์แล้วมองเข้าไปใน.s ไฟล์แอสเซมเบลอร์ที่สร้างขึ้น

Appel เขียนกระดาษ 1986 ฉบับเก่าที่อ้างว่าการรวบรวมขยะสามารถทำได้เร็วกว่าการจัดสรรสแต็ก (โดยใช้สไตล์การส่งต่อต่อเนื่องในคอมไพเลอร์) แต่นี่อาจเป็นเท็จในโปรเซสเซอร์ x86 ในปัจจุบัน (สะดุดตาเพราะลักษณะพิเศษแคช)

ขอให้สังเกตว่าการเรียกแบบแผน ABIs และรูปแบบสแต็กจะแตกต่างกันใน 32 บิต i686 และ 64 บิต x86-64 นอกจากนี้การเรียกประชุม (และผู้ที่รับผิดชอบในการจัดสรรหรือ popping กรอบการโทร) อาจแตกต่างกันกับภาษาที่แตกต่างกัน (เช่น C, Pascal, Ocaml, SBCL สามัญ Lisp มีการประชุมโทรที่แตกต่างกัน .... )

BTW, ส่วนขยาย x86 ล่าสุดเช่นAVXกำลังกำหนดข้อ จำกัด การจัดตำแหน่งที่มีขนาดใหญ่มากขึ้นบนตัวชี้สแต็ก (IIRC กรอบการโทรบน x86-64 ต้องการจัดตำแหน่งให้เท่ากับ 16 ไบต์เช่นสองคำหรือตัวชี้)


1
คุณอาจต้องการพูดถึงว่าการจัดตำแหน่ง 16 ไบต์บน x86-64 หมายถึงสองเท่าของขนาด / การจัดตำแหน่งของตัวชี้ซึ่งจริงๆแล้วน่าสนใจยิ่งกว่าจำนวนไบต์
Deduplicator

1

กล่าวอย่างง่ายๆโปรแกรมใส่ใจเพราะใช้ข้อมูลนั้นและต้องการติดตามว่าจะหาได้ที่ไหน

หากคุณประกาศตัวแปรท้องถิ่นในฟังก์ชั่นสแต็คเป็นที่ที่พวกเขาจะถูกเก็บไว้ นอกจากนี้หากคุณเรียกใช้ฟังก์ชันอื่นสแต็กจะเป็นที่ที่เก็บที่อยู่ผู้ส่งเพื่อให้สามารถกลับไปยังฟังก์ชันที่คุณอยู่เมื่อที่ที่คุณเรียกว่าเสร็จสิ้นและรับที่ที่เหลือ

หากไม่มี SP การเขียนโปรแกรมที่มีโครงสร้างตามที่เรารู้ว่ามันเป็นไปไม่ได้ (คุณสามารถหลีกเลี่ยงที่จะไม่มีมันได้ แต่มันจะต้องมีการติดตั้งเวอร์ชันของคุณเองดังนั้นมันจึงไม่แตกต่างกันมากนัก)


1
การยืนยันของคุณว่าการเขียนโปรแกรมที่มีโครงสร้างโดยไม่มีสแต็กจะเป็นไปไม่ได้เป็นเท็จ โปรแกรมที่คอมไพล์ด้วยรูปแบบการส่งผ่านที่ต่อเนื่องไม่มีการใช้สแต็ก แต่เป็นโปรแกรมที่เหมาะสมอย่างสมบูรณ์
Eric Lippert

@EricLippert: สำหรับค่าของ "ความรู้สึกสมบูรณ์แบบ" ไร้สาระอย่างเพียงพอที่พวกเขารวมถึงการยืนอยู่บนหัวของคนคนหนึ่งและหันตัวเองออกมาข้างในบางที ;-)
Mason Wheeler

1
ด้วยการส่งผ่านต่อเนื่องจึงเป็นไปได้ที่จะไม่จำเป็นต้องใช้ call stack เลย อย่างมีประสิทธิภาพทุกการโทรเป็นการโทรหางและกลับไปแทนที่จะส่งคืน "ในขณะที่ CPS และ TCO กำจัดแนวคิดของการส่งคืนฟังก์ชันโดยปริยายการใช้งานร่วมกันของพวกเขาสามารถขจัดความต้องการสแต็คแบบรันไทม์"

@MichaelT: ฉันพูดว่า "เป็นหลัก" เป็นไปไม่ได้ด้วยเหตุผล CPS ในทางทฤษฎีสามารถทำได้ แต่ในทางปฏิบัติมันจะกลายเป็นเรื่องยากขันอย่างรวดเร็วในการเขียนโค้ดจริงในโลกของความซับซ้อนใด ๆ ใน CPS, เอริคชี้ให้เห็น ในชุดของบล็อกโพสต์เกี่ยวกับเรื่องนี้
Mason Wheeler

1
@MasonWheeler Eric กำลังพูดถึงโปรแกรมที่คอมไพล์ลงใน CPS ตัวอย่างเช่นการอ้างอิงบล็อกของ Jon Harrop : In fact, some compilers don’t even use stack frames [...], and other compilers like SML/NJ convert every call into continuation style and put stack frames on the heap, splitting every segment of code between a pair of function calls in the source into its own separate function in the compiled form.นั่นแตกต่างจาก "การใช้ [สแต็ค] เวอร์ชันของคุณเอง"
Doval

1

สำหรับตัวประมวลผลสแต็กในโปรเซสเซอร์ x86 ความคล้ายคลึงของจานสแต็คนั้นไม่ถูกต้องจริงๆ
ด้วยเหตุผลต่าง ๆ (ส่วนใหญ่เป็นประวัติการณ์) ตัวประมวลผลสแต็กจะเติบโตจากด้านบนของหน่วยความจำไปยังด้านล่างของหน่วยความจำดังนั้นการเปรียบเทียบที่ดีกว่าจะเป็นเชนของลิงก์โซ่ที่ห้อยลงมาจากเพดาน เมื่อกดบางสิ่งลงบนสแต็กลิงก์ลูกโซ่จะถูกเพิ่มไปยังลิงก์ที่ต่ำที่สุด

ตัวชี้สแต็กหมายถึงลิงก์ต่ำสุดของโซ่และใช้โดยโปรเซสเซอร์เพื่อ "ดู" โดยที่ลิงก์ต่ำสุดนั้นคือลิงก์นั้นสามารถเพิ่มหรือลบลิงค์ได้โดยไม่ต้องเดินทางโซ่ทั้งหมดจากเพดานลงมา

ในแง่หนึ่งภายในโปรเซสเซอร์ x86 สแต็กนั้นกลับหัวกลับหาง แต่คำศัพท์สแต็กคำศัพท์ปกติจะถูกใช้ดังนั้นลิงก์ที่ต่ำที่สุดจึงถูกเรียกว่าเป็นด้านบนสุดของสแต็ก


ลิงก์โซ่ที่ฉันอ้างถึงข้างต้นเป็นเซลล์หน่วยความจำในคอมพิวเตอร์และพวกมันคุ้นเคยกับการจัดเก็บตัวแปรโลคัลและผลลัพธ์การคำนวณขั้นกลางบางอย่าง โปรแกรมคอมพิวเตอร์ใส่ใจที่ด้านบนสุดของสแต็ก (เช่นที่ลิงก์ต่ำที่สุดแฮงค์) เนื่องจากตัวแปรส่วนใหญ่ที่ฟังก์ชั่นต้องการเข้าถึงมีอยู่ใกล้กับตำแหน่งที่ตัวชี้สแต็กอ้างถึงและเป็นที่ต้องการอย่างรวดเร็ว


1
The stack pointer refers to the lowest link of the chain and is used by the processor to "see" where that lowest link is, so that links can be added or removed without having to travel the entire chain from the ceiling down.ฉันไม่แน่ใจว่านี่เป็นการเปรียบเทียบที่ดี ในความเป็นจริงลิงค์ไม่เคยเพิ่มหรือลบ ตัวชี้แบบสแต็กเป็นเหมือนเทปบิตที่คุณใช้เพื่อทำเครื่องหมายลิงก์ใดลิงก์หนึ่ง ถ้าคุณสูญเสียเทปนั้นคุณจะไม่ได้มีวิธีการที่จะทราบว่าเป็นด้านล่างสุดของลิงค์ที่คุณใช้ในทุก ; การเดินทางโซ่จากเพดานลงมาจะไม่ช่วยคุณ
Doval

ดังนั้นตัวชี้สแต็กจึงมีจุดอ้างอิงที่โปรแกรม / คอมพิวเตอร์สามารถใช้เพื่อค้นหาตัวแปรโลคัลของฟังก์ชันได้?
moonman239

หากเป็นเช่นนั้นคอมพิวเตอร์จะหาตัวแปรท้องถิ่นได้อย่างไร มันจะไปค้นหาทุกที่อยู่หน่วยความจำจากล่างขึ้นบนหรือไม่?
moonman239

@ moonman239: ไม่เมื่อรวบรวมคอมไพเลอร์คอยติดตามตำแหน่งที่เก็บตัวแปรแต่ละตัวที่สัมพันธ์กับตัวชี้สแต็ก หน่วยประมวลผลเข้าใจที่อยู่ที่เกี่ยวข้องดังกล่าวเพื่อให้การเข้าถึงตัวแปรโดยตรง
Bart van Ingen Schenau

1
@BartvanIngenSchenau อ่าโอเค ชนิดของความรู้สึกเมื่อคุณออกไปกลางไม่มีที่ไหนเลยและคุณต้องการความช่วยเหลือดังนั้นคุณจึงให้ 911 แนวคิดที่ว่าคุณอยู่ที่ไหนเมื่อเทียบกับสถานที่สำคัญ ในกรณีนี้ตัวชี้แบบสแต็กมักเป็น "จุดสังเกต" ที่ใกล้ที่สุดและอาจเป็นจุดอ้างอิงที่ดีที่สุด
moonman239

1

คำตอบนี้หมายถึงเฉพาะชี้สแต็คของเธรดปัจจุบัน (ของการดำเนินการ)

ในภาษาโปรแกรมโพรซีเดอร์โพรซีเดอร์โดยทั่วไปมีการเข้าถึงสแต็ก1สำหรับวัตถุประสงค์ต่อไปนี้:

  • โฟลว์ควบคุมคือ "call stack"
    • เมื่อฟังก์ชั่นหนึ่งเรียกใช้ฟังก์ชันอื่นสแตกการโทรจะจดจำตำแหน่งที่จะกลับไป
    • สแต็คโทรเป็นสิ่งจำเป็นเพราะนี่คือวิธีการที่เราต้องการ "ฟังก์ชั่นเรียกว่า" การปฏิบัติตน - "เพื่อรับที่เราออกมา"
    • มีรูปแบบการเขียนโปรแกรมอื่น ๆ ที่ไม่มีการเรียกใช้ฟังก์ชันในระหว่างการดำเนินการ (เช่นอนุญาตให้ระบุฟังก์ชั่นถัดไปเมื่อถึงจุดสิ้นสุดของฟังก์ชั่นปัจจุบัน) หรือไม่มีการเรียกใช้ฟังก์ชั่นเลย (เฉพาะการใช้ข้าม ) สไตล์การเขียนโปรแกรมเหล่านี้อาจไม่จำเป็นต้องมี call stack
  • พารามิเตอร์การเรียกใช้ฟังก์ชัน
    • เมื่อฟังก์ชันเรียกใช้ฟังก์ชันอื่นพารามิเตอร์สามารถส่งไปยังสแต็กได้
    • มันเป็นสิ่งจำเป็นสำหรับผู้โทรและผู้รับสายที่จะปฏิบัติตามแบบแผนเดียวกันกับผู้ที่รับผิดชอบในการล้างพารามิเตอร์จากสแต็คเมื่อการโทรเสร็จสิ้น
  • ตัวแปรท้องถิ่นที่อาศัยอยู่ภายในการเรียกใช้ฟังก์ชัน
    • โปรดสังเกตว่าตัวแปรโลคัลที่เป็นของผู้เรียกสามารถทำให้ผู้เข้าชมเข้าถึงได้โดยการส่งตัวชี้ไปยังตัวแปรโลคอลนั้นไปยัง callee

หมายเหตุ1 : เฉพาะสำหรับการใช้งานของเธรดแม้ว่าเนื้อหาจะสามารถอ่านได้อย่างสมบูรณ์ - และต่อยได้ - โดยเธรดอื่น ๆ

ในการเขียนโปรแกรมแอสเซมบลี C และ C ++ วัตถุประสงค์ทั้งสามสามารถทำได้โดยกองซ้อนเดียวกัน ในบางภาษาอื่น ๆ วัตถุประสงค์บางอย่างอาจสำเร็จได้ด้วยสแต็คแยกต่างหากหรือหน่วยความจำที่จัดสรรแบบไดนามิก


1

นี่คือเวอร์ชั่นที่มีการปรับให้เกินความโดยเจตนาของสแต็กที่ใช้

ลองนึกภาพกองเป็นการ์ดดัชนี ตัวชี้สแต็กชี้ไปที่การ์ดด้านบน

เมื่อคุณเรียกใช้ฟังก์ชัน:

  • คุณเขียนที่อยู่ของรหัสทันทีหลังจากบรรทัดที่เรียกว่าฟังก์ชั่นบนการ์ดและวางลงบนกอง (เช่นคุณเพิ่มตัวชี้สแต็คทีละหนึ่งและเขียนที่อยู่ไปยังที่ที่ชี้ไป)
  • จากนั้นคุณเขียนค่าที่มีอยู่ในทะเบียนลงบนการ์ดและวางลงบนกอง (เช่นคุณเพิ่มตัวชี้สแต็กตามจำนวนของรีจิสเตอร์และคัดลอกเนื้อหารีจิสเตอร์ไปยังตำแหน่งที่ชี้ไป)
  • จากนั้นคุณวางการ์ดทำเครื่องหมายบนกอง (เช่นคุณบันทึกตัวชี้สแต็กปัจจุบัน)
  • จากนั้นคุณเขียนค่าของแต่ละพารามิเตอร์ที่ฟังก์ชั่นถูกเรียกใช้โดยหนึ่งรายการไปยังการ์ดและวางลงบนกอง (เช่นคุณเพิ่มตัวชี้สแต็กตามจำนวนพารามิเตอร์และเขียนพารามิเตอร์ไปยังตำแหน่งที่ตัวชี้สแต็กชี้ไป)
  • จากนั้นคุณเพิ่มการ์ดสำหรับตัวแปรท้องถิ่นแต่ละตัวอาจเขียนค่าเริ่มต้นไว้ (เช่นคุณเพิ่มตัวชี้สแต็กตามจำนวนของตัวแปรในเครื่อง)

ณ จุดนี้รหัสในฟังก์ชั่นทำงาน รหัสจะถูกรวบรวมเพื่อให้รู้ว่าแต่ละการ์ดนั้นสัมพันธ์กับด้านบนสุดอย่างไร ดังนั้นจึงรู้ว่าตัวแปรxคือไพ่ใบที่สามจากด้านบน (เช่นตัวชี้สแต็ค - 3) และพารามิเตอร์yคือไพ่ใบที่หกจากด้านบน (เช่นตัวชี้สแต็ค - 6)

วิธีการนี้หมายความว่าที่อยู่ของตัวแปรท้องถิ่นหรือพารามิเตอร์แต่ละตัวไม่จำเป็นต้องถูกอบเข้ารหัส แต่ไอเท็มข้อมูลเหล่านี้ทั้งหมดจะได้รับการจัดการสัมพันธ์กับตัวชี้สแต็ก

เมื่อฟังก์ชันส่งคืนการดำเนินการย้อนกลับเป็นเพียง:

  • มองหาการ์ดทำเครื่องหมายและโยนไพ่ทั้งหมดที่อยู่ด้านบนออกไป (เช่นตั้งค่าตัวชี้สแต็กเป็นที่อยู่ที่บันทึกไว้)
  • คืนค่าการลงทะเบียนจากการ์ดที่บันทึกไว้ก่อนหน้าและโยนทิ้ง (เช่นลบค่าคงที่จากตัวชี้สแต็ก)
  • เริ่มเรียกใช้รหัสจากที่อยู่บนการ์ดด้านบนแล้วโยนรหัสนั้นออกไป (เช่นลบ 1 จากตัวชี้สแต็ก)

สแต็กกลับมาอยู่ในสถานะเดิมก่อนหน้าที่จะเรียกใช้

เมื่อคุณพิจารณาสิ่งนี้ให้สังเกตสองสิ่ง: การจัดสรรและการยกเลิกการจัดสรรตำแหน่งท้องถิ่นเป็นการดำเนินการที่รวดเร็วมากเนื่องจากเพิ่งเพิ่มหมายเลขไปยังหรือลบตัวเลขออกจากตัวชี้สแต็ก นอกจากนี้โปรดทราบว่าการทำงานแบบนี้กับการเรียกซ้ำเป็นธรรมชาติอย่างไร

สิ่งนี้เกินความจริงเพื่อวัตถุประสงค์ในการอธิบาย ในทางปฏิบัติพารามิเตอร์และคนในท้องถิ่นอาจถูกนำไปลงทะเบียนเป็นการปรับให้เหมาะสมและโดยทั่วไปตัวชี้สแต็กจะเพิ่มขึ้นและลดลงตามขนาดคำของเครื่องไม่ใช่อย่างใดอย่างหนึ่ง (เพื่อบอกชื่อสองสามอย่าง)


1

ภาษาการเขียนโปรแกรมสมัยใหม่ดังที่คุณทราบดีสนับสนุนแนวคิดของการเรียกรูทีนย่อย (ส่วนใหญ่มักเรียกว่า "การเรียกใช้ฟังก์ชัน") ซึ่งหมายความว่า:

  1. ในช่วงกลางของรหัสคุณสามารถเรียกฟังก์ชั่นอื่น ๆ ในโปรแกรมของคุณ
  2. ฟังก์ชั่นนั้นไม่รู้ว่ามันถูกเรียกจากที่ใด
  3. อย่างไรก็ตามเมื่องานของมันเสร็จสิ้นและมันreturnควบคุมกลับไปที่จุดที่เริ่มต้นการโทรด้วยค่าตัวแปรท้องถิ่นทั้งหมดมีผลบังคับใช้เมื่อเริ่มต้นการโทร

คอมพิวเตอร์ติดตามได้อย่างไร มันเก็บบันทึกอย่างต่อเนื่องของฟังก์ชั่นที่รอการโทรกลับ บันทึกนี้เป็นกองและเพราะมันเป็นเช่นที่สำคัญอย่างหนึ่งที่เราเรียกว่าปกติสแต็ค

และเนื่องจากรูปแบบการโทร / ส่งคืนมีความสำคัญดังนั้น CPU จึงได้รับการออกแบบมาเป็นเวลานานเพื่อให้การสนับสนุนฮาร์ดแวร์พิเศษสำหรับมัน ตัวชี้สแต็กเป็นคุณลักษณะฮาร์ดแวร์ใน CPUs - รีจิสเตอร์ที่ทุ่มเทเพื่อติดตามด้านบนสุดของสแต็กและใช้ตามคำแนะนำของซีพียูเพื่อแยกย่อยเป็นรูทีนย่อยและส่งคืนจากมัน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.