4.2 Cython:Python 語法的 C 速度
4.2 Cython:Python 語法的 C 速度
本章介紹 Cython,一種 Python 的超集語言,可以編譯成 C 程式碼。
本章目標
學完本章後,你將能夠:
- 理解 Cython 的編譯流程
- 使用型別宣告加速程式碼
- 使用 Cython 包裝 C 函式庫
【原理層】Cython 是什麼?
Python 的超集
Cython 是一種程式語言,它是 Python 的超集:
1合法的 Python 程式碼 → 合法的 Cython 程式碼
2 → 但 Cython 可以加入更多語法
3
4Cython 特有語法:
5- cdef:宣告 C 變數或函式
6- cpdef:同時暴露給 Python 和 C
7- cimport:匯入 .pxd 檔案
8- nogil:標記不需要 GIL 的區塊編譯流程
1.pyx (Cython 原始碼)
2 │
3 ↓ Cython 編譯器
4.c (C 原始碼)
5 │
6 ↓ C 編譯器 (gcc, clang, MSVC)
7.so / .pyd (Python 擴展模組)
8 │
9 ↓ import
10Python 程式為什麼 Cython 比 Python 快?
1# 純 Python:每次操作都是物件操作
2def python_sum(n):
3 total = 0 # 建立 int 物件
4 for i in range(n): # 建立 range 物件,迭代器
5 total += i # 呼叫 __add__,建立新物件
6 return total
7
8# Cython:可以使用原生 C 型別
9def cython_sum(int n):
10 cdef int total = 0 # C 的 int,不是 Python 物件
11 cdef int i
12 for i in range(n): # 編譯成 C 的 for 迴圈
13 total += i # 單一 CPU 指令
14 return total效能差異的來源:
| 操作 | Python | Cython (有型別) |
|---|---|---|
| 變數存取 | dict 查找 | 直接記憶體存取 |
| 整數加法 | 物件方法呼叫 | CPU 指令 |
| 迴圈 | 迭代器協議 | C for 迴圈 |
| 函式呼叫 | 建立 frame 物件 | C 函式呼叫 |
【設計層】Cython 基礎語法
安裝與設定
1pip install cython
2
3# 檢查版本
4python -c "import cython; print(cython.__version__)"第一個 Cython 模組
建立 example.pyx:
1# example.pyx
2
3def say_hello(name):
4 """純 Python 函式,也是合法的 Cython"""
5 print(f"Hello, {name}!")
6
7def compute_sum(int n):
8 """加入型別宣告的函式"""
9 cdef int i
10 cdef long total = 0
11
12 for i in range(n):
13 total += i
14
15 return total建立 setup.py:
1# setup.py
2from setuptools import setup
3from Cython.Build import cythonize
4
5setup(
6 ext_modules=cythonize("example.pyx"),
7)編譯與使用:
1# 編譯
2python setup.py build_ext --inplace
3
4# 使用
5python -c "import example; example.say_hello('Cython')"變數宣告
1# 型別宣告語法
2
3# cdef:宣告 C 變數(只在 Cython 內部可見)
4cdef int x = 10
5cdef double y = 3.14
6cdef char* s = "hello"
7
8# 多個變數
9cdef:
10 int a, b, c
11 double d = 0.0
12 list my_list = []
13
14# 型別推斷(Python 3 風格)
15cdef int x = 10 # 明確宣告
16x: cython.int = 10 # 註解風格(Pure Python 模式)函式類型
1# def:Python 函式,可從 Python 呼叫
2def python_func(x, y):
3 return x + y
4
5# cdef:C 函式,只能從 Cython 呼叫,最快
6cdef int c_func(int x, int y):
7 return x + y
8
9# cpdef:同時產生 Python 和 C 版本
10cpdef int hybrid_func(int x, int y):
11 return x + y
12
13# 使用情境
14def api_func(int n):
15 """公開 API"""
16 cdef int i
17 cdef int total = 0
18 for i in range(n):
19 total = _helper(total, i) # 呼叫 cdef 函式
20 return total
21
22cdef int _helper(int a, int b):
23 """內部輔助函式,不暴露給 Python"""
24 return a + b型別轉換
1# 隱式轉換
2cdef int i = 10
3cdef double d = i # int → double,自動轉換
4
5# 明確轉換
6cdef double x = 3.14
7cdef int y = <int>x # 截斷為 3
8
9# Python 物件與 C 型別
10def convert_example(obj):
11 cdef int c_int
12
13 # Python int → C int
14 c_int = <int>obj # 可能 overflow
15
16 # 安全轉換
17 if isinstance(obj, int) and -2147483648 <= obj <= 2147483647:
18 c_int = obj
19
20 return c_int【實作層】Cython 優化技巧
使用 cython -a 分析
1# 產生帶註解的 HTML 報告
2cython -a example.pyx1HTML 報告的顏色含義:
2├── 白色:純 C 程式碼,最快
3├── 淺黃色:少量 Python API 呼叫
4├── 深黃色:較多 Python 互動
5└── 橙色/紅色:大量 Python 操作,需要優化常見優化模式
1# 優化前:大量黃色
2def slow_function(data):
3 total = 0
4 for item in data:
5 total += item * item
6 return total
7
8# 優化後:大部分白色
9def fast_function(double[:] data): # 型別化記憶體視圖
10 cdef:
11 int i
12 int n = data.shape[0]
13 double total = 0.0
14
15 for i in range(n):
16 total += data[i] * data[i]
17
18 return total停用邊界檢查
1# 預設:有邊界檢查(安全但較慢)
2cdef double[:] arr = some_array
3
4# 停用檢查(確定安全時使用)
5cimport cython
6
7@cython.boundscheck(False) # 停用邊界檢查
8@cython.wraparound(False) # 停用負數索引
9def optimized_sum(double[:] arr):
10 cdef int i
11 cdef int n = arr.shape[0]
12 cdef double total = 0.0
13
14 for i in range(n):
15 total += arr[i]
16
17 return total
18
19# 或者使用全域設定
20# cython: boundscheck=False
21# cython: wraparound=False釋放 GIL
1from cython.parallel import prange
2
3# nogil:標記不需要 GIL 的區塊
4cdef double compute_heavy(double x) nogil:
5 """純 C 計算,不涉及 Python 物件"""
6 cdef double result = 0.0
7 cdef int i
8 for i in range(1000):
9 result += x * i
10 return result
11
12def parallel_compute(double[:] data):
13 cdef int i
14 cdef int n = data.shape[0]
15 cdef double[:] results = np.zeros(n)
16
17 # 使用 OpenMP 平行化
18 with nogil:
19 for i in prange(n):
20 results[i] = compute_heavy(data[i])
21
22 return np.asarray(results)【實作層】與 NumPy 整合
記憶體視圖
1import numpy as np
2cimport numpy as cnp
3
4# 型別化記憶體視圖(推薦)
5def process_array(double[:, :] arr):
6 """處理 2D double 陣列"""
7 cdef int i, j
8 cdef int rows = arr.shape[0]
9 cdef int cols = arr.shape[1]
10 cdef double total = 0.0
11
12 for i in range(rows):
13 for j in range(cols):
14 total += arr[i, j]
15
16 return total
17
18# 使用
19# import numpy as np
20# data = np.random.rand(100, 100)
21# result = process_array(data)矩陣運算範例
1# matrix_ops.pyx
2import numpy as np
3cimport numpy as cnp
4cimport cython
5
6@cython.boundscheck(False)
7@cython.wraparound(False)
8def matrix_multiply(double[:, :] A, double[:, :] B):
9 """矩陣乘法 C = A @ B"""
10 cdef int i, j, k
11 cdef int m = A.shape[0]
12 cdef int n = A.shape[1]
13 cdef int p = B.shape[1]
14
15 if B.shape[0] != n:
16 raise ValueError("矩陣維度不匹配")
17
18 cdef double[:, :] C = np.zeros((m, p), dtype=np.float64)
19
20 for i in range(m):
21 for j in range(p):
22 for k in range(n):
23 C[i, j] += A[i, k] * B[k, j]
24
25 return np.asarray(C)
26
27# 注意:這只是教學範例
28# 實際應用應使用 numpy.dot 或 BLAS效能比較
1import numpy as np
2import timeit
3
4# 假設已編譯 matrix_ops
5# from matrix_ops import matrix_multiply
6
7def benchmark():
8 A = np.random.rand(100, 100)
9 B = np.random.rand(100, 100)
10
11 # NumPy(使用 BLAS)
12 t1 = timeit.timeit(lambda: A @ B, number=100)
13
14 # Cython(我們的實現)
15 # t2 = timeit.timeit(lambda: matrix_multiply(A, B), number=100)
16
17 # 純 Python
18 def py_matmul(A, B):
19 m, n = A.shape
20 p = B.shape[1]
21 C = [[0.0] * p for _ in range(m)]
22 for i in range(m):
23 for j in range(p):
24 for k in range(n):
25 C[i][j] += A[i, k] * B[k, j]
26 return C
27
28 t3 = timeit.timeit(lambda: py_matmul(A, B), number=1)
29
30 print(f"NumPy (BLAS): {t1:.4f}s")
31 # print(f"Cython: {t2:.4f}s")
32 print(f"Pure Python: {t3:.4f}s (x1)")【實作層】包裝 C 函式庫
宣告外部函式
1# 宣告 C 標準函式庫函式
2from libc.math cimport sqrt, sin, cos, pow
3from libc.stdlib cimport malloc, free
4from libc.string cimport memcpy, strlen
5
6def compute_distance(double x1, double y1, double x2, double y2):
7 """使用 C 的 sqrt"""
8 cdef double dx = x2 - x1
9 cdef double dy = y2 - y1
10 return sqrt(dx * dx + dy * dy)宣告自訂 C 函式庫
假設有 C 標頭檔 mylib.h:
1// mylib.h
2typedef struct {
3 double x, y, z;
4} Vector3D;
5
6double vector_length(Vector3D* v);
7Vector3D vector_add(Vector3D* a, Vector3D* b);建立 Cython 宣告檔 mylib.pxd:
1# mylib.pxd
2cdef extern from "mylib.h":
3 ctypedef struct Vector3D:
4 double x
5 double y
6 double z
7
8 double vector_length(Vector3D* v)
9 Vector3D vector_add(Vector3D* a, Vector3D* b)使用宣告:
1# mylib_wrapper.pyx
2from mylib cimport Vector3D, vector_length, vector_add
3
4cdef class PyVector3D:
5 """Python 包裝類別"""
6 cdef Vector3D _vec
7
8 def __init__(self, double x, double y, double z):
9 self._vec.x = x
10 self._vec.y = y
11 self._vec.z = z
12
13 @property
14 def x(self):
15 return self._vec.x
16
17 @property
18 def y(self):
19 return self._vec.y
20
21 @property
22 def z(self):
23 return self._vec.z
24
25 def length(self):
26 return vector_length(&self._vec)
27
28 def __add__(self, PyVector3D other):
29 cdef Vector3D result = vector_add(&self._vec, &other._vec)
30 return PyVector3D(result.x, result.y, result.z)
31
32 def __repr__(self):
33 return f"Vector3D({self.x}, {self.y}, {self.z})"記憶體管理
1from libc.stdlib cimport malloc, free
2
3cdef class DynamicArray:
4 """管理動態分配記憶體的範例"""
5 cdef double* data
6 cdef int size
7
8 def __cinit__(self, int size):
9 """C 層級初始化,保證在 __init__ 之前執行"""
10 self.size = size
11 self.data = <double*>malloc(size * sizeof(double))
12 if self.data == NULL:
13 raise MemoryError("無法分配記憶體")
14
15 def __dealloc__(self):
16 """C 層級解構,保證釋放記憶體"""
17 if self.data != NULL:
18 free(self.data)
19
20 def __init__(self, int size):
21 """Python 層級初始化"""
22 cdef int i
23 for i in range(self.size):
24 self.data[i] = 0.0
25
26 def __getitem__(self, int index):
27 if index < 0 or index >= self.size:
28 raise IndexError("索引超出範圍")
29 return self.data[index]
30
31 def __setitem__(self, int index, double value):
32 if index < 0 or index >= self.size:
33 raise IndexError("索引超出範圍")
34 self.data[index] = value
35
36 def __len__(self):
37 return self.size【進階】Pure Python 模式
使用型別註解
從 Cython 3.0 開始,支援純 Python 語法:
1# pure_example.py(純 Python 檔案)
2import cython
3
4@cython.cfunc
5def c_function(x: cython.int, y: cython.int) -> cython.int:
6 """等同於 cdef int c_function(int x, int y)"""
7 return x + y
8
9@cython.ccall
10def hybrid_function(x: cython.int) -> cython.int:
11 """等同於 cpdef int hybrid_function(int x)"""
12 return c_function(x, x)
13
14def public_api(n: cython.int) -> cython.long:
15 """普通 Python 函式,但有型別最佳化"""
16 total: cython.long = 0
17 i: cython.int
18
19 for i in range(n):
20 total += hybrid_function(i)
21
22 return total優點
1Pure Python 模式的好處:
21. 不需要 .pyx 檔案,直接用 .py
32. IDE 支援更好(型別提示)
43. 可以同時作為 Python 和 Cython 使用
54. 測試更容易(不需編譯就能跑 Python)【建構】現代化建構方式
使用 pyproject.toml
1# pyproject.toml
2[build-system]
3requires = ["setuptools>=61.0", "cython>=3.0"]
4build-backend = "setuptools.build_meta"
5
6[project]
7name = "my-cython-package"
8version = "0.1.0"
9
10[tool.setuptools]
11ext-modules = [
12 {name = "my_module", sources = ["src/my_module.pyx"]}
13]使用 meson-python
1# pyproject.toml
2[build-system]
3requires = ["meson-python", "cython"]
4build-backend = "mesonpy"
5
6[project]
7name = "my-cython-package"
8version = "0.1.0" 1# meson.build
2project('my-cython-package', 'cython')
3
4py = import('python').find_installation()
5
6py.extension_module(
7 'my_module',
8 'src/my_module.pyx',
9 install: true
10)思考題
- 為什麼 cdef 函式比 def 函式快?從呼叫協議的角度解釋。
- 在什麼情況下,Cython 的效能提升最明顯?什麼情況下提升有限?
- 如何決定哪些函式應該用 cdef、cpdef 還是 def?
實作練習
- 將入門系列效能章節的
is_prime函式用 Cython 改寫,比較效能差異 - 使用 Cython 實現一個簡單的 LRU Cache,與
functools.lru_cache比較效能 - 包裝一個簡單的 C 函式庫(如 zlib)並在 Python 中使用
延伸閱讀
上一章:ctypes 與 cffi 下一章:pybind11