附注:在最初的文章里,我没说明进行模2^64的计算——我当然明白那些不是“正确的”斐波那契数列,其实我不是想分析大数,我只是想探寻编译器产生的代码和计算机体系结构而已。

最近,我一直在开发Dynvm——一个通用的动态语言运行时。就像其他任何好的语言运行时项目一样,开发是由基准测试程序驱动的。因此,我一直在用基准测试程序测试各种由不同语言编写的算法,以此对其典型的运行速度有一个感觉上的认识。一个经典的测试就是迭代计算斐波那契数列。为简单起见,我以2^64为模,用两种语言编写实现了该算法。

用python语言实现如下:

def fib(n):
    SZ = 2**64
    i = 0
    a, b = 1, 0
    while i < n:
        t = b
        b = (b+a) % SZ
        a = t
        i = i + 1
    return b

用C语言实现如下:

#include <stdio.h>
#include <stdlib.h>
typedef unsigned long ulong;
 
int main(int argc, char *argv[])
{
    ulong n = atoi(argv[1]);
    ulong a = 1;
    ulong b = 0;
    ulong t;
 
    for(ulong i = 0; i < n; i++) {
        t = b;
        b = a+b;
        a = t;
    }
 
    printf("%lu\n", b);
    return 0;
}

其他语言实现的代码示例在github上。

Dynvm包含一个基准测试程序框架,该框架可以允许在不同语言之间对比运行速度。在一台Intel i7-3840QM(调频到1.2 GHz)机器上,当n=1,000,000时对比结果如下:

=======================================================
  Language                           Time (in sec)
=======================================================
  Java                               0.133
  C Language                         0.006
  CPython                            0.534
  Javascript V8                      0.284

很明显,C语言是这里的老大,但是java的结果有点误导性,因为大部分的时间是由JIT编译器启动(~120ms)占用的。当n=100,000,000时,结果变得更明朗:

=======================================================
  Language                           Time (in sec)
=======================================================
  Java                               0.300
  C Language                         0.172
  CPython                            47.909
  Javascript V8                      24.179

在这里,我们探究下为什么C语言在2013年仍然很重要,以及为什么编程世界不会完全“跳槽”到Python或者V8/Node。有时你需要原始性能,但是动态语言仍然艰难地在这方面挣扎,即使对以上很简单的例子而言。我个人相信这种情况会被克服,通过几个项目我们能在这方面看到很大的希望:JVM,V8,PyPy,LuaJIT等等,但是在2013年我们还没有到达“目的地”。

然而,我们无法回避这样的问题:为什么差距如此之大?在C语言和Python之间有278.5x的性能差距!最不可思议的地方是,从语法角度讲,以上例子中的C语言和Python内部循环基本上一模一样。

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

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

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

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

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次。

以下是一个非常难预测的分支指令实例:

void main(void)

{

for(int i = 0; i < 1000000; i++) {

int n = random();

if(n >= 0) {

printf("positive!\n");

} else {

printf("negative!\n");

}

}

}

如果random()是真正随机的(事实上在C语言中远非如此),那么对于if-else的预测还不如随便猜来的准确。幸运的是,大部分的分支指令没有这么顽皮,但是也有很少一部分和上面例子中的循环分支指令一样变态。

回到我们的例子上:C代码实现的斐波那契数列,只产生一个非常容易预测的分支指令。相反地,CPython代码就非常糟糕。首先,所有纯粹的翻译器有一个“分配”循环,就像下面的例子:

void exec_instruction(instruction_t *inst)

{

switch(inst->opcode) {

case ADD:    // do add

case LOAD:   // do load

case STORE:  // do store

case GOTO:   // do goto

}

}

编译器无论如何处理以上代码,至少有一个间接跳转指令是必须的,而这种间接跳转指令是比较难预测的。

接下来回忆一下,动态语言必须在运行时确定如“ADD指令的意思是什么”这样基本的问题,这当然会产生——你猜对了——更加变态的分支指令。

以上所有因素加起来,最后导致一个278.5倍的性能差距!现在,这当然是一个很简单的例子,但是其他的只会比这更变态。这个简单例子足以凸显低级静态语言(例如C语言)在现代软件中的重要地位。我当然不是2013年里C语言最大的粉丝,但是C语言仍然主导着低级控制领域及对性能要求高的应用程序领域。

英文网页地址:http://jabsoft.io/2013/08/29/why-c-still-matters-in-2013-a-simple-example/

为什么C语言在2013年仍然很重要:一个简单的例子的更多相关文章

  1. Go语言之从0到1实现一个简单的Redis连接池

    Go语言之从0到1实现一个简单的Redis连接池 前言 最近学习了一些Go语言开发相关内容,但是苦于手头没有可以练手的项目,学的时候理解不清楚,学过容易忘. 结合之前组内分享时学到的Redis相关知识 ...

  2. C语言搬书学习第一记 —— 认识一个简单程序的细节

    #include<stdio.h> /*告诉编译器把stdio.h 中的内容包含在当前程序中,stdio.h是C编译器软件包的标准部分,它提供键盘输入和 屏幕输入的支持studio.h文件 ...

  3. 一个简单的例子让你很轻松地明白JavaScript中apply、call、bind三者的用法及区别

    JavaScript中apply.call.bind三者的用法及区别 引言 正文 一.apply.call.bind的共同用法 二. apply 三. call 四. bind 五.其他应用场景 六. ...

  4. 用一个简单的例子比较SVM,MARS以及BRUTO(R语言)

    背景重述 本文是ESL: 12.3 支持向量机和核中表12.2的重现过程.具体问题如下: 在两个类别中产生100个观测值.第一类有4个标准正态独立特征\(X_1,X_2,X_3,X_4\).第二类也有 ...

  5. 用 C 语言编写一个简单的垃圾回收器

    人们似乎觉得编写垃圾回收机制是非常难的,是一种仅仅有少数智者和Hans Boehm(et al)才干理解的高深魔法.我觉得编写垃圾回收最难的地方就是内存分配,这和阅读K&R所写的malloc例 ...

  6. Tinyhttpd - 超轻量型Http Server,使用C语言开发,全部代码只有502行(包括注释),附带一个简单的Client(Qt也有很多第三方HTTP类)

    - 2. Tinyhttpd tinyhttpd是一个超轻量型Http Server,使用C语言开发,全部代码只有502行(包括注释),附带一个简单的Client,可以通过阅读这段代码理解一个 Htt ...

  7. 利用OD破解一个简单的C语言程序

    最近在学习汇编(看的是王爽老师的<汇编语言(第三版)>),然后想尝试使用OD(Ollydbg)软件破解一个简单的C语言程序练练手. 环境: C语言编译环境:VC++6.0 系统:在Wind ...

  8. VS2017生成一个简单的DLL文件 和 LIB文件——C语言

    下面我们将用两种不同的姿势来用VS2017生成dll文件(动态库文件)和lib文件(静态库文件),这里以C语言为例,用最简单的例子,来让读者了解如何生成dll文件(动态库文件) 生成动态库文件 姿势一 ...

  9. R语言建立回归分析,并利用VIF查看共线性问题的例子

    R语言建立回归分析,并利用VIF查看共线性问题的例子 使用R对内置longley数据集进行回归分析,如果以GNP.deflator作为因变量y,问这个数据集是否存在多重共线性问题?应该选择哪些变量参与 ...

随机推荐

  1. java面试题系列11

    华为的JAVA面试题 QUESTION NO: 1 publicclass Test1 {       publicstaticvoid changeStr(String str){         ...

  2. LeetCode——Combinations

    Given two integers n and k, return all possible combinations of k numbers out of 1 ... n. For exampl ...

  3. c 有关N!阶乘的相关问题----陆续补充上来

    第一个:求N!结果中末尾0的个数问题.思路是末尾0的产生   5*偶数,阶乘中偶数的个数肯定比5多,所以求出阶乘中5的个数就可以求出末尾0的个数. #include<stdio.h> in ...

  4. asp.net中 服务器控件中onselectedindexchanged 没有反应的解决方案

    最近发现项目中一个BUG就是 DropDownList 中的onselectedindexchanged 没有反应 AutoPostBack="true"和页面中的<%@ P ...

  5. Stack集合、queue集合、hashtable集合

    1.栈:Stack,先进后出,一个一个赋值,一个一个取值,按顺序. .count           取集合内元素的个数 .push()         将元素一个一个推入集合中//stack集合存入 ...

  6. JSON Editor 中文文档

    JSON Editor JSON Editor 根据定义的JSON Schema 生成了一个Html 表单来对JSON进行编辑.它完整支持JSON Schema 的版本3和版本4,并且它集成了一些流行 ...

  7. 南阳师范学院ACM官方博客使用说明

    登录之后跳到如下页面: 点击博客进入如下页面: 这里每个人都有一个专栏,大家可以把自己写得博客放到自己的专栏下,同时也可以查看其他人写的博客,相互交流! 在发表博客的时候,选择个人分类中自己的专栏即可 ...

  8. jQuery扩展extend一

    把这个扩展写在这里,以后要是忘了可以回头查看. (function(j) {// 这里的j是一个形参,表示传入的jQuery对象,j可以任意填写 j.extend({// 相当于给jQuery对象加上 ...

  9. JQuery隔行变色

    <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> &l ...

  10. hdu 4180

    题意; 求接近规定 分数 的 最大分数用到 farey 数列的第二条性质 1 #include <iostream> #include<stdio.h> using names ...