探究数组排序提升Python程序的循环的运行效率的原因


Posted in Python onApril 01, 2015

早上我偶然看见一篇介绍两个Python脚本的博文,其中一个效率更高。这篇博文已经被删除,所以我没办法给出文章链接,但脚本基本可以归结如下:
fast.py
 

import time
a = [i for i in range(1000000)]
sum = 0
t1 = time.time()
for i in a:
  sum = sum + i
t2 = time.time()
print t2-t1

slow.py
 

import time
from random import shuffle
a = [i for i in range(1000000)]
shuffle(a)
sum = 0
t1 = time.time()
for i in a:
  sum = sum + i
t2 = time.time()
print t2-t1

如你所见,两个脚本有完全相同的行为。都产生一个包含前一百万个整数的列表,并打印对这些整数求和的时间。唯一的不同是 slow.py 先将整数随机排序。尽管这看起来有些奇怪,似乎随机化足够将程序明显变慢。在我机器上,运行的Python2.7.3, fast.py 始终比 slow.py 快十分之一秒(fast.py 执行大约耗时四分之三秒,这是不平常的增速)。你不妨也试试看。(我没有在Python3上测试,但结果应该不会差太多。)

那为什么列表元素随机化会导致这么明显的减速呢?博文的原作者把这记作“分支预测(branch prediction)”。如果你对这个术语不熟悉,可以在 StackOverflow 的提问中看看,这里很好地解释了这个概念。(我的疑虑是原文的原作者遇到了这个问题或者与此类似的问题,并把这个想法应用到不太适合应用的Python片段中。)

当然,我怀疑分支预测(branch prediction)是否是真正导致问题的原因。在这份Python代码中没有顶层条件分支,而且合乎情理的是两个脚本在循环体内有严格一致的分支。程序中没有哪一部分是以这些整数为条件的,并且每个列表的元素都是不依赖于数据本身的。当然,我还是不确定python是否算得上足够“底层”,以至于CPU级别的分支预测能够成为python脚本性能分析中的一个因素。Python毕竟是一门高级语言。

因此,如果不是分支预测的原因,那为什么 slow.py 会这么慢?通过一点研究,经过一些“失败的开端”之后,我觉得自己找到了问题。这个答案需要对Python内部虚拟机有点熟悉。

失败的开端:列表vs.生成器(lists and generators)

我的第一想法是Python对排序的列表[i for i in range(1000000)] 的处理效率要比随机列表高。换句话说,这个列表可以用下面的生成器替代:

def numbers():
  i = 0
  while i < 1000000:
    yield i
    i += 1

我想这可能在时间效率上更高效些。毕竟,如果Python在内部使用生成器替代真正的列表可以避免在内存中一次保存所有整数的麻烦,这可以节省很多开销。slow.py 中的随机列表不能轻易的被一个简单生成器捕获,所有VM(虚拟机)无法进行这样的优化。

然而,这不是一个有用的发现。如果在slow.py的 shuffle() 和循环之间插入 a.sort(),程序会像 fast.py一样快。很明显,数字排序后的一些细节让程序更快。

失败的开端:列表对比数组

我的第二个想法是有可能数据结构造成的缓存问题。a 是一个列表,这自然让我相信a实际上是通过链表来实现的。如果shuffle操作故意随机化这个链表的节点,那么 fast.py 可能可以把列表的所有链表元素分配在相邻地址,从而采用高级局部缓存,而slow.py会出现很多缓存未命中的情况,因为每个节点引用不在同一个缓存行上的另外一个节点。

不幸的是,这也不对。Python的列表对象不是链接的列表,而是真正意义上的数组。尤其是用C结构体定义了Python列表对象:
 

typedef struct {
 PyObject_VAR_HEAD
 PyObject **ob_item;
 Py_ssize_t allocated;
} PyListObject;

……换句话说,ob_item 是一个指向PyObjects指针数组的指针,并且分配的大小是我们分配给数组的大小。因此,这对于解决这个问题也没帮助(尽管这对我不确定Python中关于列表操作的算法复杂度有些安慰:列表的添加操作算法复杂度是O(1),访问任意列表元素的算法复杂度是O(1),等等)。我只是想说明为什么Guido选择称它们为列表“lists”而不是数组“arrays”,而实际上它们却是数组。

解决办法:整体对象

数组元素在内存中是相邻的,因此这样的数据结构不会带来缓存问题。事实证明缓存位置是 slow.py 变慢的原因,但这来自于一个意料之外的地方。在Python中,整数是分配在堆中的对象而不是一个简单的值。尤其是在虚拟机中,整数对象看起来像下面这样:
 

typedef struct {
 PyObject_HEAD
 long ob_ival;
} PyIntObject;

上面结构体中唯一“有趣”的元素是ob_ival(类似于C语言中的整数)。如果你觉得使用一个完整的堆对象来实现整数很浪费,你也许是对的。很多语言就为了避免这样而做优化。例如 Matz的 Ruby 解释器通常以指针的方式存储对象,但是对频繁使用的指针做例外处理。简单来说,Ruby解释器把定长数作为对象应用塞到同样的空间,并用最低有效位来标记这是一个整数而不是一个指针(在所有现代系统中,malloc总是返回以2的倍数对齐的内存地址)。在那时,你只需要通过合适的位移来获取整数的值——不需要堆位置或者重定向。如果CPython做类似的优化,slow.py 和 fast.py 会有同样的速度(而且他们可能都会更快)。

那么CPython是怎样处理整数的呢?解释器的什么行为给我们如此多的疑惑?Python解释器每次将整数分配到40Byte的“块”中(block)。当Python需要生成新的整型对象时,就在当前的整数“块”中开辟下一个可用空间,并将整数存储在其中。我们的代码在数组中分配一百万个整数,大部分相邻的整数会被放到相邻的内存中。因此,在有序的一百万个数中遍历展现出不错的缓存定位,而在随机排序的前一百万个数中定位出现频繁的缓存未命中。

因此,“为什么对数组排序使得代码更快”的答案就是它根本没有这个作用。没有打乱顺序的数组遍历的速度更快,因为我们访问整型对象的顺序和分配的顺序一致(他们必须被分配)。

Python 相关文章推荐
使用Python编写一个最基础的代码解释器的要点解析
Jul 12 Python
Python微信企业号开发之回调模式接收微信端客户端发送消息及被动返回消息示例
Aug 21 Python
Python中执行存储过程及获取存储过程返回值的方法
Oct 07 Python
python中logging包的使用总结
Feb 28 Python
在Python中输入一个以空格为间隔的数组方法
Nov 13 Python
Python实现将HTML转成PDF的方法分析
May 04 Python
Python 图像处理: 生成二维高斯分布蒙版的实例
Jul 04 Python
Django应用程序入口WSGIHandler源码解析
Aug 05 Python
python智联招聘爬虫并导入到excel代码实例
Sep 09 Python
Python 操作mysql数据库查询之fetchone(), fetchmany(), fetchall()用法示例
Oct 17 Python
Python实现播放和录制声音的功能
Aug 12 Python
提高python代码运行效率的一些建议
Sep 29 Python
用Python编写分析Python程序性能的工具的教程
Apr 01 #Python
对Python新手编程过程中如何规避一些常见问题的建议
Apr 01 #Python
利用Django框架中select_related和prefetch_related函数对数据库查询优化
Apr 01 #Python
用实例详解Python中的Django框架中prefetch_related()函数对数据库查询的优化
Apr 01 #Python
Python的Django框架中的select_related函数对QuerySet 查询的优化
Apr 01 #Python
简单的Python2.7编程初学经验总结
Apr 01 #Python
极简的Python入门指引
Apr 01 #Python
You might like
转生史莱姆:萌王第一次撸串开心到飞起,哥布塔撸串却神似界王神
2018/11/30 日漫
微信公众平台网页授权获取用户基本信息中授权回调域名设置的变动
2014/10/21 PHP
smarty中常用方法实例总结
2015/08/07 PHP
解析WordPress中的post_class与get_post_class函数
2016/01/04 PHP
Javascript中call的两种用法实例
2013/12/13 Javascript
基于jQuery的checkbox全选问题分析
2016/11/18 Javascript
js实现表单提交后不重新刷新当前页面
2016/11/30 Javascript
全选复选框JavaScript编写小结(附代码)
2017/08/16 Javascript
jquery在vue脚手架中的使用方式示例
2017/08/29 jQuery
Angular 数据请求的实现方法
2018/05/07 Javascript
vue cli3适配所有端方案的实现
2020/04/13 Javascript
javascript实现前端成语点击验证
2020/06/24 Javascript
[52:05]EG vs OG 2019国际邀请赛小组赛 BO2 第二场 8.16
2019/08/18 DOTA
在Windows8上的搭建Python和Django环境
2014/07/03 Python
python中元类用法实例
2014/10/10 Python
基于python的字节编译详解
2017/09/20 Python
python使用PyCharm进行远程开发和调试
2017/11/02 Python
Python缓存技术实现过程详解
2019/09/25 Python
解决Pycharm中恢复被exclude的项目问题(pycharm source root)
2020/02/14 Python
Python模拟FTP文件服务器的操作方法
2020/02/18 Python
Scrapy框架介绍之Puppeteer渲染的使用
2020/06/19 Python
python爬虫快速响应服务器的做法
2020/11/24 Python
Python基于template实现字符串替换
2020/11/27 Python
python 实现全球IP归属地查询工具
2020/12/18 Python
EM Cosmetics官网:由彩妆大神Michelle Phan创办的独立品牌
2020/04/27 全球购物
高三学习决心书
2014/03/11 职场文书
四风问题查摆材料
2014/08/25 职场文书
机械工程及自动化专业求职信
2014/09/03 职场文书
教师党员个人自我剖析材料
2014/09/29 职场文书
涉外离婚协议书怎么写
2014/11/20 职场文书
部队2014年终工作总结
2014/11/27 职场文书
公证书格式
2015/01/23 职场文书
自荐信格式范文
2015/03/04 职场文书
纪律委员竞选稿
2015/11/19 职场文书
win11无法登录onedrive错误代码0x8004def7怎么办 ?
2022/04/05 数码科技
python 单机五子棋对战游戏
2022/04/28 Python