ใครว่า Python ช้า … ถ้าเรารู้จักเขียน Cython
เป้าหมายของบทความนี้ คืออธิบายว่าทำไมภาษาคอมพิวเตอร์ลักษณะหนึ่ง (อย่าง 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 + bdef 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 จะต้องทำขั้นตอนต่อไปนี้
- ถาม dictionary ของ
plus()
ว่ามีตัวแปรชื่อ a หรือเปล่า => ไม่มี - ถาม 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 ส่วน
- ตัวภาษา คือ syntax ที่เราเขียนเป็นภาษา Python อาจจะมีได้หลากหลาย เช่น Python 2.7, Python 3.6 เป็นต้น
- ตัวแปลภาษา คือ ตัวที่อ่านภาษา 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 เราจะต้องทำดังต่อไปนี้ …
- เขียน Cython ในไฟล์นามสกุล .pyx
- import model นั้น เข้ามาใน .py ของเรา โดยจะต้อง import ไลบรารี่สำหรับ compile cython เอาไว้ด้านบนของไฟล์ก่อน
- เราสามารถใช้งาน module .pyx ได้ราวกับ .py
- ไลบรารี่ Cython จะทำการ compile เป็นไฟล์ภาษา C ให้เรา (.pyx => .c)
- ไลบรารี 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 แล้วกัน
- ลง Anaconda หรือ Miniconda (มันคืออะไร ?) ในที่นี้จะพูดถึง Python 3.6 (ล่าสุด) เท่านั้น
- ใช้คำสั่ง
conda install cython
(แต่ว่าหากลง Anaconda มันจะลง Cython ติดมาให้อยู่แล้ว) - ยังไม่จบ … เพราะว่าเราต้องลง compiler ภาษา C ด้วย ซึ่งสำหรับ Python 3.6 ของ Miniconda หรือ Anaconda ใช้ Visual C++ Build Tools 2015 ในการ build ดังนั้นเราก็ต้องลงตัวนี้ด้วย ลิงก์ ซึ่งหลังจากลงเสร็จแล้วจะมีขนาดประมาณ 4 GB
- หลังจากนั้นเราสามารถเริ่มเขียน .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 Truedef 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 slowdef 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 ทำงานช้าเพราะอะไร มันยังทำให้เราเข้าใจการทำงานระดับลึก ๆ ของคอมพิวเตอร์ดีขึ้นด้วย
Welcome to Cython's Documentation - Cython 0.25.2 documentation
Edit description
cython.readthedocs.io
การทดสอบประสิทธิภาพการเขียนแบบต่าง ๆ (ความเท่ของ Cython ก็คือ เราสามารถเขียนได้หลายระดับความยาก แก้นิดเดียวเราก็จะเร็วขึ้นมาได้ระดับนึงละ ถ้าต้องการมากกว่านั้นก็ต้องเขียนให้เหมือน C มากขึ้นไปอีก) ของ Cython เปรียบเทียบกับ Python ปกติ