Tutorial

เมื่อ Multiprogramming และ Pandas ทำพิษ แก้ปัญหายังไงดี

By Arnon Puitrakul - 25 พฤศจิกายน 2021

เมื่อ Multiprogramming และ Pandas ทำพิษ แก้ปัญหายังไงดี

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

นั่นคือการใช้ Concept เรื่องของ Multiprogramming เข้ามาช่วย เช่นการกระจายงานไปตาม Thread ต่าง ๆ เพื่อให้มันทำงานได้เร็วขึ้นบน CPU รุ่นใหม่ ๆ ที่มีจำนวน Thread ในการประมวลผลมากขึ้นเรื่อย ๆ นั่นเอง แต่เราจะบอกว่า การทำแบบนี้ตรง ๆ กับทุก ๆ เคส การเพิ่ม Thread ไม่ได้ทำให้เราได้ Performance ที่ดีขึ้นเสมอไป วันนี้เราไปหาคำตอบกันว่าทำไม และ เราจะแก้ปัญหานี้ได้อย่างไรบ้าง

ทำความรู้จักกับ Thrashing

เราเริ่มจากเคสที่ง่ายที่สุดก่อนละกัน ถ้าเราเขียนโปรแกรมแบบที่เราไม่ได้มีการเรียกใช้พวก Multiprogramming Pattern อะไรเลย เราเขียนเป็นแบบ Single Thread เราก็จะใช้งาน CPU เราได้แค่ Thread เดียว หรือถ้าเราเทียบเป็น UNIX Utilisation ก็จะได้ 100% ไป

เมื่อเราลองบอกว่า โอเค เราจะเรียกพวก Multiprogramming เข้ามา เช่นเราเรียกการใช้งาน Threading เข้ามา เราบอกว่า งั้นเราแบ่งงานไปเลย 2 Thread เอาไปกันคนละครึ่ง มันก็จะต้องมีการ แบ่งงานออกจากกัน การกระจายงาน การส่ง Signal ต่าง ๆ ระหว่าง Thread นั่นนี่ ง่าย ๆ คือ มันก็จะเริ่มมีส่วนของ Overhead เกิดขึ้นแล้ว แต่แน่นอนว่าเมื่อเราแบ่งงานกันแบบนี้ทำให้เราสามารถใช้ Utilisation ได้มากขึ้นอาจจะเป็น 200% ไปก็เป็นได้ และเราเพิ่มแบบนี้ไปเรื่อย ๆ ก็ดูเหมือนเราจะใช้ CPU ได้มากขึ้นเรื่อย ๆ แต่จริง ๆ แล้ว มันไม่ได้เป็นแบบนั้นเลย (ไม่ได้เกี่ยวว่า เครื่องเรามีกี่ Thread ด้วยนะ)

Multiprogramming Thrashing

เมื่อเราเพิ่ม Thread ไปเรื่อย ๆ (แต่งานเราเท่าเดิม) ถึงจุดนึง CPU Utilisation เราจะทำได้น้อยลง เราเรียกอาการแบบนี้ว่า Thrashing อย่างที่เราบอกว่า การเพิ่ม Thread ไปเรื่อย ๆ มันไม่ได้เอาแค่งานไป แต่มันมี Overhead และการ Copy ข้อมูลไปมาอีกหลายเรื่องมาก ๆ ประกอบกับ Resource ของเรามันมีจำกัด ทำให้ OS มันจำเป็นที่จะต้อง เลือก หรือ สลับงาน กันไปมา แทนที่มันจะทำงานเรียง ๆ เอาให้เสร็จ แต่การสลับงานมันก็มีราคาของมันที่ต้องจ่ายในอีกหลาย ๆ ส่วนอีก ที่เราขอไม่กล่าวถึงละกันมันยาววววววววว นั่นแปลว่าเวลาในการทำงานของเรามันก็จะเพิ่มขึ้นด้วยเช่นกัน มันก็จะกลายเป็นว่า อ้าว.... ทำไมเราเพิ่ม Thread ไปแล้วมันช้าลงละ นั่นก็เป็นเพราะปรากฏการณ์แบบนี้แหละ

แล้วเราควรจะกำหนด Thread ที่เท่าไหร่ละ ?

ถามคำถามนี้ การจะหาคำตอบได้ยากอยู่ เราไม่สามารถบอก Magic Number ได้ เพราะมันไม่มีอยู่ มันขึ้นกับหลาย ๆ ปัจจัย เช่น ลักษณะการทำงานของโปรแกรม และข้อมูลต่าง ๆ ที่เราเอาเข้า และ เอาออก มันมีผลกันหมด

Multiprograming Overhead

แต่เราจะมาคลายความเข้าใจผิดอะไรบางอย่างละกัน โดยเฉพาะ การที่บอกว่า เราไม่ควรที่จะตั้งจำนวน Thread มากกว่าจำนวน Thread ใน CPU ของเรา จริง ๆ อื้ม.... ต้องบอกว่าถูกครึ่ง ผิดครึ่งละกัน เพราะจริง ๆ แล้ว มันจะมีเคสที่เราแนะนำให้ลองตั้ง Thread จำนวนเยอะกว่า Thread บน CPU จริง ๆ เช่น Thread ที่มันต้องมีจังหวะการรอ เช่นรอข้อมูลจาก Disk หรือ Network พวกนี้พอมันทำงานไปถึงจุดนึง เราลองสังเกต CPU Utilisation มันจะแกว่ง ๆ ไม่ก็รันไปแปบ ๆ อ้าว.... เหลือไม่ถึง 100% ซะงั้น เป็นเพราะมันถึงจุดที่มันต้องรอโหลดข้อมูล หรือ รับส่งข้อมูลต่าง ๆ ก่อนที่มันจะรันต่อได้ ทำให้พวกนี้แหละ เราสามารถอัด Thread ไปได้เยอะ ๆ เลย เพื่อ Fed ให้ CPU เราทำงานได้ตลอด ๆ ได้มากที่สุด มันก็จะลดเวลาในการคำนวณไปได้ แต่แน่นอนว่า การเพิ่มไปเยอะ ๆๆๆๆๆๆ เลยก็ไม่ใช่คำตอบอีก เพราะ มันก็จะไปคอขวดตรงส่วนที่รอนี่แหละ เคยเจอเคสที่ทำให้ HDD แตกมาแล้ว เพราะ HDD มันมีธรรมชาติในการอ่านเขียนไม่เหมือนกับ SSD ตอนนั้นไม่รู้ HDD พังไป 2 ลูก อ่านข้อมูลไม่ขึ้นเลย เศร้าไปอีก ดังนั้นสุดท้ายมันก็จะต้องมี Limit อยู่ดี

ว่าแต่เคสไหนละที่เราควรจะตั้งไม่เกินจำนวน Thread ที่ CPU ของเรามี ก็คือเคสที่ Thread เราเน้นการคำนวณเป็นหลักเลย เช่นเราบอกให้มันบวกเลขเยอะ ๆ ทั้งงานคือคำนวณแล้วยัดใส่ Memory ล้วน ๆ แบบนั้นแหละ เราควรที่จะไม่เริ่มต้นเลือกจำนวน Thread ที่สูงกว่าที่ CPU เราทำงานได้พร้อม ๆ กัน ไม่งั้นมันจะไปถึงจุดที่ Thrashing ได้เร็วมาก ๆ เพราะ OS มันก็รับบทแม่พระ สลับงานให้ เห็นมาเยอะ เอาไปกันคนละนิดคนละหน่อย เจอค่าสลับเข้าไปอ้วกเลย ช้ากว่าเดิมอีก

ทำให้นำไปสู่คำถามที่ว่า แล้วเราจะหายังไงละว่าเท่าไหร่ สั้น ๆ สำหรับเราเลยนะคือ ลอง เท่านั้นเลย เราอาจจะลองกับ Input ที่ใหญ่ประมาณนึง ค่อย ๆ ลองเพิ่ม ๆ ลด ๆ ไปเรื่อย ๆ สุดท้าย เราน่าจะเจอจุดที่เป็น Optimal ของมันก็ได้

เราจะกำหนดจำนวน Thread ที่ให้ Pandas ได้ยังไง ?

ในการตั้งค่าจำนวน Thread ที่เราจะให้พวกหลาย ๆ Library อย่าง Pandas และ Numpy มันทำงาน เราสามารถทำได้ผ่านการตั้ง Environment Variable ที่ชื่อว่า OMP_NUM_THREADS ถ้าใครที่เคยใช้ OpenMP น่าจะคุ้นชื่อกัน มันคือตัวเดียวกันเลย

OMP_NUM_THREADS=1 python run_benchmark.py

โดยเราสามารถกำหนดได้ตรง ๆ เลย ถ้าเป็นฝั่งของ UNIX อย่าง macOS และ Linux เราสามารถกำหนดตอนที่เรารันได้เลย เช่น Command ด้านบน หรือถ้าเป็น Windows อาจจะต้องไปหาว่าการตั้งค่า Environment Variable ทำอย่างไร เท่านี้โปรแกรมเราก็จะรันได้ตามจำนวน Thread ที่เราต้องการได้แล้ว ทั่ว ๆ ไปเริ่มต้นโปรแกรมมันจะพยายาม Parallel โดยอ้างอิงจากจำนวน Thread ที่เรามีใน CPU ล้วน ๆ เลย ทำให้ถ้าเราต้องการใช้จำนวนนั้นอยู่แล้ว เราก็ไม่จำเป็นต้องไปเซ็ตอะไรเพิ่มเด้อ

สรุป

การตั้งค่าจำนวน Thread ที่เรารัน ก็เป็นวิธีหนึ่งในการช่วยให้เรา Optmise การทำงานของโปรแกรมเราได้ (แต่ก็ทำให้มันแย่ลงได้เหมือนกัน) ขึ้นกับลักษณะของโปรแกรมที่เราทำงานด้วยว่า มันมีการรอพวก Disk หรือ Network เยอะขนาดไหน หรือต้องคำนวณตรงไหนเยอะขนาดไหนด้วย ทำให้จำนวน Thread ที่ควรตั้ง มันบอกยากมาก ไม่มี Magic Number ที่แน่นอน ดังนั้น เราจะต้องค่อย ๆ ลองเพื่อหาจำนวน Thread ที่เหมาะสมกับโปรแกรมของเราอีกที

Read Next...

จัดการข้อมูลบน Pandas ยังไงให้เร็ว 1000x ด้วย Vectorisation

จัดการข้อมูลบน Pandas ยังไงให้เร็ว 1000x ด้วย Vectorisation

เวลาเราทำงานกับข้อมูลอย่าง Pandas DataFrame หนึ่งในงานที่เราเขียนลงไปให้มันทำคือ การ Apply Function เข้าไป ถ้าข้อมูลมีขนาดเล็ก มันไม่มีปัญหาเท่าไหร่ แต่ถ้าข้อมูลของเราใหญ่ มันอีกเรื่องเลย ถ้าเราจะเขียนให้เร็วที่สุด เราจะทำได้โดยวิธีใดบ้าง วันนี้เรามาดูกัน...

ปั่นความเร็ว Python Script เกือบ 700 เท่าด้วย JIT บน Numba

ปั่นความเร็ว Python Script เกือบ 700 เท่าด้วย JIT บน Numba

Python เป็นภาษาที่เราใช้งานกันเยอะมาก ๆ เพราะความยืดหยุ่นของมัน แต่ปัญหาของมันก็เกิดจากข้อดีของมันนี่แหละ ทำให้เมื่อเราต้องการ Performance แต่ถ้าเราจะบอกว่า เราสามารถทำได้ดีทั้งคู่เลยละ จะเป็นยังไง เราขอแนะนำ Numba ที่ใช้งาน JIT บอกเลยว่า เร็วขึ้นแบบ 700 เท่าตอนที่ทดลองกันเลย...

Humanise the Number in Python with "Humanize"

Humanise the Number in Python with "Humanize"

หลายวันก่อน เราทำงานแล้วเราต้องการทำงานกับตัวเลขเพื่อให้มันอ่านได้ง่ายขึ้น จะมานั่งเขียนเองก็เสียเวลา เลยไปนั่งหา Library มาใช้ จนไปเจอ Humanize วันนี้เลยจะเอามาเล่าให้อ่านกันว่า มันทำอะไรได้ แล้วมันล่นเวลาการทำงานของเราได้ยังไง...

ทำไม 0.3 + 0.6 ถึงได้ 0.8999999 กับปัญหา Floating Point Approximation

ทำไม 0.3 + 0.6 ถึงได้ 0.8999999 กับปัญหา Floating Point Approximation

การทำงานกับตัวเลขทศนิยมบนคอมพิวเตอร์มันมีความลับซ่อนอยู่ เราอาจจะเคยเจอเคสที่ เอา 0.3 + 0.6 แล้วมันได้ 0.899 ซ้ำไปเรื่อย ๆ ไม่ได้ 0.9 เพราะคอมพิวเตอร์ไม่ได้มองระบบทศนิยมเหมือนกับคนนั่นเอง บางตัวมันไม่สามารถเก็บได้ เลยจำเป็นจะต้องประมาณเอา เราเลยเรียกว่า Floating Point Approximation...