设为首页收藏本站

LUPA开源社区

 找回密码
 注册
文章 帖子 博客
LUPA开源社区 首页 IT综合资讯 查看内容

一个简单例子说明C语言在2013年仍很重要

2013-9-2 12:18| 发布者: joejoe0332| 查看: 2148| 评论: 0|原作者: 乾龙_ICT|来自: 伯乐在线

摘要:   本文作者在开发Dynym项目,这是一个动态语言的通用运行时。在开发时,作者以其他语言的运行速度作为基础比较语言的 运行速度,因此发现了一些小秘密。迭代计算斐波那契数列是测试各种语言执行速度的常见方法。作 ...

  为了找到问题的答案,我搬出了反汇编器,发现了以下现象:


0000000000400480 <main>:
247 400480: 48 83 ec 08 sub $0x8,%rsp
248 400484: 48 8b 7e 08 mov 0x8(%rsi),%rdi
249 400488: ba 0a 00 00 00 mov $0xa,%edx
250 40048d: 31 f6 xor %esi,%esi
251 40048f: e8 cc ff ff ff callq 400460 <strtol@plt>
252 400494: 48 98 cltq
253 400496: 31 d2 xor %edx,%edx
254 400498: 48 85 c0 test %rax,%rax
255 40049b: 74 26 je 4004c3 <main+0x43>
256 40049d: 31 c9 xor %ecx,%ecx
257 40049f: 31 f6 xor %esi,%esi
258 4004a1: bf 01 00 00 00 mov $0x1,%edi
259 4004a6: eb 0e jmp 4004b6 <main+0x36>
260 4004a8: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
261 4004af: 00
262 4004b0: 48 89 f7 mov %rsi,%rdi
263 4004b3: 48 89 d6 mov %rdx,%rsi
264 4004b6: 48 83 c1 01 add $0x1,%rcx
265 4004ba: 48 8d 14 3e lea (%rsi,%rdi,1),%rdx
266 4004be: 48 39 c8 cmp %rcx,%rax
267 4004c1: 77 ed ja 4004b0 <main+0x30>
268 4004c3: be ac 06 40 00 mov $0x4006ac,%esi
269 4004c8: bf 01 00 00 00 mov $0x1,%edi
270 4004cd: 31 c0 xor %eax,%eax
271 4004cf: e8 9c ff ff ff callq 400470 <__printf_chk@plt>
272 4004d4: 31 c0 xor %eax,%eax
273 4004d6: 48 83 c4 08 add $0x8,%rsp
274 4004da: c3 retq
275 4004db: 90 nop

(译注:

  • 1、不同的编译器版本及不同的优化选项(-Ox)会产生不同的汇编代码。
  • 2、反汇编方法:gcc -g -O2 test.c -o test;objdumb -d test>test.txt 读者可以自己尝试变换优化选项对比反汇编结果)


  最主要的部分是计算下一个斐波那契数值的内部循环:


262 4004b0: 48 89 f7 mov %rsi,%rdi
263 4004b3: 48 89 d6 mov %rdx,%rsi
264 4004b6: 48 83 c1 01 add $0x1,%rcx
265 4004ba: 48 8d 14 3e lea (%rsi,%rdi,1),%rdx
266 4004be: 48 39 c8 cmp %rcx,%rax
267 4004c1: 77 ed ja 4004b0 <main+0x30>

变量在寄存器中的分配情况如下:

a: %rsi
b: %rdx
t: %rdi
i: %rcx
n: %rax


  262和263行实现了变量交换,264行增加循环计数值,虽然看起来比较奇怪,265行实现了b=a+t。然后做一个简单地比较,最后一个跳转指令跳到循环开始出继续执行。


  手动反编译以上代码,代码看起来是这样的:


loop: t = a
a = b
i = i+1
b = a+t
if(n > i) goto loop


  整个内部循环仅用六条X86-64汇编指令就实现了(很可能内部微指令个数也差不多。译者注:Intel X86-64处理器会把指令进一步翻译成微指令,所以CPU执行的实际指令数要比汇编指令多)。CPython解释模块对每一条高层的指令字节码至少需要六条甚至更多的指令来解释,相比而言,C语言完胜。除此之外,还有一些其他更微妙的地方。


  拉低现代处理器执行速度的一个主要原因是对于主存的访问。这个方面的影响十分可怕,在微处理器设计时,无数个工程时 (engineering hours)都花费在找到有效地技术来“掩藏”访存延时。通用的策略包括:缓存、推测预取、load-store依赖性预测、乱序执行等等。这些方法确实 在使机器更快方面起了很大作用,但是不可能完全不产生访存操作。


  在上面的汇编代码中,从没访问过内存,实际上变量完全存储在CPU寄存器中。现在考虑CPython:所有东西都是堆上 的对象,而且所有方法都是动态的。动态特性太普遍了,以至于我们没有办法知道,a+b执行integer_add(a, b)、string_concat(a, b)、还是用户自己定义的函数。这也就意味着很多时间花在了在运行时找出到底调用了哪个函数。动态JIT运行时会尝试在运行时获取这个信息,并动态产生 x86代码,但是这并不总是非常直接的(我期待V8运行时会表现的更好,但奇怪的是它的速度只是Python的0.5倍)。因为CPython是一个纯粹 的翻译器,在每个循环迭代时,很多时间花在了解决动态特性上,这就需要很多不必要的访存操作。


  除了以上内存在搞鬼,还有其他因素。现代超标量乱序处理器核一次性可以取好几条指令到处理器中,并且“在最方便时”执行 这些指令,也就是说:鉴于结果仍然是正确的,指令执行顺序可以任意。这些处理器也可以在同一个时钟周期并行执行多条指令,只要这些指令是不相关的。 Intel Sandy Bridge CPU可以同时将168条指令重排序,并可以在一个周期中发射(即开始执行指令)至多6条指令,同时结束(即指令完成执行)至多4条指令!粗略地以上面斐 波那契举例,Intel这个处理器可以大约把28(译者注:28*6=168)个内部循环重排序,并且几乎可以在每一个时钟周期完成一个循环!这听起来很 霸气,但是像其他事一样,细节总是非常复杂的。


  我们假定8条指令是不相关的,这样处理器就可以取得足够的指令来利用指令重排序带来的好处。对于包含分支指令的指令流进 行重排序是非常复杂的,也就是对if-else和循环(译者注:if-else需要判断后跳转,所以必然包含分支指令)产生的汇编代码。典型的方法就是对 于分支指令进行预测。CPU会动态的利用以前分支执行结果来猜测将来要执行的分支指令的执行结果,并且取得那些它“认为”将要执行的指令。然而,这个推测 有可能是不正确的,如果确实不正确,CPU就会进入复位模式(译者注:这里的复位不是指处理器reset,而是CPU流水线的复位),即丢弃已经取得的指 令并且重新开始取指。这种复位操作有可能对性能产生很大影响。因此,对于分支指令的正确预测是另一个需要花费很多工程时的领域。


  现在,不是所有分支指令都是一样的,有些可以很完美的预测,但是另一些几乎不可能进行预测。前面例子中的循环中的分支指令——就像反汇编代码中267行——是最容易预测的其中一种,这个分支指令会连续向后跳转100,000,000次。


酷毙

雷人

鲜花

鸡蛋

漂亮
  • 快毕业了,没工作经验,
    找份工作好难啊?
    赶紧去人才芯片公司磨练吧!!

最新评论

关于LUPA|人才芯片工程|人才招聘|LUPA认证|LUPA教育|LUPA开源社区 ( 浙B2-20090187 浙公网安备 33010602006705号   

返回顶部