- 内容提要
- 前言
- 作者简介
- 封面简介
- 第1章 理解高性能 Python
- 第2章 通过性能分析找到瓶颈
- 2.1 高效地分析性能
- 2.2 Julia 集合的介绍
- 2.3 计算完整的 Julia 集合
- 2.4 计时的简单方法——打印和修饰
- 2.5 用 UNIX 的 time 命令进行简单的计时
- 2.6 使用 cProfile 模块
- 2.7 用 runsnakerun 对 cProfile 的输出进行可视化
- 2.8 用 line_profiler 进行逐行分析
- 2.9 用 memory_profiler 诊断内存的用量
- 2.10 用 heapy 调查堆上的对象
- 2.11 用 dowser 实时画出变量的实例
- 2.12 用 dis 模块检查 CPython 字节码
- 2.13 在优化期间进行单元测试保持代码的正确性
- 2.14 确保性能分析成功的策略
- 2.15 小结
- 第3章 列表和元组
- 第4章 字典和集合
- 第5章 迭代器和生成器
- 第6章 矩阵和矢量计算
- 第7章 编译成 C
- 第8章 并发
- 第9章 multiprocessing 模块
- 第10章 集群和工作队列
- 第11章 使用更少的 RAM
- 第12章 现场教训
6.5 numexpr:让就地操作更快更简单
numpy对矢量操作优化的一个缺陷是它一次只能处理一个操作。这意味着,当我们对numpy矢量进行A * B + C这样的操作时,首先要等待A * B操作完成,数据保存在一个临时矢量中,然后将这个新的矢量和C相加。正如例6-14中使用就地操作的扩散代码所示。
然而,有许多模块可以对这点进行优化。numexpr模块可以将整个矢量表达式 编译成非常高效的代码,可以将缓存失效以及临时变量的数量最小化。另外,它还能利用多个CPU核心(更多信息见第9章)以及Intel芯片专用的指令集来将速度最大化。
很容易修改代码来使用numexpr:我们只需要将表达式重写为使用本地变量的字符串即可。表达式会在后台被编译成优化过的代码(并被缓存来确保相同的表达式不会导致同样的编译过程发生多次)并运行。例6-19显示了evolve函数改用numexpr是多么的简单。我们在evaluate函数中使用out参数,这样numexpr就不会为返回一个新矢量而分配内存。
例6-19 使用numexpr来进一步优化大矩阵操作
from numexpr import evaluate def evolve(grid, dt, next_grid, D=1): laplacian(grid, next_grid) evaluate("next_grid*D*dt+grid", out=next_grid)
numexpr的一个重要特点是它考虑到了CPU缓存。它特地移动数据来让各级 CPU缓存能够拥有正确的数据让缓存失效最小化。当我们对更新后的代码运行perf(例6-20)时,我们看到速度的确是提升了。但是,如果我们针对一个较小的512×512的矩阵(见本章最后的图6-4)比较速度,我们会看到大约15%的速度下降。这是为什么?
例6-20 使用了numpy就地内存操作特化laplacian函数以及numexpr的性能指标
$ perf stat -e cycles,stalled-cycles-frontend,stalled-cycles-backend,instructions,\ cache-references,cache-misses,branches,branch-misses,task-clock,faults,\ minor-faults,cs,migrations -r 3 python diffusion_numpy_memory2_numexpr.py Performance counter stats for 'python diffusion_numpy_memory2_numexpr.py' (3 runs): 5,940,414,581 cycles # 1.447 GHz 3,706,635,857 stalled-cycles-frontend # 62.40% frontend cycles idle 2,321,606,960 stalled-cycles-backend # 39.08% backend cycles idle 6,909,546,082 instructions # 1.16 insns per cycle # 0.54 stalled cycles per insn 261,136,786 cache-references # 63.628 M/sec 11,623,783 cache-misses # 4.451 % of all cache refs 627,319,686 branches # 152.851 M/sec 8,443,876 branch-misses # 1.35% of all branches 4104.127507 task-clock # 1.364 CPUs utilized 9,786 page-faults # 0.002 M/sec 9,786 minor-faults # 0.002 M/sec 8,701 context-switches # 0.002 M/sec 60 CPU-migrations # 0.015 K/sec 3.009811418 seconds time elapsed
numexpr引入的大多数额外的机制都跟缓存相关。当我们的矩阵较小且计算所需的所有数据都能被放入缓存时,这些额外的机制只是白白增加了更多的指令而不能对性能有所帮助。另外,将字符串编译成矢量操作也会有很大的开销。当程序运行的整体时间较少时,这个开销就会变得相当引人注意。不过,当我们增加矩阵的大小时,我们会发现numexpr比原生numpy更好地利用了我们的缓存。而且,numexpr利用了多核来进行计算并尝试填满每个核心的缓存。当矩阵较小时,管理多核的开销盖过了任何可能的性能提升。
我们用来跑测试的电脑有20480 KB的缓存(Intel Xeon E5-2680)。因为有两个数组需要处理,一个作为输入,另一个作为输出,所以我们可以轻易计算出需要足以填满缓存的矩阵大小。矩阵元素的总数是20480 KB/64 bit = 2560000。因为我们有两个矩阵,这一总数被平分到两个对象中(所以每个对象最多可以有2560000 / 2 = 1280000个元素)。最后,对这个数字求平方根可以让我们知道能存放这么多元素的矩阵的大小。总的来说,这意味着大概两个大小为1131的2维数组就会填满缓存( )。但实际上,我们自己没办法完全填满缓存(其他程序会占用部分缓存),所以现实来说大概能填入两个800×800的数组。见表6-1和表6-2,我们可以看到当矩阵大小从512×512跳到1024×1024时,numexpr代码的性能就开始超越纯numpy。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论