代码执行的效率

代码执行的效率

在《性能调优攻略》里,我说过,要调优性需要找到程序中的Hotspot,也就是被调用最多的地方,这种地方,只要你能优化一点点,你的性能就会有质的提高。在这里我给大家举三个关于代码执行效率的例子(它们都来自于网上)

第一个例子

PHP中Getter和Setter的效率来源reddit

这个例子比较简单,你可以跳过。

考虑下面的PHP代码:我们可看到,使用Getter/Setter的方式,性能要比直接读写成员变量要差一倍以上。

<?php
	//dog_naive.php

	class dog {
		public $name = "";
		public function setName($name) {
			$this-&gt;name = $name;
		}
		public function getName() {
			return $this-&gt;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程序在函数内执行得更快?来源StackOverflow

考虑下面的代码,一个在函数体内,一个是全局的代码。

函数内的代码执行效率为 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,我们可以看到下面的反汇编(注意我高亮的地方)

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++中,这个不是一个问题)

第三个例子

为什么排好序的数据在遍历时会更快?来源StackOverflow

参看如下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 (分支预判)伟大的stackoverflow给了一个非常不错的解释。

考虑我们一个铁路分叉,当我们的列车来的时候, 扳道员知道分个分叉通往哪,但不知道这个列车要去哪儿,司机知道要去哪,但是不知道走哪条分叉。所以,我们需要让列车停下来,然后司机和扳道员沟通一下。这样的性能太差了。

所以,我们可以优化一下,那就是猜,我们至少有50%的概率猜对,如果猜对了,火车行驶性能巨高,猜错了,就得让火车退回来。如果我猜对的概率高,那么,我们的性能就会高,否则老是猜错了,性能就很差。

Image by Mecanismo, from Wikimedia Commons:http://commons.wikimedia.org/wiki/File:Entroncamento_do_Transpraia.JPG

我们的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程序

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

好烂啊有点差凑合看看还不错很精彩 (18 人打了分,平均分: 3.72 )
Loading...

代码执行的效率》的相关评论

  1. @陈皓
    哦…把它和出栈入栈并列在一起, 略显得诡异…就好像每次都要做一样. 建议改成”函数调用需要压栈出栈,需要传值,有些甚至需要中断”.

  2. PHP中如果getter、setter确实比magic function要高效,至少少了一步判断,但两种方法都低效,有没有更高效的方法?

  3. iEverX :
    if (data[j] >= 128)
    sum += data[j];

    int t = (data[j] – 128) >> 31;
    sum += ~t & data[j];
    学习了,好方法

    这玩意儿有点危险吧,隐含了data[]是int类型,并且在32位的机器上跑,出了bug也很难调。。。

  4. python 访问全局变量慢,应该是对全局变量的访问,vm是做了查表操作。而在function内的局部变量,vm的访问是通过stack 偏移量直接定位。其实在动态语言中也能将全局变量直接解释成一段地址,在vm中直接通过这个地址来进行访问。但这样做的话,就不好做gc,与宿主语言之间的相互调用~~

  5. if (data[j] >= 128)
    sum += data[j];

    int t = (data[j] – 128) >> 31;
    sum += ~t & data[j];
    这段代码是去分支了,但是引入了移位,取反,和位与的操作,这个真的效率会变高么?有没有详细的测试过

  6. 分支预测那个,
    一般我们写代码的话,估计也就直接用if判断来做了,不太会用后一种方法吧?
    一般这个东西不太至于影响性能吧,如果真的去用第二种方法的话,算不算属于过分早的做优化这一说了?

  7. 这篇文章有误导嫌疑,一个软件中需要优化的是整体的架构、关键的算法,而不是这些不可控的细节。这些细节问题都是编译器做的,程序员如果这么做就是浪费时间。

    酷壳的文章都不错,怎么这篇这么差劲

  8. “为什么排好序的数据在遍历时会更快”
    对于这点我有点疑问,如果数据本身是无序的,那么加上排序的时间是否也会话的时间更长
    另外对于这点的例子,我觉得有点以偏概全了。如果遍历数据并不需要做所谓分支预判,这个会影响效率吗?

  9. haiyidao :
    “为什么排好序的数据在遍历时会更快”
    对于这点我有点疑问,如果数据本身是无序的,那么加上排序的时间是否也会话的时间更长
    另外对于这点的例子,我觉得有点以偏概全了。如果遍历数据并不需要做所谓分支预判,这个会影响效率吗?

    支持, 遍历数据并不总是要在遍历时进行条件判断的

  10. 虫子 :
    这段代码是去分支了,但是引入了移位,取反,和位与的操作,这个真的效率会变高么?有没有详细的测试过

    比乱序时用if高,比有序时用if低。原因你也说了

    @beyondwdq
    是否属于过早优化,从代码本身上是看不出的,只取决于软件编码的进程。或许你想说的是“过度优化”。
    如果说的是“过度优化”的话,我觉得也不算过度,因为速度差别确实很大,在需要的时候它就是必须的

    @Roland
    怎么可能是未定义呢?贴C99原文

    @lukmy
    在你给出的链接中,说的是N(第二操作数)为负或过大时,其行为是undefined的,并不是说x(第一操作数)

    @clonne
    你说错了,软件优化是“先主后次”,而非“只主不次”

    @haiyidao
    你说得是对的,但本文讨论的是 无序时的优化,没叫你先排序呀

  11. 上面好多人是不是没学过体系结构。。在指令中分支指令出现的频率相当之高而且一旦出现分支指令之前的流水线有很大的可能会清空,所以分支预测才会有很大的作用,而且现在的分支预测编译器优化有限所以很多采用处理器动态优化根本就和编译器没有关系,所以提高分支预测的准确性的却能相当大的提高性能。。

  12. tui56论坛-王宝臣来看看。呵呵。博客大全已经收录贵站博客……欢迎加入bbs.tui56.com SEO论坛!tui56论坛不求盈利……只求帮助更多朋友了解学习seo……希望通过这个圈子认识更多的朋友!已做好10年发展准备。www.tui56.com 续费到2022年!通过tui56论坛告诉自己……我们还活在互联网相关的领域中,坚持到底……欢迎你的到来!

  13. 编译器能做的,自己绝对不去干,把精力全部用在要解决的问题上。比如现在还有人考虑是些i++还是++i好一些。

  14. 第二个例子,我的理解,应试是"全局变量比局部变量"慢.刚看了一本讲处理器的书,其中提到局部变量使用的是"寄存器",全局变量使用的是"内存",所以访问,操作全局变量比局部变量要慢.这个问题,在C语言中应该也有的.

  15. if 分支这个优化比较难。如果把 if (data[j] > 128) sum += data[j] 改写为 sum += data[j] > 128 ? data[j] : 0 则几乎所有的编译器都可以做去分支优化。IA32 的指令集里有各种根据条件操作的指令,比如 cmovg 之类的,编译器做优化甚至连 bit twiddling 都省了。。。

  16. @lukmy
    另外还有一点就是现代CPU都是超标量的,很多指令可以并行执行。看起来多出一条指令,实际执行时间可能并没有任何增加。

  17. @lukmy
    你的意思是说分支优化其实不是编译器做的事情,而是处理器做的事情。所以在程序设计的时候考虑这个因素也可以提高代码执行的效率。
    我这样理解对吗?
    你说没学过体系结构,是计算机体系结构么,具体看那本书比较好?

  18. 分支那段没必要吧,小的分支会有CMOV来做,无所谓预测准确吧。复杂的判断,文章讲的优化方法根本就用不上。

  19. 虫子 :
    @lukmy
    你的意思是说分支优化其实不是编译器做的事情,而是处理器做的事情。所以在程序设计的时候考虑这个因素也可以提高代码执行的效率。
    我这样理解对吗?
    你说没学过体系结构,是计算机体系结构么,具体看那本书比较好?

    编译器做的分支优化是基于代码的指示的,就像linux内核里很常见的likely优化。在编码者了解分支场景的情况下,对性能还是很有作用的,编译器会将主分支的代码安排的更紧凑,避免指令cache miss。
    但这种优化对于循环是没有什么用处的,其实代码里最常见的分支跳转不是if else,而是循环。
    现代的处理器基本上都支持分支预测的功能,采用BTB表来跟踪跳转处的历史记录,然后预测下一次跳转的位置并从这里取代码执行。我之前测过我们的程序,INTEL sandybridge 的预测命中率在95%以上,还是能很好的提升性能的。

    这里喊着微架构级调优无效的人,都是搞应用的吧,眼界不要这么狭窄,性能要求苛刻的地方多了去了

  20. “你可能会以为使用下面的代码(Magic Function)会好一些,但实际其性能更差。”
    看到那段代码大家为什么会以为“会好一些”?那段代码更复杂,直观上性能会更差一些的。

  21. 第二个例子有点偷换概念了,如果误导别人在函数里面去访问全局变量,就不太好啦

  22. @lukmy
    你说得没错,这是处理器的问题,不是编译器的问题,更不是程序员的问题。作为一个程序员不应该过度关注底层实现,依赖处理器性能来编程反而引发程序移植性问题,所以说世上没有完美的程序设计,也许最好的做法仍旧是关注源代码层面,或许报告给你的硬件工程师,这才是你的职责。

回复 Rocky 取消回复

您的电子邮箱地址不会被公开。 必填项已用*标注