美文网首页
51单片机编程进阶篇 #1简短的代码未必是高效的代码

51单片机编程进阶篇 #1简短的代码未必是高效的代码

作者: 穿山甲开源工作室 | 来源:发表于2018-01-11 22:26 被阅读518次

读大学时,我的C语言老师课间喜欢和我们吹牛,动不动就说老子当年写过几万行的代码。才读大学的我,被老师忽悠的一愣一愣,心想我什么时候也学有所成,写出几万行的代码,月薪过万不是梦啊!

然而事实是,工作接触了一些大神和工程师后才知道,很多人虽然真的可以写出成千上万行代码,但不意味着他们多有水平。那些动不动就吹嘘自己写过多少代码的人,往往是没什么事迹可吹。真正的大神,都以代码简短、高效、稳定、可读性强为荣。今天要说的是代码的简短和高效,很多人往往认为简短的代码就代表高效,殊不知,这和那些吹嘘自己写过多少万代码的人一样肤浅。下面我会通过单片机编程中常见的代码说明,简短的代码未必是高效的代码。

1 自增、自减操作

我们经常会看到这些写法:
i+ +
i- -
同样的,我们还知道这种写法:
i = i+1
i = i-1
那么要问,哪种写法更好呢?大部分人会说i+ +这种,为什么呢?他们往往会说,i+ +这种写起来简单,高效。i+ +的执行效率比i = i+1要高。
书写效率的优势是肯定的,敲3次键盘和敲5次键盘当然不一样,关于输入效率,其他文章里我会提到,这里暂且不谈,此处只讨论代码效率。
有人说,i+ +只操作一个变量,i = i+1执行了+1后,还需要赋值一次,当然效率就低。这个观点看起来没什么问题,却犯了形式主义错误。
大多数人的看法,都是通过语言本身分析。也就是说,i = i+1给人的直觉,是先执行了i+1,再赋值,是两个动作;而i++无论怎么看,都是一个动作,但这并不是事实。事实是,在Keil-C51开发环境下,他们都会被编译为同一条指令,如图1所示。

图1 i++和j=j+1的编译结果

红色框线是i+ +编译后的汇编指令,蓝色框线是j = j+1编译后的汇编指令。就算不懂汇编也可以看出,都是INC指令。0x08和0x09是变量i和j的物理地址,仅仅是地址不同。这意味着,不管你是写i+ +还是j = j+1,他们都被编译为同一条指令被CPU执行。

结论:i+ +i = i+1这两种写法在单独使用时,执行效率是完全一样的。出于个人习惯,写哪种都可以。

2、较少的行数效率就高

有一种看起来简单高效的写法,蛊惑了很多初学者多年,即:if(i++ == x)if(++i == x)

很多老师都会花很多篇幅讲他们的区别和用法。然而在Keil-C51编译环境中,纠结这两种用法是蠢的事情。本文为了更好的说明,我还是先解释一下if(i++ == x)if(++i == x)的区别。
△代码①

if(i++ == 2)
    {
        k=k+5;
        //用户代码
    }

代码①中,当i的值等于3时,执行括号里面的代码。

△代码②

if(++i == 2)
    {
        k=k+5;
        //用户代码
    }

代码②中,当i的值等于2时,执行括号里面的代码。

很多人知道这个结果,并以知道这个“技巧”为荣。这就苦了很多初学者,两种写法得到不同的结果,导致调试很久。如果初学者问有经验的人,他们大多是这么解释:

if(i++ == 2)是先执行i++,让在执行if条件语句判断,而if(++i == 2)是先引用i的值判断,再执行++i。有道理吗?有,因为编译器确实是这么编译的。有兴趣的读者可以自己debug试一试。所以呢,很多人写程序的之后,会巧用这个技巧,写出自以为高明的代码。实际上并不怎么高明,我来解释一下为什么。

我们写的代码,大多数是要拿给别人看的,除非你是初学者,写的代码到处是拼音、错拼的英文,不规范的缩进。但作为老师、硬件开发者,代码除了要达到目的,还要兼顾维护性。可维护性包含的内容很多,最直观的就是阅读性,所以先说一下阅读性。

对于初学者,最开始是先接触i++++ii = i+1这种基础内容。我们也很容易在老师那知道,i++++i都是自增操作。所以直觉告诉我们,前文提到的两段代码执行结果应该相同,然而测试后才发现不同。这里说直觉,是的,人就是无法避免直觉性的判断,虽然很多时候我们被直觉欺骗。就比如前文提到的i++i = i+1,很多是根据直觉判断谁的效率高,但没有任何事实依据。我不反对老师讲课的时候讲解if(i++ == x)if(++i == x)的区别,但我希望最后加上一句:不要纠结这些用法,忘记了网上搜索一下即可。因为你们可以把代码写为这种格式:

    unsigned char i,k;
    i = i + 1;      
    if(i == 2)
    {
        k = k + 5;
    }

这样不管写为i++++i 还是i = i + 1;都可以实现预期结果。这种写法,读者不需要考虑if(i++ == x)if(++i == x)的区别。我的代码里,都是这种写法,有朋友看了后对此嗤之以鼻。他说,说明明可以写为if(++i == 2)这种格式,一行不比两行效率高吗?一行不比两行看起来简洁吗?对于这种人,最开始做朋友没什么,时间久了了,类似“高明”的建议多了,我就敬而远之。

我们来看看所说的1行是不是真的比2行高效,先设计一段2行代码:
△代码③

    unsigned char i,k;
    ++i;                
    if(i == 2)
    {
        k = k + 5;
    }

Debug结果如图2所示

图2 代码3

++i和if(i == 2)编译后汇编语句如下:

INC     0x08
MOV     A,0x08
CJNE    A,#0x02,C:0010

读者可能说,我不懂汇编啊,没关系,姑且先记下这3行汇编,我们看一看下面的代码。
△代码④

    unsigned char i,k;
    if(++i == 2)
    {
        k = k + 5;
    }

Debug结果如图3所示

图3 代码4

红色框线内是if(++i == 2)编译后的汇编语:

INC    0x08
MOV    A,0x08
CJNE   A,#0x02,C:0010

这三行是不是似曾相识呢?没错,和代码③编译的结果完全相同,这也意味着,不管怎么写,编译后的汇编语句相同,执行效率自然也相同。所以不存在后者比前者高效的结论。

至于阅读效率,我只谈谈我的个人感受。当写为代码③的格式时,不管你是老师授课,还是研发者,当把代码给他人参考、修改,只要读者不是太离谱,都看得懂。但是写为代码④的格式后,至少初学单片机C编程的人,阅读起来会产生麻烦,不幸这个人还很健忘,忘记了if(++i == 2)if(i++ == 2)的区别,没准老师还没讲过区别,那么几乎都会出现理解问题。

相比Python这种优雅的语言,就取消了自增自减操作,这不是倒退,这是一种进步,至少,没有了自增自减,程序设计者不需要再纠结上述困扰。

3.简短的语句未必高效

我写程序的时候,switch case语句和if语句都经常用到。早期的开源设计代码中,几乎都是使用switch case,后来逐渐的使用ifif elseif。具体原因我会在其他章节专门介绍这两种语句的时候详细说明。本文着重说一下两个语句效率问题。

朋友、同事之间互看代码是常有的事情,也是一个朋友,看到我一段代码里是这样的:

unsigned char test1()
{
    unsigned char m=2,n;

    if(m == 0)
    {
         n = m * 10 + 1;    
    }
    if(m == 1)
    {
         n = m * 10 + 1;    
    }
    if(m == 2)
    {
         n = m * 10 + 1;    
    }
    return n;
}

当时我的整个程序是有问题的,找朋友是请教一下,给我看看代码可能哪里出错。结果看到上面这个函数后(这是精简后的示例,不是实际代码,但结构相同),就开始喷了。

朋友:你这种水平的代码,我看都懒得看,你先优化一下代码,我再给你检查问题。

我:哪里体现出水平有问题呢?

朋友:代码讲究的是简短、高效,你这么多if,看着都难受。你这里如果用switch case语句,看着多舒服,执行起来效率也更高。

我:••••••

后来再也没找他请教过问题,因为我们的水平完全不再一个档次,和这种人讨论编程简直就是侮辱自己的智商。

实际情况真的有如我朋友所言吗?switch case语句就真的比if这种高效吗?这次我们换个方式看一下效率,看看switch case语句是否在效率上比if语句更快。测试代码如下:

unsigned char test1()
{
    unsigned char m=2,n;

    if(m == 0)
    {
         n = m * 10 + 1;    
    }
    if(m == 1)
    {
         n = m * 10 + 2;    
    }
    if(m == 2)
    {
         n = m * 10 + 3;    
    }
    return n;
}

unsigned char test2()
{
    unsigned char m=2,n;
    switch (m)
    {
        case 0 : n = m * 10 + 1; break;
        case 1 : n = m * 10 + 2; break;
        case 2 : n = m * 10 + 3; break;
        default: break;
    }
    return n;
}

void main()
{
    unsigned char tmp;
    while(1)
    {
        tmp = test1();
        tmp = test2();  
    }           
}

两个测试函数(test1()test2())的功能是一样的,我们通过debug功能看一下效率问题。Debug调试结果如图4所示。

图4

红色箭头所指的“sec”为执行代码所经过的时间,在执行tmp = test1()之前,时间已经经过了0.00019450秒,记为t0。黄色箭头所指的即为即将执行的代码,按下“F10”键(或单击“Step Over”)后,如图5所示。

图5

执行tmp = test1()后,时间变为0.00020500秒,记为t1。

此时即将执行的代码为tmp = test2(),再次按“F10”键,时间变为0.00021550秒,记为t2。如图6所示。

图6

根据两次执行结果,可得到:
执行tmp = test1()的时间 = t1 - t0 = 0.00001050秒
执行tmp = test2()的时间 = t2 - t1 = 0.00001050秒

至此,我们可以说,把test1()里面的if语句改为switch case语句并没有提高效率,反倒是完全一样。

读者们,你们能根据前文的方法,发现为什么两种写法效率一样吗?有兴趣的读者可以把if语句改为if else语句与switch case语句再次对比,能否发现什么

总结:if语句改为switch case语句,确实让代码行数少了很多,尤其是在出现多个if的时候,但“看起来”简短的语句,也不能代表代码效率的高效。这里还要补充一点,虽然if语句行看起来没有switch case语句简短,但阅读效率未必就低下,同时,如果需要多重判断(往往上百次上千其次),switch case语句也会显得臃肿,那种情况下,会有更好的写法,我会在其他文章详细介绍。

** 上述测试环境为Keil4 C51 @12Mhz **

我的那些“朋友”,至今也坚信i++i=i+1高效,switch caseif高效。他们都犯了形式主义错误,很多单片机C语言老师或工程师,往往把其他编译环境的结论带到单片机领域,在他们眼中,并无不可,结果却误导了很多人。我在学习的过程中,也经常被各种人被所谓的经验误导。学习编程,就是要敢于质疑权威,这些权威可以是老师,你的师兄,也可以是我,只有这样,才能学到真正的知识。

作者水平有限,编写过程中难免出现不当之处,还望读者诸君不吝赐教,或许您有好的建议,欢迎与我联系QQ:136678431,作者将报以实质性奖励。

相关文章

网友评论

      本文标题:51单片机编程进阶篇 #1简短的代码未必是高效的代码

      本文链接:https://www.haomeiwen.com/subject/sbatoxtx.html