By Arnon Puitrakul - 03 พฤษภาคม 2024
หากเราเปรียบเทียบภาษา Programming ที่เกิดขึ้นในยุคใหม่ ๆ เช่น Python และ Go เทียบกับภาษาเก่า ๆ หน่อยอย่าง C และ C++ ทำไมพวกภาษาใหม่ ๆ ถึงไม่มีการ Implement Feature สำหรับการเข้าถึง Memory โดยตรงอย่าง Pointer Concept ใน C การอนุญาติให้ใช้ Pointer และการไม่อนุญาติ มันมีข้อดีข้อเสียอย่างไร และ ในเมื่อภาษาใหม่ ๆ มันไม่มีการใช้ Concept พวกนี้แล้ว ในฐานะนักคอมพิวเตอร์เรายังต้องเรียนเรื่องพวกนี้อีกมั้ย
Pointer มันคือการเก็บ Memory Address เป็นตัวแปรนึง ซึ่งเราสามารถใช้ Pointer นี้ชี้ไปที่ค่าในอีกจุดของ Memory หรือกระทั่งการชี้ (Reference) ไปที่จุดใน Memory ส่วนอื่น ๆ ของเครื่องคอมพิวเตอร์ได้ และสามารถเอาค่าที่อยู่ใน Address เหล่านั้นออกมา เราเรียกว่า Dereferencing
int a = 10;
// Referencing
int *aAddr = &a;
// Dereferencing
printf("%d", *aAddr);
ตัวอย่างจาก Snippet ด้านบน เป็นภาษา C เริ่มจากสร้างตัวแปร a ที่เก็บค่า 10 เอาไว้ จากนั้น เราทดลอง Referencing ให้ดู โดยการสร้าง Pointer ขึ้นมาตัวนึง โดยให้มันมีค่าเป็น Address ของตัวแปร a ที่เราสร้างไว้ก่อนหน้านี้ และสุดท้าย เราต้องการเอาค่าของตัวแปร a ออกมา โดยการ Dereferencing ค่า Address ที่อยู่ใน aAddr ซึ่งคือ Address ของตัวแปร a เมื่อเราเอาค่าออกมา มันเลยมีค่าเป็น 10 นั่นเอง
ก่อนหน้านั้น เราต้องเข้าใจก่อนว่า Pointer เป็น Concept ที่เกิดขึ้นมานานมาก ๆ แล้ว ถ้าเราลองดูภาษาเก่า ๆ อย่าง C เราจะเห็นชัดมากว่า การใช้ Concept การเข้าถึงข้อมูลผ่านการใช้ Pointer มันมีอยู่แล้ว หรือในภาษาที่ Low Level ลงมาอีกอย่าง Assembly ก็มี Pointer เหมือนกัน ทำให้เกิดคำถามว่า ทำไมเราจะต้องใช้ Pointer ละ
เหตุผลแรก เราลองจินตนาการว่า เราต้องโปรแกรมเครื่องคอมพิวเตอร์เก่า ๆ ที่มี Memory อยู่สัก 8kB สิ่งที่เราต้องทำคือ เราต้องการ Transform จำนวนที่มากกว่า 10 ให้เป็น 0 จากตัวเลขจำนวน 1,500 ตัว และสำหรับคนที่เขียนโปรแกรม ถ้าเราพูดถึง Integer นั่นแปลว่า ตัวเลข 1 ตัวมีขนาด 4 kB ดังนั้นตัวเลขทั้งหมดควรมีขนาดประมาณ 6 kB
for (int i=0;i<1500;i++) {
if (dataset[i] >= 10)
dataset[i] = 0;
}
การทำ Linear Search ปกติ มันดูเป็นเรื่องง่ายมาก ๆ ถ้าเกิดว่า เราต้องการทำให้เร็วขึ้นละ เราเขียนโดยใช้ Parallel Programming ได้มั้ย
เราจะต้องแบ่งงานออกมา เป็นส่วน ๆ เช่น มี 2 Thread ช่วยกัน เราจะต้องแบ่งครึ่งตัวเลขกันออกไป เช่น Thread 1 จัดการตัวเลขลำดับที่ 0-749 และ Thread 2 จัดการตัวเลขลำดับที่ 750-1,499 ดังนั้น เราจะต้อง Copy ตัวเลขแต่ละชุดไปที่แต่ละ Thread และค่อยเอาผลมารวมกัน
นั่นแปลว่า เราจะมีตัวเลขชุดเดิมก่อนหั่นอยู่ 6 kB และ ตัวเลขที่หั่นไป 2 ชิ้น ชิ้นละ 3 kB นั่นแปลว่า เราจะต้องใช้ Memory ขนาด 12 kB ซึ่งเกิน 8kB ที่เรามีไปแล้วอะสิ ทำยังไงดี
Pointer เข้ามาช่วยเรื่องนี้ได้เป็นอย่างดี แทนที่เราจะ Copy ตัวเลขแยกชุดตาม Thread เราสามารถให้ Pointer ที่มีค่าเป็น Address ตำแหน่งแรกของ Thread นั้น ๆ เช่น Thread 1 จะได้ Address ของตัวเลขตัวแรก และ Thread 2 จะได้ Address ของตัวเลขตัวที่ 750 ทำให้ เราไม่ต้องเก็บตัวเลขอะไรเพิ่มเลย นอกจาก Pointer อีก 2 ตัวเท่านั้น และ เราไม่ต้องเสียเวลา เอาผลลัพธ์ทั้ง 2 ส่วนนี้กลับมารวมกันให้กิน Memory เพิ่มอีก เพราะค่ามันถูกแก้โดยตรงใน ตัวเลขชุดต้นฉบับอยู่แล้ว
ดังนั้น เหตุผลแรกคือเรื่องของการใช้ Memory อย่างมีประสิทธิภาพมากขึ้น ทั้งในแง่ของพื้นที่ ที่เราไม่จำเป็นต้อง Copy ข้อมูลซ้ำ ๆ กัน และสามารถตัดเวลาในการ Copy ออกไปได้อีก อย่างน้อยที่สุดการ Reference/Dereference Pointer เร็วกว่าการ Copy ชุดตัวเลขขนาดใหญ่ ๆ แน่นอน ช่วยเพิ่ม Performance ในการทำงานบางอย่าง โดยเฉพาะในตัวอย่างนี้ที่เราทำงานเป็นรอบ ๆ (Iterable) ยิ่งถ้าเป็น Data Structure ที่ซับซ้อนกว่านี้อย่าง Graph ยิ่งรู้เรื่อง
อีกตัวอย่าง เป็นตัวอย่างที่ตอนเรียนเราโคตรสงสัย พอมาอ่านเข้าจริง เชี้ยยยยย เออหวะ Mega Clever มาก คือ การ Abstract Data Type ด้วย Void Pointer ปกติเวลาภาษา C เป็นภาษาที่ อยู่ในกลุ่ม Strong Type หรือ เราจำเป็นต้องกำหนด Type ของตัวแปรทุกครั้ง เราจะเห็นได้จากการประกาศตัวแปรในภาษา C เทียบกับภาษาอื่นอย่าง Python ที่อยู่ ๆ ตัวแปรจะเป็น Integer รันไปอีกหน่อย อ้าวเป็น String ซะงั้น
แต่บางครั้ง เอาเข้าจริงมันมีเคสที่ เราไม่แน่ใจว่ามันจะเป็น Type อะไร เราจะแก้ปัญหานี้อย่างไร ก็โดยการใช้ Void Pointer เข้ามานี่แหละ คือ เรามี Void Pointer ที่มีค่าเป็น Address ที่เราจองเอาไว้ผ่าน malloc() ถึงเวลาจริง เราจะ Cast ให้มันเป็นอะไรก็ได้ ตามใจเราเลย เป็น Concept ที่ดูเหมือนจะไม่ได้ใช้นะ แต่มันอยู่ใน Core Language เลยทีเดียว กระทั่งคำสั่ง malloc ที่เราใช้จอง Memory ลองไปอ่านใน Document มันได้ว่ามันเป็น Function ที่ Return Void Pointer กลับมาจริง ๆ
ทั้งสองตัวอย่างที่เล่ามาเป็นเพียงตัวอย่างหนึ่งในการเอา Pointer เข้ามาใช้ มันเป็น Concept ที่โคตร Powerful และ Versatile มาก ๆ นอกจากนั้นยังมีเรื่องของการ Link Library ต่าง ๆ เข้ามา ก็ใช้หลักการนี้ด้วยเช่นกัน ตัวอย่างอื่น ๆ เราสามารถไปหาศึกษาได้เยอะมาก ๆ ลองไปอ่านดู
ถ้าของมันดีย์จริง ใครมันจะอดใจไม่ใช้ไหวใช่ป่ะ จริง ๆ มันไม่ได้มีแต่ของดีซะทีเดียว การที่เราเข้าถึง Memory ลักษณะนี้มันก็มีปัญหาเหมือนกัน
เรื่องแรกคือ ความปลอดภัย เนื่องจากเราสามารถเข้าถึง Memory ได้โดยตรง อยู่ ๆ เราอยากกระโดดจากจุดนึงไปอีกจุดนึงได้แบบ ง่ายเฉย ๆ มันไม่ปลอดภัยเท่าไหร่ โดยเฉพาะ OS สมัยก่อนที่เขาไม่มี กลไกในการป้องกัน Memory นั่นหมายความว่า Application ที่รันอยู่ในเครื่องนั้นสามารถเข้าถึง Memory ทุกส่วนได้หมดพร้อม ๆ กัน และบางครั้งโปรแกรมอาจจะต้องมีการเก็บข้อมูลสำคัญไว้ใน Memory หากเราไม่มีกลไกการป้องกันนี้เลย มันก็อาจจะมีคนสร้างโปรแกรมขึ้นมาอ่าน Memory ส่วนนั้นขึ้นมา เป็นการขโมยข้อมูลได้เลย หรือหนักกว่านั้นสามารถเขียนข้อมูลบางอย่างลงไปใน Memory ของ Application อื่น จนทำให้ทำงานผิดพลาด หรือเป็น Attack Surface สำหรับการโจมตีอื่น ๆ ที่จะตามมาได้
อาจจะคิดว่า เห้ย มันเป็นปัญหาขนาดนี้ แล้วทำไม Concept นี้มันยังถูกเอามาใช้ละ ต้องเข้าใจก่อนนะว่า Pointer มันถูกคิดขึ้นมานานมาก ๆ แล้ว สักช่วง 70s ตอนนั้นคอมพิวเตอร์บนโลกเรายังมีไม่มาก อยู่ในกลุ่มนักวิจัยเท่านั้น ทำให้ไม่มีใครมานั่งแคร์เรื่องความปลอดภัยเท่าไหร่
สำหรับปัจจุบัน ปัญหานี้เราเบาใจได้หน่อย เพราะในปัจจุบัน OS ส่วนใหญ่มีการ Implement ระบบการป้องกัน Memory แล้ว เช่นการใช้ Virtual Addressing และ กลไกอื่น ๆ เช่น Rings, Segmentation และ Masking (อยากหาอ่านเพิ่มไปทรมานได้ในวิชา OS) แต่ไม่ว่ายังไง คนมันจะทะลวงมันต้องไปให้สุด ทำให้เราอาจจะเจอพวก การโจมตีอย่างการทำให้ Stack Overflow เข้าไปในพื้นที่ของ Application ข้าง ๆ ทำให้โปรแกรมเหล่านั้นสามารถเข้าถึง Memory ที่ไม่ควรจะเข้าถึงได้
เรื่องที่ 2 มันอาจก่อให้เกิดปัญหา Memory Leak ได้ เพราะการใช้ Pointer มันคือการให้อำนาจเราจัดการ Memory เองเกือบทั้งหมด ตั้งแต่ การจอง และ การปล่อย Memory ออกไป โดย Memory มันเป็นสมบัติที่มีอยู่อย่างจำกัดในระบบคอมพิวเตอร์ เมื่อเราจองแล้วใช้เสร็จแล้ว เราก็ควรจะคืน หรือปล่อยมันให้กับ OS เพื่อเอาให้ Application อื่น ๆ ใช้งานต่อ แต่ถ้าเราจองแล้วเราลืมคืนมันด้วยความผิดพลาดบางอย่างละ นั่นแหละคือสิ่งที่เรียกว่า Memory Leak อาการที่เราจะเจอได้คือ ทำไม Application รันแล้วมันกิน Memory เพิ่มขึ้นเรื่อย ๆ อย่างไร้เหตุผล
ตัวอย่างที่เราเจอมาล่าสุดคือ Adobe Lightroom บน Apple Silicon เขียนไว้ตั้งแต่ปี 2022 จนตอนนี้ปี 2024 ก็ยังไม่ได้รับการแก้ไขเลยนะ อาจจะคิดว่า มันอาจจะใช้ Memory เยอะรึเปล่า แต่อาการที่เจอคือ เมื่อเราเข้า Develop Mode มันค่อย ๆ กิน Memory เยอะมากขึ้นเรื่อย ๆ ยิ่งทำแล้วเลื่อนรูปไปเรื่อย ๆ มันกินเยอะขึ้นเรื่อย ๆ จน ปิดโปรแกรม Kill Process และเข้าใหม่ มันจะกลับมากินน้อยเท่าเดิม แล้วพอทำรูปไปเรื่อย ๆ มันจะกินเพิ่มเรื่อย ๆ ต้องทำไปปิดเปิดโปรแกรมใหม่ไปเรื่อย ๆ นี่แหละตัวอย่างที่ดีสำหรับอาการ Memory Leak
เรื่องที่ 3 คือ ความสะดวกในการใช้งาน มันก็จริงที่ การใช้ Pointer ทำให้เราสามารถทำงานได้อย่างมีประสิทธิภาพมากกว่า ทั้งในมิติของ เวลา และ การจัดสรรพื้นที่ แต่มันเริ่มมีปัญหามากขึ้นในระบบคอมพิวเตอร์สมัยใหม่ (ใหม่ที่ว่าคือ แบบ ใหม่กว่าภาษา C Version แรกหน่อยนึงอะนะ) ที่เราเริ่มมีการนำ Concurrent Concept เข้ามาทำงานกัน เมื่อเรามีโปรแกรมหลาย ๆ ส่วนเข้าถึง Memory ส่วนเดียวกัน นั่นแปลว่า มันมีโอกาสที่จะเกิด Race Condition ขึ้นได้ ดังนั้นเป็น งานของ Programmer ที่ต้องมีความเชี่ยวชาญเพียงพอที่จะจัดการมันได้ เช่นการ Implement Queue ขึ้นมาเพื่อจัดการ หรือการใช้ Mutex หรือ Semaphore ที่เป็นกลไกของ OS เข้ามาช่วย ซึ่งมันอาศัย Learning Curve และประสบการณ์มหาศาลในการจัดการเลยก็ว่าได้ ขนาดเราเขียนโปรแกรมบน HPC มาเยอะ ยังตึง ๆ จน Deadlock งม Debug มาหลายดอกแล้ว ฮา ๆๆๆๆๆๆ
จากเหตุทั้งหมดที่เราได้เล่าไป ทำให้เราเห็นว่า การใช้ Pointer หรือการอนุญาติให้ Referencing/Dereferencing Memory มันไม่ได้มีแต่ข้อดีเสมอไป มันอาศัยว่า เรารู้ว่าเรากำลังทำอะไรอยู่ ต้องมีสติในการเขียน และ ตรวจทานโปรแกรมสูงมาก ๆ จนไปถึงทักษะการทำ Profiling ที่ดีมาก ๆ ด้วยเช่นกัน
อย่างที่ได้เล่าไปในข้อเสีย ส่วนใหญ่ ๆ เราคิดว่า เหตุคือ มันอาศัย Learning Curve ในการเรียนค่อนข้างสูง พูดตรง ๆ ว่าปัจจุบันนี้ ถ้าไม่ได้เรียน Computer Science เป็นคนมาเรียนเขียนโปรแกรม มันจะมีสักกี่คนที่นั่งสอน Advance Concept อย่าง Memory Locking Mechanism หรือ Parallel Programming กันละ (นอกจากกรูเนี่ย !!!) แล้วถ้าให้คนที่ไม่รู้เรื่องพวกนี้มาเขียน เราอาจจะเจอโปรแกรมที่แตกยับ ๆ กันได้แน่นอน
ประกอบกับระบบคอมพิวเตอร์ในปัจจุบัน เรามีความเร็วสูงมากกว่าเดิม และมี Memory ที่มากกว่าเดิมแล้ว การที่เราจะ Abstract เรื่องการจัดการ Memory พวกนี้ไป ยอม Trade-off ความสามารถสักหน่อย เสริมระบบการจัดการอย่าง Garbage Collection แล้วให้ Programmer มา Focus กับ Business Logic จริง ๆ ของโปรแกรมอาจจะเป็นเรื่องที่ดีกว่าในบางเคสก็ได้
แต่แน่นอนว่า มันไม่ได้เป็นแบบนั้นทุกเคส มันก็ยังมีเคสที่เรายังคงต้องใช้ Pointer อยู่ เพราะการที่เรา Abstract Concept พวกนี้ไปเบื้องหลังของมันก็คือการเขียนโปรแกรมมาไว้ให้แล้ว เราเลยเรียกภาษาที่มันมีการ Implement ส่วนประกอบหลาย ๆ อย่างที่จำเป็นมาว่า มันเป็น High-Level Language กลับกันภาษาที่ Plain มาก ๆ แทบไม่มีอะไรเลย เราจะเรียกว่า Low-Level Language
และเราไม่ได้บอกนะว่า ภาษาใหม่ ๆ ทุกตัวมันจะ High-Level หมด เช่น Rust พึ่งเกิดมาในปี 2015 นี่เอง ถือว่าใหม่พอสมควร เป็นภาษาที่เราชอบมาก ๆ มันมีเครื่องมือมา Abstract บางเรื่องให้เราและให้อิสระกับเรามาก ในสัดส่วนที่พอดีมาก ๆ อ่านเข้าใจง่าย เข้ากับ Design Concept สมัยใหม่มากกว่าภาษาเก่า ๆ บางตัว
เอาเข้าจริง มันไม่มีภาษาอะไรดีกว่าภาษาอะไรหรอก ภาษาเป็นเหมือน เครื่องมือ มากกว่า เราแค่หยิบเครื่องมือที่เหมาะกับเราขึ้นมาใช้เท่านั้นเอง หรือไม่ก็เปรียบกับรถเกียร์กระปุก กับเกียร์ Auto เราไปถึงที่ได้เหมือนกัน แค่ว่าเราจะไปแบบไหนเท่านั้นแหละ เช่น เราอยากจะ Take Control กลไกบางอย่าง เราก็อาจจะต้องเลือกเครื่องมือ หรือภาษาที่อนุญาติให้เราทำแบบนั้น หรือ เราไม่จำเป็นต้องยุ่งกับมัน เราก็แค่เลือกภาษาที่มีเครื่องมือจัดการให้เรา เราก็จะทำงานง่ายขึ้นเยอะ
ส่วนตัวเรานะ เครื่องมือที่มีให้ในปัจจุบัน มันเข้ามาช่วยเราทำงานได้มาก และเกือบทุกภาษาเป็น Turing Complete ความจำเป็นในการเรียน Advance Concept พวกนี้มันค่อย ๆ ลดน้อยลงเรื่อย ๆ สำหรับคนที่เขียนโปรแกรมเฉย ๆ อาจจะไม่ได้จำเป็นขนาดนั้น แบบเหมือนเราบอกว่า เออ เราอยากเขียนเว็บ แกต้องไปเรียน Assembly ก่อนมันก็ไม่ใช่เรื่อง
แต่ถ้าเราเขียนเข้าไปลึกจริง ๆ โดยเฉพาะคนที่เรียน Computer Science มันเป็น Concept ที่สำคัญมาก ๆ เพราะการที่เราเรียน Computer Science เราไม่ได้โฟกัสแค่การทำให้โปรแกรมทำงานได้อย่างถูกต้องเท่านั้น แต่มันทำให้เราเขียนโปรแกรมได้อย่างมีประสิทธิภาพ การที่เราจะทำแบบนั้นได้ เราจะต้องรู้ด้วยว่า คอมพิวเตอร์ทำงานอย่างไร กว่าคำสั่งสักตัวจะรันได้มันเกิดอะไรขึ้นบ้าง ทำไมโปรแกรมเราถึงทำงานได้ถูกต้องละ ทำไมโปรแกรมนี้ทำงานได้เร็วกว่า นี่แหละมันคือจุดที่แยกระหว่างคนที่เรียนคอมพิวเตอร์ตรงมาเขาต้องเรียนทุกอย่าง (ใช่ เรียน Assembly จริง ๆ และทุกวันนี้เสือกต้องเขียน กับ Debug อีกนะ นี่สินะ เกลียดสิ่งใด ได้สิ่งนั้น มุแง๊) กับเรียนแค่การเขียนโปรแกรม
หลังจากเมื่อหลายอาทิตย์ก่อน Apple ออก Mac รัว ๆ ตั้งแต่ Mac Mini, iMac และ Macbook Pro ที่ใช้ M4 กันไปแล้ว มีหลายคนถามเราเข้ามาว่า เราควรจะเลือก M4 ตัวไหนดีถึงจะเหมาะกับเรา...
จากตอนก่อน เราเล่าเรื่องการ Host Website จากบ้านของเราอย่างปลอดภัยด้วย Cloudflare Tunnel ไปแล้ว แต่ Product ด้าน Zero-Trust ของนางยังไม่หมด วันนี้เราจะมาเล่าอีกหนึ่งขาที่จะช่วยปกป้อง Infrastructure และ Application ต่าง ๆ ของเราด้วย Cloudflare Access กัน...
ทุกคนเคยได้ยินคำว่า Mainframe Computer กันมั้ย เคยสงสัยกันมั้ยว่า มันต่างจากเครื่องคอมพิวเตอร์ที่เราใช้งานกันทั่ว ๆ ไปอย่างไรละ และ Mainframe ยังจำเป็นอยู่มั้ย มันได้ตายจากโลกนี้ไปหรือยัง วันนี้เรามาหาคำตอบไปด้วยกันเลย...
เคยมั้ยเวลา Deploy โปรแกรมสักตัว เราจะต้องมานั่ง Provision Infrastructure ไหนจะ VM และ Settings อื่น ๆ อีกมากมาย มันจะดีกว่ามั้ยถ้าเรามีเครื่องมือบางอย่างที่จะ Automate งานที่น่าเบื่อเหล่านี้ออกไป และลดความผิดพลาดที่อาจจะเกิดขึ้น วันนี้เราจะพาทุกคนมาทำความรู้จักกับ Infrastructure as Code กัน...