惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
GbyAI
GbyAI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 三生石上(FineUI控件)
美团技术团队
Last Week in AI
Last Week in AI
WordPress大学
WordPress大学
L
LangChain Blog
雷峰网
雷峰网
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园 - 叶小钗
Engineering at Meta
Engineering at Meta
腾讯CDC
Recent Announcements
Recent Announcements
The Register - Security
The Register - Security
有赞技术团队
有赞技术团队
Blog — PlanetScale
Blog — PlanetScale
博客园 - Franky
博客园 - 司徒正美
The Cloudflare Blog
Google DeepMind News
Google DeepMind News
T
Tailwind CSS Blog
C
Check Point Blog
小众软件
小众软件
V
Visual Studio Blog
V
V2EX
F
Full Disclosure
J
Java Code Geeks
MongoDB | Blog
MongoDB | Blog
罗磊的独立博客
人人都是产品经理
人人都是产品经理
量子位
Apple Machine Learning Research
Apple Machine Learning Research
F
Fortinet All Blogs
Microsoft Security Blog
Microsoft Security Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 【当耐特】
博客园_首页
Y
Y Combinator Blog
N
Netflix TechBlog - Medium
酷 壳 – CoolShell
酷 壳 – CoolShell
Stack Overflow Blog
Stack Overflow Blog
Recorded Future
Recorded Future
G
Google Developers Blog
Vercel News
Vercel News
大猫的无限游戏
大猫的无限游戏
Microsoft Azure Blog
Microsoft Azure Blog
U
Unit 42
爱范儿
爱范儿
Jina AI
Jina AI

博客园 - eaglet

HubbleDotNet 的注册码生成器 文件名通配符匹配的代码 .net 下如何将文档文件(Word, Pdf等) 中的文本提取出来 NTCPMSG 开源高性能TCP消息发送组件 文件在不同文件系统间拷贝文件时间改变的问题 文件是否真的写入了磁盘? 如何自动延长 windows 2008 试用版的试用期 获取ALL USER 的特殊目录的类 多线程环境下调用 HttpWebRequest 并发连接限制 HubbleDotNet+Mongodb 构建高性能搜索引擎--概述 C# 重启计算机的问题 HubbleDotNet 索引分词的测试方法和分词技巧 Windows 启动顺序详解 记录windows操作系统启动日志 C# 程序自动批量生成 google maps 的KML文件 通过 Windows 7 共享上网 正确理解 SqlConnection 的连接池机制 Windows 2008 server + IIS 7 设置身份模拟(ASP.NET impersonation) IIS 7.5 配置 FTP Passive 模式
i—比 i++ 快?
eaglet · 2012-05-21 · via 博客园 - eaglet

今天在微博上看到有人说 i—比 i++ 快,我用C写了个程序测试了一下,还真的是快,难道减法运算比加法快?从原理上分析感觉不可能啊,于是深入研究了一下,终于找到原因。

先看一下测试代码:

#include <stdio.h>
#include <time.h>

int main()
{
    int count = 1000000000;

    clock_t cl = clock ();

    for(int i = count; i > 0 ; i--)
    {
    }

    printf("Elapse %u ms\r\n", (clock () - cl));

    cl = clock ();

    for(int i = 0; i < count ; i++)
    {
    }

    printf("Elapse %u ms\r\n", (clock () - cl));

    return 0;
}

以上代码在VC 2008 下编译,编译时取消优化选项(如果不取消优化的话,上面两个循环语句由于什么都没干,会被编译器优化掉)。

运行后的结果是
Elapse 2267 ms
Elapse 2569 ms

也就是说减法循环比加法循环10亿次时快300毫秒,超过10%。

从C语言层面上分析,这两个代码几乎是一样的,我一开始也是楞了1分多钟,后来仔细比较两个代码,感觉它们的差别主要在两个地方,一个是加法和减法的差别,一个是for循环的第二个语句中一个是和立即数比较一个是和变量比较。以我掌握的计算机硬件原理知识,我首先排除了第一个差别造成性能影响的可能,那么问题很可能就出在第二个差别上,因为我知道在汇编语言中两个内存变量是不能直接比较的,中间必须要通过寄存器转储一次。这样就会多出至少一个指令。问题可能就在这里。为了验证我的判断,我们来看一下上面代码的汇编语句到底是什么样子的:

   1:  int main()
   2:  {
   3:  00CC1000  push        ebp  
   4:  00CC1001  mov         ebp,esp 
   5:  00CC1003  sub         esp,10h 
   6:   
   7:      int count = 1000000000;
   8:  00CC1006  mov         dword ptr [count],3B9ACA00h 
   9:   
  10:   
  11:      clock_t cl = clock ();
  12:  00CC100D  call        dword ptr [__imp__clock (0CC209Ch)] 
  13:  00CC1013  mov         dword ptr [cl],eax 
  14:   
  15:      for(int i = count; i > 0 ; i--)
  16:  00CC1016  mov         eax,dword ptr [count] 
  17:  00CC1019  mov         dword ptr [i],eax 
  18:  00CC101C  jmp         main+27h (0CC1027h) 
  19:  00CC101E  mov         ecx,dword ptr [i] //把i的内存值拷贝到寄存器ecx中
  20:  00CC1021  sub         ecx,1 //ecx 减1
  21:  00CC1024  mov         dword ptr [i],ecx //把ecx 的值拷贝到i对应的内存地址,这里完成i--操作 
  22:  00CC1027  cmp         dword ptr [i],0 //i对应的内存值和0进行比较 
  23:  00CC102B  jle         main+2Fh (0CC102Fh) //如果小于等于0,跳转到98行
  24:      {
  25:      }
  26:  00CC102D  jmp         main+1Eh (0CC101Eh)//如果大于0,跳转到19行,继续循环
  27:   
  28:      printf("Elapse %u ms", (clock () - cl));
  29:  00CC102F  call        dword ptr [__imp__clock (0CC209Ch)] 
  30:  00CC1035  sub         eax,dword ptr [cl] 
  31:  00CC1038  push        eax  
  32:  00CC1039  push        offset ___xi_z+30h (0CC20F4h) 
  33:  00CC103E  call        dword ptr [__imp__printf (0CC20A4h)] 
  34:  00CC1044  add         esp,8 
  35:   
  36:      cl = clock ();
  37:  00CC1047  call        dword ptr [__imp__clock (0CC209Ch)] 
  38:  00CC104D  mov         dword ptr [cl],eax 
  39:   
  40:      for(int i = 0; i < count ; i++)
  41:  00CC1050  mov         dword ptr [i],0 
  42:  00CC1057  jmp         main+62h (0CC1062h) 
  43:  00CC1059  mov         edx,dword ptr [i]//把i的内存值拷贝到寄存器edx中 
  44:  00CC105C  add         edx,1 //edx 加 1 
  45:  00CC105F  mov         dword ptr [i],edx //将edx的值拷贝到i变量对应地址 
  46:  00CC1062  mov         eax,dword ptr [i] //将i变量值拷贝到寄存器eax中 
  47:  00CC1065  cmp         eax,dword ptr [count] //用eax 和 count地址上的值进行比较
  48:  00CC1068  jge         main+6Ch (0CC106Ch)//如果大于等于count,跳出循环 
  49:      {
  50:      }
  51:  00CC106A  jmp         main+59h (0CC1059h)//否则跳转到43行继续循环

我把汇编语句中的循环部分用红色标记出来,并加上注释。我们可以清楚的看到第二个循环中的汇编指令为7个,第一个为6个,也就是说第一个要比第二个要快 1/7 左右,这个和实际测试出来的结果基本上是吻合的。

那么我们再看看为什么编译器要多一个机器指令。原因是汇编语句不可能对两个内存值直接比较,内存值只能和寄存器进行比较,这个应该是计算机硬件结构决定的,这个问题就导致编译器必须要加一个指令来转储内存值到寄存器中。

再进一步,我们发现编译器似乎很蠢,如果在循环之前把 dword ptr[count] 拷贝到一个寄存器中,比如 ecx ,然后在46 行直接 cmp ecx, dword ptr [i] ,就不需要第47行这个指令了。但事实上编译器可能并没有蠢到这个地步,本文前面说过,我将编译器的优化给禁用了,因为如果优化的话,上面两个for循环将被完全忽略掉,根本不会执行,测试出来的时间为0秒。那么既然我们告诉编译器不优化,编译器也就不会优化这个指令,如果真的按照上面方法优化了,那么在调试环境下,如果我们想在循环中更改 count 的值就比较困难了,需要调试器来做一些编译器要做的事情。

再深入一点,我们还会发现这个汇编语句中还有一个地方可以优化,就是

  21:  00CC1024  mov         dword ptr [i],ecx //把ecx 的值拷贝到i对应的内存地址,这里完成i--操作 
  22:  00CC1027  cmp         dword ptr [i],0 //i对应的内存值和0进行比较 

第22行这个地方完全可以优化为 cmp ecx, 0

我们知道对寄存器的读写是最快的,其次是一级缓存,二级缓存,三级缓存,然后才是内存,最后是磁盘。

如果22行优化为 cmp ecx, 0 其运行速度肯定要比 cmp dword ptr[i], 0 要快,因为后面的语句要进行一次寻址,从缓存中读取数据(如果CPU有缓存的话),如果没缓存,就是从内存读一次,那就更慢了。

最后我们把i++那个循环改成

for(int i = 0; i < 1000000000 ; i++) 再测一次,结果为

Elapse 2334 ms
Elapse 2290 ms

可以看出两个循环的用时基本上相等了

    for(int i = 0; i < 1000000000 ; i++)
01201050  mov         dword ptr [i],0 
01201057  jmp         main+62h (1201062h) 
01201059  mov         edx,dword ptr [i] 
0120105C  add         edx,1 
0120105F  mov         dword ptr [i],edx 
01201062  cmp         dword ptr [i],3B9ACA00h 
01201069  jge         main+6Dh (120106Dh) 
    {
    }
0120106B  jmp         main+59h (1201059h) 

看一下汇编语句,for 循环的第二句改成立即数比较后,汇编语句变成了6个指令了。所以用时也基本相同了。

结论:

i++ 和 i-- 性能是没有区别的,之所以我们感觉i--快,是因为在汇编层面上,i++ 那个循环中多了一个机器指令造成的。另外通过本文,我们也了解了一些关于汇编的指令优化的知识,希望对大家能有帮助。

微博: http://weibo.com/hubbledotnet