คอมขยายสมอง (3): วิธีทำให้ไพธอนเร็วขึ้นหลายๆเท่า

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

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

โปรแกรมภาษาไพธอนนั้นเขียนง่าย อ่านง่าย แต่ขึ้นชื่อเรื่องความช้าเมื่อต้องทำงานมากๆเมื่อเทียบกับภาษาอื่นเช่น C หรือ Java

ปกติเรายินดีที่จะเขียนโปรแกรมด้วยภาษาไพธอนเพราะงานส่วนใหญ่คอมพิวเตอร์ประมวลผลแป๊บเดียว จุดเด่นที่ภาษาไพธอนเขียนง่ายและอ่านง่ายเป็นข้อได้เปรียบที่สำคัญ

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

ถ้าโปรแกรมเราไม่ได้เขียนให้ใช้ Numpy แต่เราอยากให้มันวิ่งเร็วขึ้น โดยไม่อยากดัดแปลงโปรแกรม เราสามารถลองใช้ PyPy มาเรียกโปรแกรมของเรา เมื่อเราติดตั้ง PyPy แล้วเราสามารถเรียกโปรแกรมของเราชื่อ my_program.py ด้วยคำสั่ง pypy my_program.py หรือ pypy3 my_program.py แทน python my_program.py ได้เลย ส่วนใหญ่โปรแกรมจะเร็วขึ้นหลายเท่าเหมือนกัน

อีกวิธีง่ายๆที่ควรลองคือใช้ Numba กับโปรแกรมของเราโดยใส่โค้ดเพิ่มเข้าไปไม่กี่บรรทัดแล้วโปรแกรมก็มักจะวิ่งเร็วขึ้นหลายๆเท่า

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

import random

def prob_triangle(ntrials):
    """
    ประมาณความน่าจะเป็นที่เส้นตรงที่ถูกแบ่งเป็นสามส่วนแบบสุ่มๆ
    จะสามารถประกอบเป็นสามเหลี่ยมได้
    
    ทำการทดลอง ntrials ครั้ง
    """
    
    # ntriangles เก็บจำนวนครั้งที่ประกอบเป็นสามเหลี่ยมได้
    ntriangles = 0 
    
    # ทำการทดลองแบ่งเส้นตรงความยาว 1 หน่วยเป็นสามส่วน
    # ทั้งหมดntrials ครั้ง
    
    for n in range(ntrials):
        
        # x, y คือตำแหน่งที่เราสุ่มตัดเส้นตรงสองตำแหน่ง
        # ทำให้แบ่งเส้นตรงเป็นสามส่วน
        # เราจะเรียงตำแหน่งให้ x <= y
        # ถ้าไม่เป็นอย่างนั้นเราจะสลับ x และy
        
        x = random.random()
        y = random.random()
        if x > y:
            x, y = y, x
        
        # เมื่อเราสุ่ม x, y มาตัดเส้นตรงได้แล้ว
        # เราจะมีเส้นตรงสามชิ้นยาว x, y-x, และ 1-y
        # เราจะเรียกชิ้นที่ยาวที่สุดว่า longest
        
        longest = max(x, y-x, 1-y)
        
        # ถ้าชิ้นที่ยาวที่สุดมีความยาวไม่เกิน 1/2 ของ
        # ความยาวเส้นตรงดั้งเดิม เราจะสามารถเอา
        # ทั้งสามชิ้นมาประกอบเป็นสามเหลี่ยมได้
        # และเราก็จะเพิ่มจำนวนครั้งที่ประกอบเป็นสามเหลี่ยมสำเร็จ
        
        if longest < 0.5:
            ntriangles += 1
            
         # ประมาณความน่าจะเป็นที่เป็นสามเหลี่ยมสำเร็จ
         # เท่ากับ (จำนวนสามเหลี่ยม)/(จำนวนครั้งที่ทดลอง)
            
    return ntriangles/ntrials

ลองจับเวลาให้ทดลองกับเส้นตรง 10 ล้านเส้นใช้เวลา 3.67 วินาที:

%time prob_triangle(10_000_000)
CPU times: user 3.66 s, sys: 8.11 ms, total: 3.67 s
Wall time: 3.67 s

0.2499737

เราใช้ Numba โดยพิมพ์เพิ่ม 2 บรรทัดดังนี้ (บรรทัดที่ 1 และ 4)

from numba import njit #เรียกใช้ฟังก์ชั่นต่างๆใน numba
import random

@njit() #เพิ่มบรรทัดนี้เหนือฟังก์ชั่นที่เราเขียนไว้ มักจะทำงานได้เร็วขึ้น
def prob_triangle_numba(ntrials):
    """
    ประมาณความน่าจะเป็นที่เส้นตรงที่ถูกแบ่งเป็นสามส่วนแบบสุ่มๆ
    จะสามารถประกอบเป็นสามเหลี่ยมได้
    
    ทำการทดลอง ntrials ครั้ง
    """
    
    # ntriangles เก็บจำนวนครั้งที่ประกอบเป็นสามเหลี่ยมได้
    ntriangles = 0 
    
    # ทำการทดลองแบ่งเส้นตรงความยาว 1 หน่วยเป็นสามส่วน
    # ทั้งหมดntrials ครั้ง
    
    for n in range(ntrials):
        
        # x, y คือตำแหน่งที่เราสุ่มตัดเส้นตรงสองตำแหน่ง
        # ทำให้แบ่งเส้นตรงเป็นสามส่วน
        # เราจะเรียงตำแหน่งให้ x <= y
        # ถ้าไม่เป็นอย่างนั้นเราจะสลับ x และy
        
        x = random.random()
        y = random.random()
        if x > y:
            x, y = y, x
        
        # เมื่อเราสุ่ม x, y มาตัดเส้นตรงได้แล้ว
        # เราจะมีเส้นตรงสามชิ้นยาว x, y-x, และ 1-y
        # เราจะเรียกชิ้นที่ยาวที่สุดว่า longest
        
        longest = max(x, y-x, 1-y)
        
        # ถ้าชิ้นที่ยาวที่สุดมีความยาวไม่เกิน 1/2 ของ
        # ความยาวเส้นตรงดั้งเดิม เราจะสามารถเอา
        # ทั้งสามชิ้นมาประกอบเป็นสามเหลี่ยมได้
        # และเราก็จะเพิ่มจำนวนครั้งที่ประกอบเป็นสามเหลี่ยมสำเร็จ
        
        if longest < 0.5:
            ntriangles += 1
            
         # ประมาณความน่าจะเป็นที่เป็นสามเหลี่ยมสำเร็จ
         # เท่ากับ (จำนวนสามเหลี่ยม)/(จำนวนครั้งที่ทดลอง)
            
    return ntriangles/ntrials

เมื่อเราจับเวลาจะพบว่าฟังก์ชั่นแบบใช้ Numba ของเราเร็วขึ้นมาก ใช้เวลาเพียง 0.149 วินาที แทนที่จะเป็น 3.67 วินาที หรือเร็วเป็น 25 เท่า

%time prob_triangle_numba(10_000_000)
CPU times: user 148 ms, sys: 798 µs, total: 149 ms
Wall time: 149 ms

0.2499591

ถ้าเราเรียกฟังก์ชั่นเราครั้งเดียว ผลต่างระหว่าง 3 วินาที กับ 0.15 วินาทีจะดูไม่มาก แต่เมื่อเราให้คอมพิวเตอร์เรียกฟังก์ชั่นนั้นซ้ำๆกันเป็นพันครั้ง (เช่นเมื่อเราต้องการวาดฮิสโตแกรม) เวลาที่ใช้ก็จะต่างกันมากแบบ 1 ชั่วโมง vs. 2.5 นาที ดังนั้นเราควรรู้จักเทคนิคพวกนี้เมื่อสั่งให้คอมพิวเตอร์ทำงานหนักๆแทนเรา อาจจะประหยัดเวลาได้มาก

ถ้าคอมพิวเตอร์เรามีหลายๆคอร์ซีพียู (ซึ่งคอมปัจจุบันมักจะมีอย่างน้อยสองคอร์ขึ้นไปอยู่แล้ว) เราอาจขอให้ Numba พยายามใช้คอร์หลายๆคอร์ช่วยคำนวณด้วยคำสั่ง njit(parallel=True) และใช้ prange แทน range ในการคำนวณแบบนี้ (สังเกตบรรทัดที่ 1, 4, และ 23) เมื่อจับเวลาก็พบว่าเร็วขึ้นประมาณ 6 เท่าสำหรับซีพียูแบบ 8 คอร์:

from numba import njit, prange #เรียกใช้ฟังก์ชั่นต่างๆใน numba
import random

@njit(parallel=True) #เพิ่มบรรทัดนี้เหนือฟังก์ชั่นที่เราเขียนไว้ ฟังก์ชั่นเรามักจะทำงานได้เร็วขึ้น
def prob_triangle_numba_parallel(ntrials):
    """
    ประมาณความน่าจะเป็นที่เส้นตรงที่ถูกแบ่งเป็นสามส่วนแบบสุ่มๆ
    จะสามารถประกอบเป็นสามเหลี่ยมได้
    
    ทำการทดลอง ntrials ครั้ง
    """
    
    # ntriangles เก็บจำนวนครั้งที่ประกอบเป็นสามเหลี่ยมได้
    ntriangles = 0 
    
    # ทำการทดลองแบ่งเส้นตรงความยาว 1 หน่วยเป็นสามส่วน
    # ทั้งหมดntrials ครั้ง
    
    # มีการใช้ prange แทน range เพื่อทำงานแบบพร้อมๆกัน
    # ตามจำนวนคอร์ใน CPU
    
    #ใช้ prange แทน range
    for n in prange(ntrials):
        
        # x, y คือตำแหน่งที่เราสุ่มตัดเส้นตรงสองตำแหน่ง
        # ทำให้แบ่งเส้นตรงเป็นสามส่วน
        # เราจะเรียงตำแหน่งให้ x <= y
        # ถ้าไม่เป็นอย่างนั้นเราจะสลับ x และy
        
        x = random.random()
        y = random.random()
        if x > y:
            x, y = y, x
        
        # เมื่อเราสุ่ม x, y มาตัดเส้นตรงได้แล้ว
        # เราจะมีเส้นตรงสามชิ้นยาว x, y-x, และ 1-y
        # เราจะเรียกชิ้นที่ยาวที่สุดว่า longest
        
        longest = max(x, y-x, 1-y)
        
        # ถ้าชิ้นที่ยาวที่สุดมีความยาวไม่เกิน 1/2 ของ
        # ความยาวเส้นตรงดั้งเดิม เราจะสามารถเอา
        # ทั้งสามชิ้นมาประกอบเป็นสามเหลี่ยมได้
        # และเราก็จะเพิ่มจำนวนครั้งที่ประกอบเป็นสามเหลี่ยมสำเร็จ
        
        if longest < 0.5:
            ntriangles += 1
            
         # ประมาณความน่าจะเป็นที่เป็นสามเหลี่ยมสำเร็จ
         # เท่ากับ (จำนวนสามเหลี่ยม)/(จำนวนครั้งที่ทดลอง)
            
    return ntriangles/ntrials

ถ้าจะใช้ parallel และ prange ควรอ่านเอกสารคู่มือเรื่องนี้ของ Numba ก่อนนะครับว่ามีข้อควรระวังอะไรบ้าง

หวังว่าผู้อ่านจะได้ไอเดียหรือประโยชน์บ้าง ถ้ามีคำแนะนำหรือข้อสงสัยส่งข้อความอินบ๊อกซ์ไปที่เพจวิทย์พ่อโก้บนเฟซบุ๊คได้ครับ: https://www.facebook.com/witpokosci/

(ตอนที่แล้วอยู่ที่ https://witpoko.com/?p=7485)

One thought on “คอมขยายสมอง (3): วิธีทำให้ไพธอนเร็วขึ้นหลายๆเท่า”

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.