“pypy为什么能让python比c还快” 刷屏了,原文讲的内容偏理论,干货比较少。我们可以再深入一点点,了解pypy的真相。
话不多说,正式开始,本文包括下面几个部分:
我们先从最基本的一些语言分类概念聊起,对这部分内容了解的朋友可以跳过。
如果在编译时知道变量的类型,则该语言为静态类型。静态类型语言的常见示例包括Java,C,C ++,FORTRAN,Pascal和Scala。在静态类型语言中,一旦使用类型声明了变量,就无法将其分配给其他不同类型的变量,这样做会在编译时引发类型错误。
# java
int data;
data = 50;
data = “Hello Game_404!”; // causes an compilation error
如果在运行时检查变量的类型,则语言是动态类型的。动态类型语言的常见示例包括JavaScript,Objective-C,PHP,Python,Ruby,Lisp和Tcl。
在动态类型语言中,变量在运行时通过赋值语句绑定到对象,并且可以在程序执行期间将相同的变量绑定到不同类型的对象。
# python
data = 10;
data = "Hello Game_404!"; // no error caused
data = data + str(10)
一般来说静态语言编译成字节码执行,动态语言使用解释器执行。编译型语言性能更高,但是较难移植到不同的CPU架构体系和操作系统。解释型语言易于移植,性能会比编译语言要差得多。这是频谱的两个极端。
强类型语言是一种变量被绑定到特定数据类型的语言,如果类型与表达式中的预期不一致,将导致类型错误,比如下面这个
# python
temp = “Hello Game_404!”
temp = temp + 10; // program terminates with below stated error (TypeError: must be str, not int)
python和我们感觉不一致,背叛了弱类型语言,不像世界上最好的语言:(
# php
$temp = “Hello Game_404!”;
$temp = $temp + 10; // no error caused
echo $temp;
python是一门动态编程语言,由特定的解释器解释执行。下面是一些解释器实现:
还有几个相关概念
这里大家会有一个疑问,python不是解释型语言嘛?怎么又有编译后的pyc。
是这样的: py文件编译成pyc后,解释器默认 优先 执行pyc文件,这样可以加快python程序的 启动速度 (注意是启动速度)。继背叛弱类型语言后,python这个鬼又在编译语言和解释语言之间横跳。
还有一个事件是Go语言在1.5版本实现自举。Go语言在1.5版本之前使用c实现的编译器,在1.5版本时候使用Go实现了自己的编译器,这里有一个鸡生蛋和蛋生鸡的过程,也挺有意思。
pypy使用python的子集rpython实现了解释器,和前面介绍的Go的自举有点类似。反常识的是rpython的解释器会比c实现的解释器快? 主要是因为pypy使用了JIT技术。
Just-In-Time (JIT) Compiler 试图通过对机器码进行一些实际的编译和一些解释来获得两全其美的方法。简而言之,以下是JIT编译为提高性能而采取的步骤:
这也是 “pypy为什么能让python比c还快” 一文中的示例展现出来的能力。pypy除了速度快外,还有下面一些特点:
以上都是宣称
pypy这么强,快和省都占了,为什么没有大规模流行起来呢? 我个人认为,主要还是python的原因。
需要注意的是,pypy一样也有GIL的存在, 所以高并发主要在stackless。
我们可以编写性能测试用例,用代码说话,对各个实现进行对比。本文的测试用例并不严谨,不过也足够说明一些问题了.
原文中累加测试用例是100000000次,我们减少成1000次
import time
start = time.time()
number = 0
for i in range(1000):
number += i
print(number)
print(f"Elapsed time: {time.time() - start} s")
测试结果如下图
结果显示运行1000次循环的情况下cpython要比pypy快,这和循环100000000次 相反 。用下面的例子可以非常形象的解释这一点。
假设您想去一家离您家很近的商店。您可以步行或开车。您的汽车显然比脚快得多。但是,请考虑需要执行以下操作:
开车要涉及很多开销,如果您想去的地方在附近,这并不总是值得的!现在想想如果您想去五十英里外的邻近城市会发生什么。开车去那里而不是步行肯定是值得的。
尽管速度的差异并不像上面类比那么明显,但是PyPy和CPython的情况也是如此
我们横向对比一下c,python3, pypy3, js 和lua的性能。
# js
const start = Date.now();
let number = 0
for (i=0;i<100000000;i++){
number += i
}
console.log(number)
const millis = Date.now() - start;
console.log(`milliseconds elapsed = `, millis);
# lua
local starttime = os.clock();
local number = 0
local total = 100000000-1
for i=total,1,-1 do
number = number+i
end
print(number)
local endtime = os.clock();
print(string.format("elapsed time : %.4f", endtime - starttime));
# c
#include
#include
const long long TOTAL = 100000000;
long long mySum()
{
long long number=0;
long long i;
for( i = 0; i < TOTAL; i++ )
{
number += i;
}
return number;
}
int main(void)
{
// Start measuring time
clock_t start = clock();
printf("%llu \n", mySum());
// Stop measuring time and calculate the elapsed time
clock_t end = clock();
double elapsed = (end - start)/CLOCKS_PER_SEC;
printf("Time measured: %.3f seconds.\n", elapsed);
return 0;
}
测试结果可见,c无疑是最快的,秒杀其它语言,这是编译语言的特点。在解释语言中,pypy3表现配得上优秀二字。
内存占用
测试用例中增加内存占用的输出
p = psutil.Process()
mem = p.memory_info()
print(mem)
测试结果如下
# python3
pmem(rss= 9027584, vms=4747534336, pfaults= 2914, pageins=1)
# pypy3
pmem(rss=39518208, vms=5127745536, pfaults=12188, pageins=58)
pypy3的内存占用会比python3要高,这个才科学,用内存空间换了运行时间。当然这个评测并不严谨,实际情况如何,pypy宣称的内存占用较少,我表示怀疑,但是没有证据。
了解语言的性能比较后,我们再看看一些性能优化的方法,这对在cpython和pypy之间选型有帮助。
python中使用c函数,比如这里的累加可以使用reduce替换,可以提高效率
def my_add(a, b):
return a + b
number = reduce(add, range(100000000))
优化最关键的地方,提高算法效率,减少循环。更改一下累加的需求,假设我们是求100000000以内的偶数的和,下面展示了使用range的步进减少循环次数来提高性能
try:
xrange # python2注意使用xrange是迭代器,而range是返回一个list
except NameError: # python3
xrange = range
def test_0():
number = 0
for i in range(100000000):
if i % 2 == 0:
number += i
return number
def test_1():
number = 0
for i in xrange(0, 100000000, 2):
number += i
return number
python3可以使用类型注解,提高代码可读性。类型确定逻辑上对性能有帮助,每次处理数据的时候,不用再进行类型推断。
number: int = 0
for i in range(100000000):
number += i
内存相当于一个空间,我们要用不同的盒子去填充它。图中左边部分1,都使用长度为4(想像float类型)的盒子填充,一行一个,速度最快;图中中间部分2,使用长度为3(想像long类型)和长度为1(想像int类型)的箱子,一行2个,也挺快;图中右侧3,虽然箱子长度仍然是3和1,但是由于没有刻度,填充时候需要试装,所以速度最慢。
优化到最后,最重量级的内容登场:高斯求和算法。高斯的故事,想必大家都不陌生,下面是算法实现
def gaussian_sum(total: int) -> int:
if total & 1 == 0:
return (1 + total) * int(total / 2)
else:
return total * int((total - 1) / 2) + total
# 4999999950000000
number = gaussian_sum(100000000 - 1)
使用高斯求和后,程序秒开。这大概就是业内面试,要考算法的真相,也是算法的魅力所在。
简单介绍一下优化的原则,主要是下面2点:
python3 -m timeit 'x=3' 'x%2'
10000000 loops, best of 5: 25.3 nsec per loop
python3 -m timeit 'x=3' 'x&1'
5000000 loops, best of 5: 41.3 nsec per loop
python2 -m timeit 'x=3' 'x&1'
10000000 loops, best of 3: 0.0262 usec per loop
python2 -m timeit 'x=3' 'x%2'
10000000 loops, best of 3: 0.0371 usec per loop
上面示例展示了,求奇偶的情况下,python3中位运算比取模慢,这是个反直觉推测的地方。而且需要注意的是,python2和python3表现相反,所以性能优化要实测,注意环境和实效性。
pypy还有下面一些特性:
使用slots在python对象中,可以减少对象内存占用,提高效率,下面是测试用例
def test_0():
class Player(object):
def __init__(self, name, age):
self.name = name
self.age = age
players = []
for i in range(10000):
p = Player(name="p" + str(i), age=i)
players.append(p)
return players
def test_1():
class Player(object):
__slots__ = "name", "age"
def __init__(self, name, age):
self.name = name
self.age = age
players = []
for i in range(10000):
p = Player(name="p" + str(i), age=i)
players.append(p)
return players
测试日志如下
# python3 slots
pmem(rss=10776576, vms=5178499072, pfaults=3351, pageins=58)
Elapsed time: 0.010818958282470703 s
# python3 默认
pmem(rss=11792384, vms=5033795584, pfaults=3587, pageins=0)
Elapsed time: 0.01322031021118164 s
# pypy3 slots
pmem(rss=40042496, vms=5263011840, pfaults=12341, pageins=4071)
Elapsed time: 0.005321025848388672 s
# pypy3 默认
pmem(rss=39862272, vms=4974653440, pfaults=12280, pageins=0)
Elapsed time: 0.004619121551513672 s
pypy最重要的特性还是stackless,支持高并发。这里有IO密集型任务(I/O-bound)和CPU密集型任务(compute-bound)的区分,CPU密集型任务的代码,速度很慢,是因为执行大量CPU指令,比如上文的for循环;I / O密集型,速度因磁盘或网络延迟而变慢,这两者之间是有区别的。
python是一门解释型编程语言,具有多种解释器实现,常见的是cpython的实现。pypy使用了JIT技术,在一些常见的场景下可以显著提高python的执行效率,对cpython的兼容性也很高。如果项目纯python部分较多,推荐尝试使用pypy运行程序。
注:由于个人能力有限,文中示例如有谬误,还望海涵。
测试环境
如有疑问,欢迎在评论区一起讨论!
如有不正确的地方,欢迎指导!