เมื่อ 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 ด้วยนะ)

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

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

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

แต่เราจะมาคลายความเข้าใจผิดอะไรบางอย่างละกัน โดยเฉพาะ การที่บอกว่า เราไม่ควรที่จะตั้งจำนวน 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 ที่เหมาะสมกับโปรแกรมของเราอีกที