周末的餐厅后厨里,新来的帮厨小周炒番茄蛋时,每放一个动作就要翻一次菜谱:锅烧热没有,盐放几克,鸡蛋要打几圈……手忙脚乱还特别慢。老田师傅不用看菜谱,闭眼也能颠勺,几分钟就出一大桌菜。Python 就像小周:写起来自由,什么菜谱都现翻、什么容器都互相通用,但每道指令都要“查字典、验身份”,自然跑不快。Cython 就是让 Python 提前“背菜谱”:在代码里用 cdef 给变量标上类型,再编译成 C 扩展。编译后的扩展直接运行机器码,循环、数学计算、NumPy 数组处理都能快几十倍。全世界大量数据科学库都在用它加速核心逻辑,而你只需要学会几个关键字,就能让老项目吃得快、跑得稳。

岗位关联

对 AI 从业者来说,掌握 Cython 意味着能把自己写的特征工程、图像预处理、训练数据 pipeline 从“能跑”升级成“跑得快”,在模型上线和高频迭代场景里真正省下真金白银和宝贵时间。

目录
  1. 可视化演示
  2. 性能瓶颈成因:为什么Python慢
  3. cdef类型注解:让变量变成肌肉记忆
  4. cpdef函数加速:给函数装两个窗口
  5. NumPy内存视图:把数组装进编号抽屉
  6. GIL释放技巧:多开几个灶台
  7. 编译构建流程:把菜谱写进厨房手册
  8. 性能对决与改造抉择:先量再改再上桌
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示Cython如何通过cdef静态类型声明、编译管线和GIL释放实现数十倍加速,可视化Python到C的转换全过程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

cdef类型注解 cdef声明C级静态类型变量,绕过动态对象开销 编译器生成高效C代码,性能提升数十倍 cdef int x → C int

性能瓶颈成因:为什么Python慢

周末的餐厅后厨里,新来的帮厨小周炒番茄蛋时,每做一个动作就要翻一次菜谱——锅热了吗?盐该撒几克?鸡蛋要不要先打散?他一边翻、一边做,出菜速度自然慢。Python 解释器就像小周:先把人类可读的 Python 代码编译成字节码,再由解释器一条条地读取和执行。每执行到一个名字,都要在命名空间里“翻菜谱”才能找到对应的对象;每遇到一次运算,还要确认对象的“身份”是否允许这个操作,于是大量时间都耗在“确认”上,真正干活的反而不多。

这里的“确认”其实对应三个具体开销:名字查找、类型检查、对象装箱和拆箱。名字查找是去名字空间里搜变量;类型检查是运行时判断变量是不是数字;装箱拆箱则是把 Python 整数对象转换成底层 C 整数、再包装回来。这三件事在每次循环里不断重复,累积下来的代价非常可观。如果循环一百万次,就相当于让小周把同一页菜谱翻一百万次。

所以,Python 慢不是因为“厨师不勤劳”,而是因为他每做一步都要先停下来确认。Cython 的思路就是让小周成为老田师傅:把所有菜谱提前背熟,把食材位置全部固定,这样手起刀落、行云流水。

Tpy=N×(tlookup+tcheck+texecute)\displaystyle T_{py} = N \times (t_{lookup} + t_{check} + t_{execute})
纯 Python 的总耗时约等于循环次数 N 乘以单次循环的“查找、检查、执行”三项开销之和。
  1. 循环总代价总时间由循环次数决定,每循环一次都要付出完整的三项开销。
    Tpy=N×(tlookup+tcheck+texecute)T_{py} = N \times (t_{lookup} + t_{check} + t_{execute})
  2. 名字查找Python 在命名空间里通过变量名找到对象,像厨师翻菜谱找“盐”放在哪里。
    tlookupt_{lookup}
  3. 类型检查运行时确认对象类型并决定采用哪条运算路径,不能提前省略。
    tcheckt_{check}
  4. 实际执行真正做加法、比较或赋值的机器工作,有时只占总开销的一小部分。
    texecutet_{execute}

cdef类型注解:让变量变成肌肉记忆

老田师傅为什么快?因为他早就清楚番茄在左手边第三个篮子,鸡蛋在操作台右上角,盐罐永远放在锅柄右侧。Cython 中的 cdef 就扮演着“记忆坐标”的角色:你告诉编译器“这个变量是 C 语言的整数、那个是双精度浮点数”,编译器立刻在内存里为它划好固定位置,后续读写不再需要临时去 Python 的命名空间翻找。

写法非常平易近人:在函数内部使用 cdef int total = 0 或 cdef double weight = 1.5。这样声明的变量会被直接映射到 C 语言的原生类型,栈上分配,访问速度远高于普通的 Python 对象。再加上 cdef 声明后,类型在编译期已确定,Cython 可以生成直接的 C 赋值语句、C 加法指令,避开 Python 运行时那套“查字典”机制。

cdef 最适合给循环里的累加器、下标、中间计算结果做“肌肉记忆”。比如一个求和的循环里,把 total 和 i 都用 cdef 声明成 int,性能立刻脱胎换骨。对普通 Python 变量来说,cdef 是一个“身份确认”的外挂,但对 C 类型变量来说,它只是简简单单的“我知道它在哪儿”。

Tcy≈N×(texec+tcheckk),k≫1\displaystyle T_{cy} \approx N \times \left(t_{exec} + \frac{t_{check}}{k}\right), \quad k \gg 1
加好类型注解后,单次循环的检查开销被除以一个很大的倍数 k,执行更接近机器原生速度。
  1. 类型已知变量类型在编译期就确定,运行时的检查分支大幅减少。
    tcheckk,k≫1\frac{t_{check}}{k}, \quad k \gg 1
  2. 机器码执行加法、乘法等操作直接编译成 CPU 指令,不再走解释器分发流程。
    texect_{exec}
  3. 循环总数不变N 仍是原来的循环次数,但每一项都更轻,整体时间明显下降。
    Tcy≈N×(texec+… )T_{cy} \approx N \times (t_{exec} + \dots)

cpdef函数加速:给函数装两个窗口

现在老田师傅的厨房要兼顾餐厅点菜和后厨配菜。Python 调用一个普通 def 函数,就像客人从前门进店,走点菜、传单、排队的完整流程;Cython 里的 cdef 函数则像后厨侧门,只给内部人员快速进出,但客人不能直接从侧门点菜。cpdef 的出现,刚好给一个函数开了两个窗口:对外保留 Python 原生的调用入口,对内提供一条 C 级别的高速通道。

用 cpdef double compute(double x) 声明函数时,Cython 会生成两个版本:一个给 Python 用,负责类型转换和边界检查;另一个给 C 调用,参数和返回值都直接用原生类型传输。两边的函数体共享同一份逻辑,却避免了 Python 调用时反复的参数装箱、返回值拆箱。

值得注意的是,cpdef 还会自动处理异常:C 层发现错误时,会把状态传递给 Python 层包装成异常抛出。这就像后厨发现食材变质,还要通过前台告诉客人一样——功能完整,速度却快得多。

Speedupcall=tPython calltCython call≫1\displaystyle \text{Speedup}_{call} = \frac{t_{Python\ call}}{t_{Cython\ call}} \gg 1
调用开销的加速比,简单说就是“前门排长队”和“后门直接进”的时间差距。
  1. 双入口机制cpdef 同时生成 Python 可调用包装器和 C 快速通道。
    tPython callt_{Python\ call}
  2. 原生参数传递C 通道里参数直接以 double、int 等原生类型传递,不装箱拆箱。
    tCython callt_{Cython\ call}
  3. 加速比结果两者相除远大于 1,说明高频循环里应优先选择 cpdef 或 cdef。
    tPython calltCython call≫1\frac{t_{Python\ call}}{t_{Cython\ call}} \gg 1

NumPy内存视图:把数组装进编号抽屉

数据分析离不开 NumPy 数组,但 Python 里逐个读取数组元素时,每次都要把数组元素包装成 Python 对象再交给解释器。老田师傅的做法则不同:他把所有调料按格子编号放好,心里有张“位置地图”,直接伸手去第 3 排第 5 格取,不需要打开盒子再确认。Cython 的 typed memoryview 正是这样一张“位置地图”。

声明 cdef double[:] a = np.arange(n) 后,Cython 会直接绑定 NumPy 数组的底层缓冲区,不复制数据,而是记录数组的首地址和每一维的步长。之后在循环里写 a[i],生成的是“基地址 + i × 步长”这样一条指针算术指令,几乎等价于 C 语言直接操作数组。

除了 double[:],你还可以用 int[:]、float[:, :] 等声明任意维度的 memoryview。缺点是它只接受“同类元素连续排列”的缓冲区,但对 NumPy 数组刚好完美契合。零拷贝、按步长寻址、直接读内存,三个优势叠加,数据处理循环的速度就能逼近纯 C。

addr(A[i,j])=base+i×stridei+j×stridej\displaystyle \text{addr}(A[i, j]) = \text{base} + i \times \text{stride}_i + j \times \text{stride}_j
内存视图通过“基地址 + 步长”的公式直接算出元素位置,访问数组时不再逐个翻译成 Python 对象。
  1. 基地址 base数组在内存中的起始位置,整个数组共享同一个起点。
    base\text{base}
  2. 步长 stride沿着某一维度前进一格时,指针需要跳过的字节数。
    stridei,stridej\text{stride}_i, \quad \text{stride}_j
  3. 地址计算将每个维度的偏移乘以对应步长,再加到基地址,就能精确拿到元素。
    base+i×stridei+j×stridej\text{base} + i \times \text{stride}_i + j \times \text{stride}_j

GIL释放技巧:多开几个灶台

不管后厨有多少帮厨,如果只有一个灶台,所有人都得排队等火。CPython 解释器的 GIL(全局解释器锁)就是这口唯一的灶台:同一时刻只允许一个线程执行 Python 字节码。于是多线程写 Python 计算任务时,CPU 再多也只能干瞪眼。Cython 提供了一把“钥匙”:当你确定某个代码块不碰 Python 对象时,可以用 with nogil: 把 GIL 暂时放在一边,让其他线程也能同时开火。

更进一步的并行武器是 cython.parallel.prange。它像老田师傅把一道菜分成好几份,分别交给几个灶台同时炒,最后再汇合装盘。使用 prange 时,循环应满足“每次迭代互相独立”,且循环体内不能直接操作 Python 对象、不能抛出 Python 异常,否则编译器无法安全并行。

不过要提醒你:释放 GIL 不等于“可以乱碰共享对象”。如果循环里还在调用 Python 对象的方法,GIL 就无法真正释放;如果多个线程同时修改同一个变量,还会引发数据竞争。老田师傅的原则是:并行之前先明确这道菜的各部分彼此独立,否则宁可排着队也别把厨房烧了。

Tpar≈N⋅topP+toverhead\displaystyle T_{par} \approx \frac{N \cdot t_{op}}{P} + t_{overhead}
理论上,用 P 个灶台并行执行,总耗时大致从 N 份工作变成 N/P 份,再加上一点点调度开销。
  1. 任务拆分N 个迭代被等分成 P 组,交给 P 个线程同时执行。
    N⋅topP\frac{N \cdot t_{op}}{P}
  2. 并行开销线程创建、任务调度、结果汇合都会产生额外开销。
    toverheadt_{overhead}
  3. 最终公式并行后总时间约等于分组计算时间加上 overhead,核心数越多越划算。
    Tpar≈N⋅topP+toverheadT_{par} \approx \frac{N \cdot t_{op}}{P} + t_{overhead}

编译构建流程:把菜谱写进厨房手册

小周要成为老田师傅,不能只靠熟背菜谱,还得把菜谱正式装订成册、交给中央厨房统一加工。Cython 的“菜谱”就是 .pyx 文件:你把带 cdef、cpdef 的 Python 代码写进去,接下来就是编译三步曲。第一步,cythonize 把 .pyx 翻译成 C 代码;第二步,系统的 C 编译器把这些 C 代码编译成机器码;第三步,链接成 Python 可导入的扩展模块 .so 文件。

最常用的入口是 setup.py 文件加一行 cythonize 调用。在命令行执行 python setup.py build_ext --inplace,就会在当前目录生成 xxx.cpython-xxx-linux-gnu.so。之后 import xxx 就能直接使用编译后的扩展,仿佛导入一个普通 Python 模块。

如果你不想每次写 setup.py,还可以用 pyximport 在本地直接导入 .pyx 文件:只需一句 pyximport.install(),Cython 会在背后自动完成上述三步。它不适合在生产环境长期使用,但在快速原型验证里非常舒服,相当于“菜谱写完立刻试吃”。

Tbuild=tcythonize+tCC+tlink\displaystyle T_{build} = t_{cythonize} + t_{CC} + t_{link}
一次编译的总时间等于“翻译成 C + 编译成机器码 + 链接成扩展”三段时间之和,编译做一次,收益却是永恒的。
  1. 翻译阶段cythonize 根据类型注解生成对应的 C 代码。
    tcythonizet_{cythonize}
  2. 编译阶段系统 C 编译器将 C 源码变成目标机器码文件。
    tCCt_{CC}
  3. 链接阶段把机器码与 Python 运行时符号链接成 .so 扩展,耗时较短。
    tlinkt_{link}

性能对决与改造抉择:先量再改再上桌

厨房要上新菜,老田师傅不会一口气把全部菜谱都改掉,而是先站在出菜口观察:哪个菜销量最高、出菜最慢,就从哪里下手。Cython 优化也一样:先让 Python 代码跑起来,用 cProfile 或 time.perf_counter 测出每个函数耗时,确认热点在大循环还是反复调用的函数里,再决定要不要动手改造。

基准测试最常见的指标是“加速比”:分别测出纯 Python 版本和 Cython 版本的运行时间,然后相除。改造策略建议“渐进式”:先把最内层、重复次数最多的循环抽取出来,加上 cdef 和 memoryview,验证结果正确后再继续扩展。不要一上来就把整个项目重写——那样改动大、风险高,收益却不一定会更好。

最终厨房的结论很清晰:Cython 不是用来替代 Python 的,而是给“最需要速度的那几道菜”开一个专用灶台。数据科学家要做的,就是拿计时器量一量,找到那道排队最久的菜,然后让老田师傅用类型注解和内存视图把它一次做好。

speedup=TPythonTCython\displaystyle \text{speedup} = \frac{T_{Python}}{T_{Cython}}
加速比就是“改造前耗时 ÷ 改造后耗时”。大于 1 说明变快了,超过 10 倍就是一场漂亮的胜利。
  1. 测量 Python 基准用高精度计时器记录原始实现总耗时。
    TPythonT_{Python}
  2. 测量 Cython 版本在相同输入、相同环境下记录改造后耗时。
    TCythonT_{Cython}
  3. 计算加速比两者相除得到倍数,用来判断优化是否值得长期维护。
    TPythonTCython\frac{T_{Python}}{T_{Cython}}

AI 应用场景

特征工程中的批量计算加速

阶段:数据处理与特征构造

解决的问题:传统 pandas 逐行 apply 处理上百万样本时极慢,且每次调用 Python 函数都要付出解释器开销。通过 Cython cpdef 包装特征计算函数、用 typed memoryview 读入原始数组,可让特征提取飞起来。

具体案例:某风控团队每日处理 500 万条交易记录,原始 Python 做 20 个特征计算需要 40 分钟;把核心循环用 Cython 重写后,整个过程压缩到 3 分钟,模型训练前的数据产出不再成为瓶颈。

图像预处理流水线

阶段:模型训练前数据增强与归一化

解决的问题:图像数据通常是 NumPy 三维数组,循环里反复切片、缩放、归一化会产生大量中间对象。Cython 通过内存视图直接访问原始像素缓冲区,配合 with nogil 并行处理多张图片,吞吐量成倍提升。

具体案例:某自动驾驶团队需要对每帧 1080p 图像做 resize、白平衡和归一化。用 Cython 重写预处理内核后,每秒可处理的帧数从 20 帧提升到 180 帧,GPU 训练时数据供给不再“断粮”。

量化金融中的实时因子计算

阶段:策略信号实时计算

解决的问题:高频行情序列上滚动均值、波动率等因子如果逐个用 Python 循环算,延迟极高。Cython 的静态类型和 prange 可以同时对多只股票并行计算因子,毫秒级给出信号。

具体案例:某量化团队对 3000 只股票的分钟线计算 10 个技术指标,纯 Python 需要 8 秒;用 Cython 编写指标函数并启用 prange 并行后,同一批计算降到 0.4 秒,接近实时决策的要求。

Python 演示

python38 行
# cython_demo.py# 运行前先安装: pip install cythonimport timeimport cython  @cython.cfuncdef cy_sum_squares(n: cython.long) -> cython.long:    total: cython.long = 0    i: cython.long    for i in range(n):        total += i * i    return total  def py_sum_squares(n):    total = 0    for i in range(n):        total += i * i    return total  def bench(n=10_000_000):    t0 = time.perf_counter()    py_result = py_sum_squares(n)    t1 = time.perf_counter()    cy_result = cy_sum_squares(n)    t2 = time.perf_counter()    py_time = t1 - t0    cy_time = t2 - t1    print(f"Python time: {py_time:.3f}s")    print(f"Cython time: {cy_time:.5f}s")    print(f"Speedup: {py_time / cy_time:.1f}x")    assert py_result == cy_result  if __name__ == "__main__":    bench()
目的:对比纯 Python 循环和 Cython 编译版本计算“1 到 N 的平方和”的耗时,直观看到加速比。
  1. @cython.cfunc让这个函数在 Cython 编译时变成 C 级别的函数,不经过 Python 调用栈。
  2. n: cython.long入参被固定为 C 长整型,省去参数类型检查。
  3. total: cython.long = 0累加器也声明为静态类型,循环内不会装箱拆箱。
  4. i: cython.long循环变量提前声明,避免每轮迭代都创建 Python int。
  5. py_time / cy_time两个版本的时间相除即加速比,数字越大说明改造越值得。
  6. assert py_result == cy_result用结果一致性校验保证 Cython 版本没有算错。

常见误区

误区我只要在 Python 函数里写 `x: int` 或 `x: float`,速度就会自动变快。为什么我加了注解后跑起来更慢了?

正解普通 Python 的类型注解只是“元数据”,默认不会改变解释器运行方式。真正提速靠 Cython 的 cdef 编译期静态类型,或 cpdef 的 C 通道。如果只在纯 Python 里写 def f(x: int) -> int:,解释器还是照旧动态执行,甚至还要多花一点时间解析注解。想提速,应该把代码放进 .pyx 文件并经过 Cythonize 编译。

为何常见:大家看到“类型注解”四个字,自然联想到 Cython,但 Python 和 Cython 的注解在底层机制上完全不同,容易混淆。

误区我没系统学过 C 语言,肯定学不会 Cython。Cython 是给底层性能黑客准备的武器。

正解Cython 的语法是 Python 的超集:你在 .pyx 里写普通 Python 代码也能跑,只是不会快。要提速只需掌握 cdef、cpdef、typed memoryview 五个以内的高频关键字,完全不需要手动管理内存、不需要写指针运算。C 编译器是自动调用的,普通工程师也能轻松上手。

为何常见:Cython 名字里带着“C”,生成产物又是 .c 文件,大家会被底层实现吓住;实际上使用体验更像“加了编译器的 Python”。

误区我用了 `with nogil` 和 `prange`,就能让多线程同时读写同一个 Python 字典,速度应该直接翻 N 倍。

正解with nogil 只能让不触碰 Python 对象的代码真正并行。在块内操作 Python 对象不仅不会提升速度,还会触发运行时错误或段错误。多线程想修改共享结果时,需要自己设计“各自写独立位置、最后合并”的模式,或使用锁、原子操作。GIL 只是把“灶台”空出来,并不会自动帮你做好分配和合并。

为何常见:人们把“释放 GIL”等同于“线程安全”,但 GIL 保护的是解释器内部状态,不保护你的业务共享数据。

误区只要我在 Cython 里用了 NumPy 数组,循环就会自动变成 C 速度。

正解在 Cython 里直接拿 NumPy 数组下标访问元素,仍然可能产生 Python 调用和对象转换。正确做法是把数组声明成 typed memoryview,例如 cdef double[:] a = arr,这样下标访问会直接走内存地址计算,才称得上接近 C 速度。数组本身不会自动“变身”,需要你给编译器一把钥匙。

为何常见:NumPy 底层本来就是 C 实现,大家容易误以为“所有 NumPy 操作都很快”,但逐元素 Python 级访问依旧是瓶颈。

面试题

Q1. 以下哪项最能描述 Cython 的主要作用?

单选题 · easy

  • A.把 Python 源码编译为 C 扩展← 正确答案
  • B.为 Python 提供静态语法检查
  • C.把 Python 源码翻译成汇编
  • D.让 Python 执行 Java 字节码

答案:A。Cython 的本质是 Python 的超集,它把 .pyx 文件先翻译成 C 代码,再编译成 Python 可导入的扩展模块。B 是 linter 的用途,C 并不准确,D 则把方向完全搞错了。

Q2. Cython 源码文件默认的扩展名是什么?

单选题 · easy

  • A.扩展名为 .pyx 的文件← 正确答案
  • B.扩展名为 .pyd 的文件
  • C.扩展名为 .c 的文件
  • D.扩展名为 .cpp 的文件

答案:A。.pyx 是 Cython 源码的标准后缀,.pyd 是 Windows 下编译出的扩展库,.c/.cpp 是 cythonize 生成的中间产物,不是原始源码。

Q3. 在 Cython 中声明一个静态类型变量的关键字是什么?

单选题 · easy

  • A.cdef 声明静态类型变量← 正确答案
  • B.def 声明普通 Python 函数
  • C.class 声明一个对象类
  • D.import 导入一个外部模块

答案:A。cdef int x = 0 表示把 x 声明为 C 原生整数;def 只声明普通 Python 函数,class 用来定义类,import 用来导入模块,它们和静态类型声明无关。

Q4. 关于 CPython 的 GIL,下列说法正确的是?

单选题 · mid

  • A.Python 线程同一时刻只能执行一条字节码← 正确答案
  • B.释放 GIL 后 Python 对象访问自动安全
  • C.GIL 能让线程在多个核心上并行运行
  • D.必须手动加锁才能读取普通 Python 列表

答案:A。GIL 保证同一时刻只有一个线程执行 Python 字节码,所以多线程做 CPU 计算时无法跑满多核。释放 GIL 只允许非 Python 代码并行,并不等于对象访问安全;读取列表在 Python 层也有解释器保护,不需要手动加锁。

Q5. cpdef 函数相比 cdef 函数的优势是什么?

单选题 · mid

  • A.Python 与 C 均可调用← 正确答案
  • B.只能被 Cython 内部用
  • C.一定比 def 函数慢
  • D.必须写在 Python 中

答案:A。cpdef 会同时生成 Python 和 C 两个入口,外部 Python 可以正常调用,内部 Cython 调用时又走快速通道。cdef 只能被 Cython 内部使用,因此对外暴露核心接口时优先选 cpdef。

Q6. 在 Cython 中高效读写 NumPy 数组的推荐方式是什么?

单选题 · mid

  • A.使用 typed memoryview← 正确答案
  • B.使用 Python 列表逐个遍历
  • C.使用 print 打印数组内容
  • D.使用字符串拼接出数组

答案:A。typed memoryview 直接绑定数组底层缓冲区,通过步长寻址读取元素,零拷贝且快。Python 列表遍历和字符串拼接反而会引入巨大的解释器开销,print 只是输出工具,不能加速访问。

Q7. 在 Cython 中绕过 GIL 的关键语法是什么?

单选题 · mid

  • A.with nogil 包裹代码块← 正确答案
  • B.with gil 强制获取 GIL
  • C.lock gil 获取全局锁
  • D.free gil 释放全局锁

答案:A。标准写法是 with nogil:,它要求块内不操作 Python 对象。with gil 用于在已释放 GIL 的上下文里重新获取,lock gil 和 free gil 都不是 Cython 关键字。

Q8. cdef 类型注解可以消除下列哪些开销?

多选题 · hard

  • A.消除运行时的动态查找← 正确答案
  • B.消除对象装箱和拆箱← 正确答案
  • C.消除一部分类型检查分支← 正确答案
  • D.消除循环中的全部 I/O 开销

答案:ABC。静态类型让编译器直接生成机器指令,省去名字查找、对象装箱拆箱和类型检查分支。但 I/O 是外部资源操作,类型注解无法让它消失,所以 D 不能选。

Q9. 下列哪些情况会让 Cython 的加速效果大打折扣?

多选题 · hard

  • A.循环内部反复调用 Python 函数← 正确答案
  • B.依赖未注解的全局变量← 正确答案
  • C.反复对 NumPy 对象进行下标操作← 正确答案
  • D.用 cdef 声明热点循环局部变量

答案:ABC。循环里调用 Python 函数会重新回到解释器开销;全局变量每次访问仍需查找;NumPy 对象下标若未用 memoryview,也会产生转换成本。用 cdef 声明局部变量恰恰是正确做法,不会拖累性能。

Q10. 关于 cython.parallel.prange,下列说法正确的有哪些?

多选题 · extreme

  • A.每次迭代必须相互独立← 正确答案
  • B.循环中不能直接操作 Python 对象← 正确答案
  • C.共享累积变量需要加锁或同步← 正确答案
  • D.循环中调用任意 Python 函数都安全

答案:ABC。prange 会把循环分配给多个线程并行执行,因此迭代必须独立,否则结果不确定;循环体触碰 Python 对象会让编译器无法安全释放 GIL;多个线程写同一个累计变量需要加锁或原子操作。D 明显错误,Python 函数调用会破坏并行前提。