代码执行的效率
程序员文章站
2022-05-19 12:33:25
...
要进行性能调优,首先就要了解操作系统性能,找到程序中的Hotspot,也就是被调用最多的热点地方,只要能够好好优化一下这些地方,性能就会有质的提高。这里给大家用三个网上关于代码执行效率的例子来说明一下。
第一个例子
PHP中Getter和Setter的效率(来源reddit)
这个例子比较简单,可以简单了解一下。
考虑下面的PHP代码:我们可看到,使用Getter/Setter的方式,性能要比直接读写成员变量要差一倍以上。
这个并没有什么稀奇,因为有函数调用的开销,函数调用需要压栈出栈,需要传值,有时还要需要中断,要干的事太多了。所以,代码多了,效率自然就慢了。所有的语言都都是这样,这就是为什么C++要引入inline的原因。而且Java在打开优化的时候也可以优化之。但是对于动态语言来说,这个事就变得有点困难了。
你可能会以为使用下面的代码(Magic Function)会好一些,但实际其性能更差。
动态语言的效率从来都是一个问题,如果你需要PHP有更好的性能,你可能需要使用FaceBook的HipHop来把PHP编译成C语言。
第二个例子
为什么Python程序在函数内执行得更快?(来源*)
考虑下面的代码,一个在函数体内,一个是全局的代码。
函数内的代码执行效率为 1.8s
函数体外的代码执行效率为 4.5s
不用太纠结时间,只是一个示例,我们可以看到效率差的很多。为什么会这样呢?我们使用 dis module 反汇编函数体内的bytecode 代码,使用 compile builtin 反汇编全局bytecode,我们可以看到下面的反汇编:
Main函数反汇编
全局代码
我们可以看到,差别就是 STORE_FAST 和 STORE_NAME,前者比后者快很多。所以,在全局代码中,变量i成了一个全局变量,而函数中的i是放在本地变量表中,所以在全局变量表中查找变量就慢很多。如果你在main函数中声明global i 那么效率也就下来了。原因是,本地变量是存在一个数组中(直到),用一个整型常量去访问,而全局变量存在一个dictionary中,查询很慢。
(注:在C/C++中,这个不是一个问题)
第三个例子
为什么排好序的数据在遍历时会更快?(来源*)
参看如下C/C++的代码:
如果你的data数组是排好序的,那么性能是1.93s,如果没有排序,性能为11.54秒。差5倍多。无论是C/C++/Java,或是别的什么语言都基本上一样。
这个问题的原因是—— branch prediction (分支预判)伟大的*给了一个非常不错的解释。
考虑一个铁路分叉,当我们的列车来的时候, 扳道员知道每个分叉通往哪,但不知道这个列车要去哪儿,司机知道要去哪,但是不知道走哪条分叉。所以,我们需要让列车停下来,然后司机和扳道员沟通一下。这样的性能太差了。
所以,我们可以优化一下,那就是猜,我们至少有50%的概率猜对,如果猜对了,火车行驶性能巨高,猜错了,就得让火车退回来。如果我猜对的概率高,那么,我们的性能就会高,否则老是猜错了,性能就很差。
我们的if-else 就像这个铁路分叉一样,下面红箭头所指的就是搬道器。
那么,我们的搬道器是怎么预判的呢?就是使用过去的历史数据,如果历史数据有90%以上的走左边,那么就走左边。所以,我们排好序的数据就更容易猜得对。
排好序的
未排序的
从上面我们可以看到,排好序的数据更容易预测分支。
对此,那我们怎么办?我们需要在这种循环中除去if-else语句。比如:
我们把条件语句:
变成:
“没有分叉”的性能基本上和“排好序有分支”一个样,无论是C/C++,还是Java。
注:在GCC下,如果你使用 -O3 or -ftree-vectorize 编译参数,GCC会帮你优化分叉语句为无分叉语句。VC++2010没有这个功能。
最后,推荐大家一个网站——Google Speed,网站上的有一些教程告诉你如何写出更快的Web程序。
第一个例子
PHP中Getter和Setter的效率(来源reddit)
这个例子比较简单,可以简单了解一下。
考虑下面的PHP代码:我们可看到,使用Getter/Setter的方式,性能要比直接读写成员变量要差一倍以上。
<?php //dog_naive.php class dog { public $name = ""; public function setName($name) { $this->name = $name; } public function getName() { return $this->name; } } $rover = new dog(); //通过Getter/Setter方式 for ($x=0; $x<10; $x++) { $t = microtime(true); for ($i=0; $i<1000000; $i++) { $rover->setName("rover"); $n = $rover->getName(); } echo microtime(true) - $t; echo "\n"; } //直接存取变量方式 for ($x=0; $x<10; $x++) { $t = microtime(true); for($i=0; $i<1000000; $i++) { $rover->name = "rover"; $n = $rover->name; } echo microtime(true) - $t; echo "\n"; } ?>
这个并没有什么稀奇,因为有函数调用的开销,函数调用需要压栈出栈,需要传值,有时还要需要中断,要干的事太多了。所以,代码多了,效率自然就慢了。所有的语言都都是这样,这就是为什么C++要引入inline的原因。而且Java在打开优化的时候也可以优化之。但是对于动态语言来说,这个事就变得有点困难了。
你可能会以为使用下面的代码(Magic Function)会好一些,但实际其性能更差。
class dog { private $_name = ""; function __set($property,$value) { if($property == 'name') $this->_name = $value; } function __get($property) { if($property == 'name') return $this->_name; } }
动态语言的效率从来都是一个问题,如果你需要PHP有更好的性能,你可能需要使用FaceBook的HipHop来把PHP编译成C语言。
第二个例子
为什么Python程序在函数内执行得更快?(来源*)
考虑下面的代码,一个在函数体内,一个是全局的代码。
函数内的代码执行效率为 1.8s
def main(): for i in xrange(10**8): pass main()
函数体外的代码执行效率为 4.5s
for i in xrange(10**8): pass
不用太纠结时间,只是一个示例,我们可以看到效率差的很多。为什么会这样呢?我们使用 dis module 反汇编函数体内的bytecode 代码,使用 compile builtin 反汇编全局bytecode,我们可以看到下面的反汇编:
Main函数反汇编
13 FOR_ITER 6 (to 22) 16 STORE_FAST 1 (i) 19 JUMP_ABSOLUTE 13
全局代码
13 FOR_ITER 6 (to 22) 16 STORE_NAME 1 (i) 19 JUMP_ABSOLUTE 13
我们可以看到,差别就是 STORE_FAST 和 STORE_NAME,前者比后者快很多。所以,在全局代码中,变量i成了一个全局变量,而函数中的i是放在本地变量表中,所以在全局变量表中查找变量就慢很多。如果你在main函数中声明global i 那么效率也就下来了。原因是,本地变量是存在一个数组中(直到),用一个整型常量去访问,而全局变量存在一个dictionary中,查询很慢。
(注:在C/C++中,这个不是一个问题)
第三个例子
为什么排好序的数据在遍历时会更快?(来源*)
参看如下C/C++的代码:
for (unsigned i = 0; i < 100000; ++i) { // primary loop for (unsigned j = 0; j < arraySize; ++j) { if (data[j] >= 128) sum += data[j]; } }
如果你的data数组是排好序的,那么性能是1.93s,如果没有排序,性能为11.54秒。差5倍多。无论是C/C++/Java,或是别的什么语言都基本上一样。
这个问题的原因是—— branch prediction (分支预判)伟大的*给了一个非常不错的解释。
考虑一个铁路分叉,当我们的列车来的时候, 扳道员知道每个分叉通往哪,但不知道这个列车要去哪儿,司机知道要去哪,但是不知道走哪条分叉。所以,我们需要让列车停下来,然后司机和扳道员沟通一下。这样的性能太差了。
所以,我们可以优化一下,那就是猜,我们至少有50%的概率猜对,如果猜对了,火车行驶性能巨高,猜错了,就得让火车退回来。如果我猜对的概率高,那么,我们的性能就会高,否则老是猜错了,性能就很差。
我们的if-else 就像这个铁路分叉一样,下面红箭头所指的就是搬道器。
那么,我们的搬道器是怎么预判的呢?就是使用过去的历史数据,如果历史数据有90%以上的走左边,那么就走左边。所以,我们排好序的数据就更容易猜得对。
排好序的
T = 走分支(条件表达式为true) N = 不走分支(条件表达式为false) data[] = 0, 1, 2, 3, 4, ... 126, 127, 128, 129, 130, ... 250, 251, 252, ... branch = N N N N N ... N N T T T ... T T T ... = NNNNNNNNNNNN ... NNNNNNNTTTTTTTTT ... TTTTTTTTTT (easy to predict)
未排序的
data[] = 226, 185, 125, 158, 198, 144, 217, 79, 202, 118, 14, 150, 177, 182, 133, ... branch = T, T, N, T, T, T, T, N, T, N, N, T, T, T, N ... = TTNTTTTNTNNTTTN ... (completely random - hard to predict)
从上面我们可以看到,排好序的数据更容易预测分支。
对此,那我们怎么办?我们需要在这种循环中除去if-else语句。比如:
我们把条件语句:
if (data[j] >= 128) sum += data[j];
变成:
int t = (data[j] - 128) >> 31; sum += ~t & data[j];
“没有分叉”的性能基本上和“排好序有分支”一个样,无论是C/C++,还是Java。
注:在GCC下,如果你使用 -O3 or -ftree-vectorize 编译参数,GCC会帮你优化分叉语句为无分叉语句。VC++2010没有这个功能。
最后,推荐大家一个网站——Google Speed,网站上的有一些教程告诉你如何写出更快的Web程序。