免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
12下一页
最近访问板块 发新帖
查看: 6534 | 回复: 10
打印 上一主题 下一主题

[C++] 一个简单例子说明C语言在2013年仍很重要 [复制链接]

论坛徽章:
2
2015年辞旧岁徽章
日期:2015-03-03 16:54:152015年迎新春徽章
日期:2015-03-04 09:55:28
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2013-09-02 08:58 |只看该作者 |倒序浏览
伯乐在线导读:本文作者在开发Dynym项目,这是一个动态语言的通用运行时。在开发时,作者以其他语言的运行速度作为基础比较语言的运行速度,因此发现了一些小秘密。迭代计算斐波那契数列是测试各种语言执行速度的常见方法。作者以不同的语言进行测试,最终发现C语言要比Python编写的计算斐波那契数列快278.5倍。在底层开发,以及专注性能的应用程序中,选择是显而易见的。而为什么会有如此大的运行性能差距呢。作者进一步研究了程序的反汇编代码,发现差别出在内存的访问次数,以及预测的CPU指令的正确性方面。(感谢 乾龙_ICT 的热心翻译。如果其他朋友也有不错的原创或译文,可以尝试提交到伯乐在线。)以下是译文。

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

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

用Python语言实现如下:

1 2 3 4 5 6 7 8 9 10        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语言实现如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20        #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 时,对比结果如下:

1 2 3 4 5 6 7        ======================================================= 语言 时间 (秒) ======================================================= Java 0.133 C Language 0.006 CPython 0.534 Javascript V8 0.284
很明显,C语言是这里的老大,但是java的结果有点误导性,因为大部分的时间是由JIT编译器启动(~120ms)占用的。当n=100,000,000时,结果变得更明朗:

1 2 3 4 5 6 7        ======================================================= 语言 时间(秒) ======================================================= 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.5倍的性能差距!最不可思议的地方是,从语法角度讲,以上例子中的C语言和Python内部循环基本上一模一样。

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30        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 读者可以自己尝试变换优化选项对比反汇编结果)
最主要的部分是计算下一个斐波那契数值的内部循环:

1 2 3 4 5 6        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>
变量在寄存器中的分配情况如下:

1 2 3 4 5        a: %rsi b: %rdx t: %rdi i: %rcx n: %rax
262和263行实现了变量交换,264行增加循环计数值,虽然看起来比较奇怪,265行实现了b=a+t。然后做一个简单地比较,最后一个跳转指令跳到循环开始出继续执行。

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

1 2 3 4 5        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次。

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

1 2 3 4 5 6 7 8 9 10 11        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代码就非常糟糕。首先,所有纯粹的翻译器有一个“分配”循环,就像下面的例子:

1 2 3 4 5 6 7 8 9        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语言仍然主导着低级控制领域及对性能要求高的应用程序领域。

原文链接: Anthony J Bonkoski 翻译: 伯乐在线 - 伯乐在线读者

译文链接: http://blog.jobbole.com/47096/


本文来自ChinaUnix新闻频道,如果查看原文请点:http://news.chinaunix.net/opensource/2013/0901/2920367.shtml

论坛徽章:
208
巨蟹座
日期:2013-09-02 09:16:36卯兔
日期:2013-09-02 20:53:59酉鸡
日期:2013-09-05 21:21:45戌狗
日期:2013-10-15 20:51:17寅虎
日期:2013-10-18 21:13:16白羊座
日期:2013-10-23 21:15:19午马
日期:2013-10-25 21:22:48技术图书徽章
日期:2013-11-01 09:11:32双鱼座
日期:2013-11-01 20:29:44丑牛
日期:2013-11-01 20:40:00卯兔
日期:2013-11-11 09:21:32酉鸡
日期:2013-12-04 19:56:39
2 [报告]
发表于 2013-09-02 09:38 |只看该作者
Patagonia 发表于 2013-09-02 08:58
  伯乐在线导读:本文作者在开发Dynym项目,这是一个动态语言的通用运行时。在开发时,作者以其他语言的运 ...

以前一朋友开发密码攻击用脚本,花了一天都没跑完
结果换c,5分钟就出来

论坛徽章:
36
CU大牛徽章
日期:2013-09-18 15:24:20NBA常规赛纪念章
日期:2015-05-04 22:32:03牛市纪念徽章
日期:2015-07-24 12:48:5515-16赛季CBA联赛之辽宁
日期:2016-03-30 09:26:4715-16赛季CBA联赛之北控
日期:2016-03-30 11:26:2315-16赛季CBA联赛之广夏
日期:2016-05-20 15:46:5715-16赛季CBA联赛之吉林
日期:2016-05-24 11:38:0615-16赛季CBA联赛之青岛
日期:2016-05-30 13:41:3215-16赛季CBA联赛之同曦
日期:2016-06-23 16:41:052015年亚洲杯之巴林
日期:2015-02-03 15:05:04CU大牛徽章
日期:2013-09-18 15:24:52CU十二周年纪念徽章
日期:2013-10-24 15:46:53
3 [报告]
发表于 2013-09-02 10:06 |只看该作者
这排版有点坑哦,C语言真的这么神奇吗

论坛徽章:
89
水瓶座
日期:2014-04-01 08:53:31天蝎座
日期:2014-04-01 08:53:53天秤座
日期:2014-04-01 08:54:02射手座
日期:2014-04-01 08:54:15子鼠
日期:2014-04-01 08:55:35辰龙
日期:2014-04-01 08:56:36未羊
日期:2014-04-01 08:56:27戌狗
日期:2014-04-01 08:56:13亥猪
日期:2014-04-01 08:56:02亥猪
日期:2014-04-08 08:38:58程序设计版块每日发帖之星
日期:2016-01-05 06:20:00程序设计版块每日发帖之星
日期:2016-01-07 06:20:00
4 [报告]
发表于 2013-09-02 10:16 |只看该作者
本帖最后由 fender0107401 于 2013-09-02 10:16 编辑

就这还用证明。

论坛徽章:
0
5 [报告]
发表于 2013-09-02 10:29 |只看该作者
如果谁能写出不是以C语言为主的操作系统,没准C语言就不重要了
实际情况是,C和硬件交互的不错,当然,除了汇编能更好外。

论坛徽章:
8
CU大牛徽章
日期:2013-04-17 10:59:39CU大牛徽章
日期:2013-04-17 11:01:45CU大牛徽章
日期:2013-04-17 11:02:15CU大牛徽章
日期:2013-04-17 11:02:36CU大牛徽章
日期:2013-04-17 11:02:58技术图书徽章
日期:2013-12-04 10:48:50酉鸡
日期:2014-01-03 10:32:30辰龙
日期:2014-03-06 15:04:07
6 [报告]
发表于 2013-09-02 11:13 |只看该作者
这个还不算太夸张……数值处理,脚本语言做的够好了。稍微复杂点的字符串处理问题,它们会更惨。



之前曾用tcl(一种只有字符串类型的脚本语言)写过一个分析字符串模式的代码(用于区分被底层bug混淆的多个命令的返回文本),文本量不超过30K的样子。

我认为应分为多个细节级别,最简单的一级是根据序列(包括日期时间序列和id序列)和排版模式(分几个列、各个列的数据类型等),计算出行与行之间的相似度,然后根据相似度划分数据。

程序很快写出来了,算法很简单,只是字符串操作太笨拙,多花了很大功夫在数据管理上……

测试结果是:仅仅是最低的细节级别已经能解决问题;但这段程序跑完需要几十秒——这才仅仅不到30K的数据。


后来没事写加解密程序玩,这次用C,算法比之前那个tcl代码简单一些。内存加密的测试结果是:处理速率在偶的520m CPU上能跑到2.5G/秒。
按照两个程序的操作数估算,如果把之前那个tcl代码搬到c上,应该至少能达到100M/秒的处理速率。

论坛徽章:
4
水瓶座
日期:2013-09-06 12:27:30摩羯座
日期:2013-09-28 14:07:46处女座
日期:2013-10-24 14:25:01酉鸡
日期:2014-04-07 11:54:15
7 [报告]
发表于 2013-09-02 11:33 |只看该作者
脚本慢的要命, 用C替换通常性能是飞跃, 深有体会的事情了.

论坛徽章:
5
丑牛
日期:2014-01-21 08:26:26卯兔
日期:2014-03-11 06:37:43天秤座
日期:2014-03-25 08:52:52寅虎
日期:2014-04-19 11:39:48午马
日期:2014-08-06 03:56:58
8 [报告]
发表于 2013-09-02 14:05 |只看该作者
这个例子凸显了C语言在现代软件中的重要地位。

论坛徽章:
15
射手座
日期:2014-11-29 19:22:4915-16赛季CBA联赛之青岛
日期:2017-11-17 13:20:09黑曼巴
日期:2017-07-13 19:13:4715-16赛季CBA联赛之四川
日期:2017-02-07 21:08:572015年亚冠纪念徽章
日期:2015-11-06 12:31:58每日论坛发贴之星
日期:2015-08-04 06:20:00程序设计版块每日发帖之星
日期:2015-08-04 06:20:00程序设计版块每日发帖之星
日期:2015-07-12 22:20:002015亚冠之浦和红钻
日期:2015-07-08 10:10:132015亚冠之大阪钢巴
日期:2015-06-29 11:21:122015亚冠之广州恒大
日期:2015-05-22 21:55:412015年亚洲杯之伊朗
日期:2015-04-10 16:28:25
9 [报告]
发表于 2013-09-02 14:55 |只看该作者
linux_c_py_php 发表于 2013-09-02 11:33
脚本慢的要命, 用C替换通常性能是飞跃, 深有体会的事情了.

嗯。
有时,cpu使用趋于0,感觉快的不合理。
然后,想方设法提高系统利用率。

论坛徽章:
0
10 [报告]
发表于 2013-09-02 17:01 |只看该作者
支持C语言:wink:
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP