ใครว่า Python ช้า … ถ้าเรารู้จักเขียน Cython

Konpat Ta Preechakul
6 min readMay 23, 2017

--

เป้าหมายของบทความนี้ คืออธิบายว่าทำไมภาษาคอมพิวเตอร์ลักษณะหนึ่ง (อย่าง Python) ถึงทำงานได้ช้ากว่าภาษาอีกลักษณะหนึ่ง (อย่าง C) … แล้วผมก็จะมาเล่าให้ฟังถึงทางออกหนึ่งซึ่งยังพยายามรักษาความเรียบง่ายของภาษา Python และความทรงพลังของ C โดยที่เราไม่ต้องแก้ไขสิ่งที่เราเคยทำไปแล้วมากมายด้วย !

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

ทำไม Python ช้า

คำว่าช้าในที่นี้ จะหมายถึงช้ากว่าภาษาอย่าง C หรือ C++ อันที่จริงเกือบทุกภาษาที่เราเคยมีมาทั้งหมด … ช้ากว่า C แต่ในที่นี้ผมจะพยายามอธิบายว่าทำไม Python ถึงเป็นหนึ่งในภาษานั้นที่ช้ากว่า C

ส่วนตัวผมก็ไม่สามารถ เล่าได้หมดว่าทำไม Python ถึงช้า … อะไรคือความต่าง “ทั้งหมด” ระหว่าง Python กับ C แต่ผมก็พอจะเล่าได้บ้างว่า เหตุผลอะไรบ้างแน่ ๆ ที่ทำให้ Python ช้า …

Python เป็นภาษาแบบ Dynamic

คำว่า dynamic แบบนึงจะหมายถึง ว่าเราต้องประกาศชนิดตัวแปร ตอนที่เราประกาศตัวแปรหรือเปล่า ? ถ้าหากเคยเขียน ภาษา C หรือ Java มาก่อน ก็อาจจะคุ้นเคยกับการ ที่ต้องประกาศชัดเจนว่า ตัวแปรนี้ เป็นชนิดอะไร เช่น int a = 10;

แต่จะเห็นว่าใน Python เราเขียนแค่ a = 10 ก็พอแล้ว … แล้วแม้แต่เวลาต่อ ๆ มานิดนึง เราเขียนแก้อีกว่า a = 'hello' Python ก็ยังแฮปปี้ ไม่ได้กร่นด่าอะไรเราเลย (ภาษา C นี่ด่ายับแล้ว)

ดังนั้น Python เป็นภาษาแบบที่เรียกว่า dynamically typed แปลว่า ตัวแปรจะเป็นชนิดอะไรก็ได้ แล้วแต่ว่าตอนนั้น ๆ เราอยากให้มันเป็นอะไร

ภาษาแบบ dynamically typed มีอีกหลายอย่างเช่น php, javascript, ruby เป็นต้น แต่จริง ๆ แล้ว คำว่า dynamic ยังกว้างขวางกว่านี้มาก แต่โดยรวมแล้ว คำว่า dynamic ในที่นี้ก็คือการลดข้อจำกัด อะไรทำได้ก็ปล่อยให้ทำไปก่อน

ข้อดีของภาษาแบบนี้ที่เรามักจะรู้สึกได้แรก ๆ ก็คือ มันเขียนง่ายกว่ามาก เพราะเราไม่ต้อง “ทำตามกฎ” เราไม่ต้อง “คิดไว้ก่อน” เหมือนกับว่า “เขียน ๆ ไปเลย ไม่ต้องคิดมาก” (ผู้เขียนไม่ได้มีเจตนาโฆษณาว่า Python เป็นภาษาที่เขียนง่ายแต่อย่างใด)

แต่การที่เราไม่ต้องทำตามกฎ มันก็มีข้อเสียอยู่ในเชิงการ optimize เพราะว่าการที่มันไม่ค่อยมีกฎ แปลว่ามัน general มากขึ้น การที่ัมัน general มากขึ้น ทำให้การ optimize สำหรับกรณีพิเศษ ๆ ก็เป็นไปได้ยาก (เพราะว่ามันไม่ค่อยมีกรณีพิเศษ)

บางภาษาที่มีกฎตายตัวชัดเจนมาก ๆ เช่น ภาษา C ทำงานได้เร็วกว่า Python มาก ก็เพราะว่ากฎการเขียนภาษา C ซึ่งเป็น statically typed หนาแน่นกว่ามาก การที่ 1) ประกาศตัวแปรไว้ก่อน 2) บอกชนิดตัวแปร 3) ไม่แก้ไขชนิดอีก ของเหล่านี้ช่วยในการสร้าง compiler ที่มีประสิทธิภาพดีได้มาก … เพราะมันไม่ต้องเผื่อกรณีเหล่านี้เลย และทุก ๆ อย่างเหมือนสามารถ “เตรียมการ” ไว้ก่อนล่วงหน้าได้ … ทำให้การเรียกใช้สามารถทำให้เร็วได้ ต่างจาก Python ที่อยากได้อะไรก็เรียกเลย ไม่มีก็สร้างให้ด้วย … ทำขั้นตอนต่าง ๆ มีมากกว่าเยอะ

เปรียบเทียบ Python กับ C ซึ่งเป็นภาษา Static

สมมติว่าเรามีโค้ดภาษา Python ต่อไปนี้

def plus():
return a + b
def main():
global a, b
a = 10
b = 20
plus()
main()

และโค้ดภาษา C (ที่ควรจะทำงานเหมือนกัน) ดังต่อไปนี้

#include <stdio.h>int plus() {
return a + b
}
int main() {
int a = 10;
int b = 20;
plus();
}

ความต่างอย่างแรกของ 2 โค้ดนี้ก็คือ ของ Python สามารถรันผ่านและทำงานได้ถูกต้อง แต่ว่าของ C นั้นจะ compile error ก็เพราะว่าในฟังก์ชัน plus() มันไม่รู้จักตัวแปร a และ b ก็แน่นอนเพราะว่าตัวแปรเหล่านี้ไม่ใช่ global แต่หากเราต้องการทำให้มัน global ในภาษา C วิธีเดียวที่เราทำได้ก็คือ ต้อง “บอกแต่ต้น” ซึ่งเรา “ไม่จำเป็นต้องทำ” ในภาษา Python

ตรงนี้ทำให้ขั้นตอนในการ “หาค่าตัวแปร” แต่ละตัวในภาษา Python นั้นยากเย็นกว่าภาษา C อย่างมาก

ขั้นตอนการเรียกค่าตัวแปรของ Python และ C

ใน Python เนื่องจากตัวแปรจะถูกประกาศเมื่อไหร่ก็ได้ และเรียกใช้เมื่อไหร่ก็ได้ ทำให้ในฟังกชัน plus() ไม่สามารถบอกได้ว่า a กับ b มีตัวตนหรือไม่ ? มันจะรู้ได้ตอนเดียวก็คือ ตอนที่จะต้อง “ถามค่า a กับ b จริง ๆ” ซึ่งทำให้ในภาษา Python จะต้องมี “ดิกชันนารี” (dictionary) สำหรับแต่ละ scope ว่า ใน scope หนึ่ง ๆ มีตัวแปรอะไรบ้าง ?

สำหรับฟังก์ชัน plus() ก็จะมี dictionary หนึ่งไว้เก็บว่ามีตัวแปรอะไรบ้างตอนนี้ และส่วนของ global ก็ยังมี dictionary อีกอัน ซึ่งเก็บในลักษณะเดียวกัน

(จริง ๆ แล้ว ตัวแปร local กับ ตัวแปร global ใน Python มีการออกแบบให้เก็บไม่เหมือนกันเพื่อความเร็ว ซึ่งอาจจะไม่ใช้ dictionary ในบางกรณี)

ดังนั้นในการถามค่า a Python จะต้องทำขั้นตอนต่อไปนี้

  1. ถาม dictionary ของ plus() ว่ามีตัวแปรชื่อ a หรือเปล่า => ไม่มี
  2. ถาม dictionary ของ global ว่ามีตัวแปรชื่อ a หรือเปล่า => มี มีค่า 10

โดยในการถามค่า dictionary ก็จะต้องทำการในชื่อไปเข้าฟังก์ชัน hash แล้วจึงมาตรวจสอบในโครงสร้างข้อมูลอีกทีหนึ่ง … จะเห็นว่าแม้แต่ขั้นตอนง่าย ๆ อย่างการถามว่า “ตัวแปรนี้มีค่าเท่าไหร่” ก็เป็นงานไม่ง่ายสำหรับ Python แล้ว

ในทางกลับกัน ในภาษา C ขั้นตอนนี้ง่ายมาก ๆ และมันชัดเจนเสมอ และสามารถตรวจสอบได้ตั้งแต่ตอน compile เลยด้วยซ้ำ เพราะ ถ้าไม่มีการประกาศแปลว่าไม่มีตัวแปรนั้น ถ้าไม่มีการประกาศไว้ใน local และไม่มีใน global ตอนเริ่มโปรแกรม ก็แปลว่าไม่มีอีกแล้ว … สามารถปัดตกได้ตั้งแต่การ compile

นี่ยังไม่หมดแค่นี้ เนื่องจากว่าภาษา C เราต้องกำหนดชนิดตัวแปร ตั้งแต่เริ่ม … นั่นแปลว่า compiler สามารถจองเมโมรีล่วงหน้า สมมติว่าจองได้ตำแหน่ง 101–104 ในที่นี้ 4 bytes สำหรับ int32 1 ตัว compiler ก็จะไปอ่านโค้ดส่วนอื่น ๆ หากมีการอ้างถึงตัวแปรนี้ ก็จะสามารถแก้เป็นตำแหน่ง 101–104 โดยที่ไม่ต้องมีการพูดถึง “ชื่อ” ของตัวแปรเลยด้วยซ้ำ (เพราะฉะนั้นก็ไม่ต้องมี dictionary อะไรเลยด้วย)

เพราะฉะนั้นการ “ถามค่าตัวแปร” ในภาษาซี สามารถทำได้ในระดับการนับ cpu clocks แต่ว่าการทำสิ่งเดียวกันนี้ในภาษา Python นั้นทำในระดับที่ช้ากว่านั้นมาก ๆ เรานับเวลาเอาดีกว่า …

ความต่างตรงนี้คือความต่างระหว่าง static binding (early) อย่างในภาษา C และ dynamic binding (late) อย่างในภาษา Python

ส่วนใหญ่แล้ว Python ใช้ Global Interpreter Lock (GIL)

อะไรคือ GIL (Global Interpreter Lock) ทำไมมันทำให้ช้า

ในเชิงการทำงานแล้ว GIL เหมือนตัวแปรเล็ก ๆ ตัวนึง ซึ่งจะบอกว่า “มีใครทำงานอยู่ ณ ตอนนี้หรือเปล่า ?” ถ้ามี … ก็จะยอมให้ใครไปทำงานอีกไม่ได้ … หากไม่มี ก็สามารถขอสิทธิ์ในการทำงานได้ (mutex)

การที่มีแค่คนเดียวสามารถเข้าไปทำงานได้นี่แหละ ทำให้มันช้า … เพราะว่า CPU สมัยใหม่ มีหลาย core มาก ๆ สามารถทำงานได้พร้อม ๆ กันหลายงาน และการยอมให้มันทำงานพร้อม ๆ กัน (อย่างเป็นระบบ) ก็ย่อมทำให้งานเราเสร็จเร็วขึ้นไปด้วย

แล้วทำไม Python ส่วนใหญ่ต้องมีมัน

(มีคนถามเรื่องนี้ใน stackexchange ผมจะพยายามแปรเอาใจความสำคัญออกมา)

Python ถูกออกแบบมาให้สามารถทำงานได้ดีกับไลบรารีภาษา C หากเราคุ้นเคยกับไลบรารีอย่าง numpy หรือ scipy จะรู้ว่าพวกนี้ไม่ได้เขียนด้วยภาษา Python หากแต่เขียนด้วย C เพื่อให้สามารถทำงานเหล่านี้ได้เร็ว …

เพราะว่าด้วยเหตุผลที่กล่าวไปข้างต้น ไม่ว่าจะเขียนอย่างไร Python ก็ไม่มีโอกาสที่จะเร็วได้มากกว่า C ดังนั้นงานที่ต้องการความเร็วจริง ๆ Python ก็เปิดโอกาสให้เขียนด้วย C แทน ซึ่งนั่นก็แปลว่า Python จะต้องรักษาการติดต่อระหว่าง C — Python ให้ทำงานได้รวดเร็วและมีข้อจำกัดน้อยที่สุด

ปรากฎว่าวิธีการหนึ่งที่จะทำให้ไลบรารีภาษา C สามารถทำงานกับ Python ได้ง่ายคือการรับประกันว่า จะไม่มีใครมาแย่งใช้ตัวแปรของเรา เพราะว่าไลบรารี C ส่วนใหญ่ ไม่ได้เขียนมาเผื่อกรณีที่ต้องแย่ง resource กัน (ไม่ thread-safe) นั่นก็คือความจำเป็นของ GIL อย่างหนึ่ง

ดังนั้นไอเดียของการเขียน Python ก็คือ “เชื่อมโค้ดส่วนต่าง ๆ ซึ่งทำงานได้เร็ว เพราะเขียนด้วย C เข้าด้วยกัน” ราวกับว่า Python คือ glue language นั่นเอง

แล้วทำไมแค่ “ส่วนใหญ่” ไม่ใช่ทั้งหมด

จริง ๆ แล้ววลีที่ว่า “Python ใช้ GIL” นั้นผิด … ก็เพราะอย่างแรกคือ Python เป็นแค่ภาษาคอมพิวเตอร์ แต่ว่า GIL นั้นคือวิธีการออกแบบตัวแปรภาษา

ดังนั้นคำว่า Python ในความหมายที่เราเข้าใจกันจริง ๆ แล้ว ประกอบไปด้วย 2 ส่วน

  1. ตัวภาษา คือ syntax ที่เราเขียนเป็นภาษา Python อาจจะมีได้หลากหลาย เช่น Python 2.7, Python 3.6 เป็นต้น
  2. ตัวแปลภาษา คือ ตัวที่อ่านภาษา Python แล้วแปลให้เป็นภาษาเครื่องให้ทำงานตาม syntax ของภาษา Python

ตัวแปลภาษา (interpreter; Python มักไม่ใช้ compiler) นั้นมีได้หลากหลายกว่าตัวภาษาเสียอีก เพราะว่าภายในมาตรฐานภาษาเดียวกัน ก็แล้วแต่คุณแล้วล่ะว่าจะเขียนโปรแกรมมาแปลภาษานี้อย่างไร ขอแค่มันทำงานได้อย่างที่มาตรฐานกำหนดไว้ก็พอ อย่างไรก็ตามตัว Python นั้นจะพัฒนาตัวแปลมาตรฐานติดมาด้วยเสมอเรียกว่า CPython (ใช้คำว่า C เพราะว่าตัวแปลภาษานี้เขียนด้วยภาษา C)

แต่ว่า CPython ไม่ใช่เจ้าเดียวที่ทำออกมา ยังมี Pypy, Jython และอื่น ๆ อีก ซึ่งแต่ละตัวก็มีจุดขายของตัวเอง อย่าง Pypy ซึ่งชูจุดขายว่าเป็น just-in-time compiler และเร็วกว่า CPython (หากว่างานเหล่านั้นเขียนด้วย Python เป็นหลัก) และ Jython ที่ชูดจุดขายว่ารันบน JVM (java virtual machine) แปลว่าสามารถดึง library ต่าง ๆ ที่มีอยู่เยอะมาก ๆ บน java มาใช้บน syntax ของ Python ที่รันบน Jython ได้เลย

แต่ว่าโดยส่วนใหญ่เรา ๆ ท่าน ๆ ก็จะใช้ CPython เป็นค่าปริยายอยู่แล้ว … และคำว่าช้าที่ผมพูด ๆ ในที่นี้ก็จะหมายถึง CPython นี่แหละ … ตัว GIL ที่กล่าวไว้ด้านบนเอง ก็ปรากฎอยู่แต่บน CPython เท่านั้น แต่ก็คิดเป็นส่วนใหญ่ของประชากร Python อยู่ดี

แล้วทางออกที่ทำให้ Python เร็วขึ้นคือ … ?

ขอเสนอ ! Cython ซึ่งจะทำให้เราสามารถแก้โค้ด Python ที่เรามีอยู่แล้ว เพียงเล็กน้อย แต่สามารถเพิ่มประสิทธิภาพของโค้ดนั้นได้หลายเท่า !

Cython คืออะไร ? แล้วมันทำให้โปรแกรมเร็วขึ้นได้ไง ?

จริง ๆ แล้ว Cython คือ “ภาษาคอมพิวเตอร์” แต่ในตัวภาษา Cython เองก็รองรับภาษา Python ในนั้นด้วย นั่นแปลว่าในการเขียน Cython เราสามารถใช้ คำสั่งเฉพาะทางของ Cython นอกเหนือจาก syntax ของ Python ได้

โดยหน้าตาของโค้ด Cython จะคล้าย ๆ แบบนี้

def prime(int n):
if n <= 1: return False
cdef int i = 2
while i < n:
if n % i == 0:
return False
i += 1
return True

เห้ย ! นี่มันคล้าย ๆ ลูกครึ่ง C — Python นี่นา … ก็ใช่แล้วเพราะว่าสิ่งที่ Cython ทำ คือพยายามอ่านโค้ดเราแล้ว “แปลง” เป็นโค้ดภาษา C แล้วค่อย compile ด้วย compiler ภาษา C เพื่อจะได้โค้ดที่มีประสิทธิภาพดีขึ้น (เร็วราวกับเขียนด้วยภาษา C เลยล่ะ !) และยิ่งเราใช้ syntax เฉพาะทางของ Cython มากขึ้นเท่าไหร่ Cython ก็ยิ่งแปลงโค้ดได้ดีขึ้นเท่านั้น

สรุปว่า … เวลาเขียนโค้ดด้วย Cython เราจะต้องทำดังต่อไปนี้ …

  1. เขียน Cython ในไฟล์นามสกุล .pyx
  2. import model นั้น เข้ามาใน .py ของเรา โดยจะต้อง import ไลบรารี่สำหรับ compile cython เอาไว้ด้านบนของไฟล์ก่อน
  3. เราสามารถใช้งาน module .pyx ได้ราวกับ .py
  4. ไลบรารี่ Cython จะทำการ compile เป็นไฟล์ภาษา C ให้เรา (.pyx => .c)
  5. ไลบรารี Cython จะเรียกใช้ compiler ภาษา C บนเครื่อง (ปกติต้องเป็นตัวเดียวกับที่ใช้ compile Python ตัวที่เราใช้) เป็นไฟล์ .so บน Linux และ .pyd บน Windows

โดยเราสามารถใช้งาน Python (ที่เรามี) กับ Cython ไปพร้อม ๆ กันได้ เพียงแค่ลงไลบรารีของมันเพิ่มเข้าไปเท่านั้น (เหมือนกับ pip install ไรงี้)

ด้วย Cython สามารถปลด GIL ชั่วคราวได้

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

หมายเหตุ: ผู้เขียนยังไม่เคยเขียน Cython เพื่อปลด GIL แต่อย่างใด แต่ก็ได้พยายามสืบหาลิงค์ที่น่าสนใจไว้ ณ ที่นี้

ข้อจำกัด

บทความด้านบนอธิบายข้อจำกัดเชิงเทคทิคของ Cython ไว้อย่างดี … โดยทั่วไปแล้วไม่ได้น่ากังวลอะไรเลย Cython เองไม่ได้มาแทนที่การทำงานของ CPython แต่ว่าเป็น “ส่วนเสริม” ให้กับ CPython เสียมากกว่า ดังนั้นหน้าที่ของ มันก็จะจำกัดอยู่กับฟังก์ชันของมันเท่านั้น

แต่ประเด็นที่น่าสนใจกว่าก็คือ ในเชิง “ปฏิบัติ” แล้ว เราก็ไม่อยากจะละทิ้งความงดงามและความง่ายในการเขียน Python ไปเสียหมด เพราะว่าเราก็อยากจะเขียน Python ไม่ใช่ภาษา C ดังนั้นเราควรจะสนใจเฉพาะฟังก์ชันที่ทำงานช้า ๆ หรือต้องถูกเรียกใช้ให้ทำงานบ่อย ๆ ฟังก์ชันพวกนี้หากเราทำให้เร็วขึ้นได้ ซึ่งหากเราเขียนแบบ functional programming อยู่แล้ว การทำให้ฟังก์ชันเหล่านี้กลายเป็น Cython จะทำได้ไม่ยาก และให้ผลลัพธ์ที่ดีเป็นการตอบแทน

เนื่องจากไม่ใช่ทุกเครื่องสามารถ compile ภาษา C ได้ และยิ่งต้องอาศัย compiler ตัวเดียว version เดียวกันกับที่ compile Python ด้วยแล้วก็ยิ่งยากใหญ่ (บน linux อาจจะไม่ยากมาก เพราะว่า build-essentials อาจจะจบเลย แต่ว่าบน Windows มักจะต้องลง Visual C++ Build Tools สำหรับ Python version ใหม่ ๆ … และอาจจะยากกว่านี้หาก user ไม่ได้ลง Python ผ่านทาง installer)

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

ขั้นตอนการติดตั้ง Cython

เนื่องจากบน Linux หรือ OSX มันติดตั้งง่ายเกินไป ผมจะพูดถึงวิธีการบน Windows แล้วกัน

  1. ลง Anaconda หรือ Miniconda (มันคืออะไร ?) ในที่นี้จะพูดถึง Python 3.6 (ล่าสุด) เท่านั้น
  2. ใช้คำสั่ง conda install cython (แต่ว่าหากลง Anaconda มันจะลง Cython ติดมาให้อยู่แล้ว)
  3. ยังไม่จบ … เพราะว่าเราต้องลง compiler ภาษา C ด้วย ซึ่งสำหรับ Python 3.6 ของ Miniconda หรือ Anaconda ใช้ Visual C++ Build Tools 2015 ในการ build ดังนั้นเราก็ต้องลงตัวนี้ด้วย ลิงก์ ซึ่งหลังจากลงเสร็จแล้วจะมีขนาดประมาณ 4 GB
  4. หลังจากนั้นเราสามารถเริ่มเขียน .pyx ได้แล้ว

ตัวอย่างการใช้งาน และเปรียบเทียบ

สมมติว่าเราจะลองเขียน โปรแกรม “ทดสอบจำนวนเฉพาะ” โดยที่ไม่มีการ optimize อะไรเพิ่มเติมเลย เพื่อดูว่ามันเร็วขึ้นจริงหรือไม่ …

ไฟล์ fast.pyx เราจะเขียนโค้ด Cython ไว้ที่นี่

def prime(int n):
if n <= 1: return False
cdef int i = 2
while i < n:
if n % i == 0:
return False
i += 1
return True
def prime2(n):
if n <= 1: return False
for i in range(2,n):
if n % i == 0:
return False
return True

จะเห็นว่าในกรณีของ prime ผมพยายามเขียนให้ใกล้เคียงกับ C ที่สุด หลีกเลี่ยงการใช้ generator range(...) และใช้ while loop พร้อมกับกำหนดตัวแปร i ให้มีชนิดเป็น int

แต่ในกรณีของ prime2 ผมเขียนเหมือนกับโค้ด Python ปรกติเลย

ไฟล์ slow.py เขียนโค้ด Python ไว้ดังนี้

def prime(n):
if n <= 1: return False
for i in range(2,n):
if n % i == 0:
return False
return True

ลองรันเปรียบเทียบด้วย Jupyter Notebook

import pyximport; pyximport.install() # เพื่อทำให้สามารถ import .pyx ได้
import fast
import slow
def test(prime_fn):
res = []
for i in range(5000):
res.append(prime_fn(i))
return res
%timeit test(fast.prime)
# 100 loops, best of 3: 4.7 ms per loop
%timeit test(slow.prime)
# 10 loops, best of 3: 93.1 ms per loop
%timeit test(fast.prime2)
# 10 loops, best of 3: 47.7 ms per loop

โค้ด Cython ใช้เวลา 4.7 ms เทียบกับ Python ซึ่งใช้เวลา 93.1 ms ต่างกันถึง 19.8 เท่า ! … แม้ว่าอัลกอริทึมจะเหมือนกัน big-O ก็เท่ากันก็ตาม

แน่นอนว่าโค้ด Cython ก็คงไม่ได้เขียนง่ายเหมือน Python หรอก แต่เราสามารถเขียนโค้ดให้ C น้อย-หรือ-มาก ขนาดไหนก็ได้ หากเราขยันมาก เขียนได้ใกล้เคียง C มาก เราก็จะได้ประโยชน์จากการใช้ Cython มากขึ้นไปเรื่อย ๆ

ที่น่าสนใจอีกคือแม้เราจะไม่ได้แก้โค้ดอะไรเลย (ในกรณีของ prime2) เราก็มีโอกาสที่จะได้ประโยชน์จากการ compile โค้ดของ Cython เช่นกัน ซึ่งในตัวอย่างข้างต้นแสดงให้เห็นว่าเร็วขึ้นราว ๆ 2 เท่า โดยที่เราไม่ต้องทำอะไรเลย

อ่านต่อ

Cython Documentation บทความนี้ไม่ได้แค่สอนว่าจะใช้งาน Cython ได้อย่างไร แต่มันสอนเราด้วยว่า Cython ทำอย่างไรให้ CPython ทำงานได้เร็วขึ้นและ CPython ทำงานช้าเพราะอะไร มันยังทำให้เราเข้าใจการทำงานระดับลึก ๆ ของคอมพิวเตอร์ดีขึ้นด้วย

การทดสอบประสิทธิภาพการเขียนแบบต่าง ๆ (ความเท่ของ Cython ก็คือ เราสามารถเขียนได้หลายระดับความยาก แก้นิดเดียวเราก็จะเร็วขึ้นมาได้ระดับนึงละ ถ้าต้องการมากกว่านั้นก็ต้องเขียนให้เหมือน C มากขึ้นไปอีก) ของ Cython เปรียบเทียบกับ Python ปกติ

เอกสารอ้างอิง

  1. http://cython.readthedocs.io/en/latest/src/userguide/early_binding_for_speed.html

--

--

Konpat Ta Preechakul

A graduate student in Machine Learning seeking revolutionary intelligent machines