写在最前面的

再庞大复杂的代码编译器都能接受,编译器会变得越来越聪明,让我们原本的代码更加高效。但是代码执行的多变与不可预测性,如果编译器大肆“优化”,偶尔或者大胆的说“在大多数情况下”,会得到“聪明反被聪明误”的后果,所以编译器非常小心谨慎,一遇到不可预测后果的优化,它就会立即折返,停止这一步的优化工作,因为它不知道程序员的本意是什么,“它怕得罪你”。
程序员要编写容易优化的代码,以帮助编译器扫清障碍。关于代码优化,笔者特别喜欢《深入理解计算机系统》一书中的第五章,有兴趣的可以阅读一下。

代码优化小剖

代码优化的方法总结了5种。

  • 将函数展开,即内敛函数,以优化函数的调用。 《从简单的算法初探过程汇编》,过多的函数调用会导致极大的调用开销。所以,简单而且时常调用的函数建议内敛,例如swap,max,min。改用宏或者直接展开函数更好。
  • 消除循环的低效率
    这一点我感触比较深。

    toupper(* str)
        for i=[0,strlen(str))
            if(str[i]>='a' && str[i]<='z')
                str[i] += ('A'-'a')

    strlen的源码大概如下:

    strlen(* str) 
        len = 0
        while(*s!='\0')
            s++,len++
        return len

    所以toupper中对i的每一次检测,都调用了strlen,由此可见toupper的浪费由来。以前写程序一直采用上面的toupper,以为够简洁,就像第三点中的sum伪代码。

  • 减少不必要的存储器引用
    sum(* src,n,* dest)    //    src向量相加结果放入dest
        for i=[0,n)
            *dest += src + i

    代码如此简洁而有力,以前一直为这种风格而自豪。但它的缺点也是显然的,循环内部过多的引用dest地址:每次循环都要把dest地址内的数据取出到寄存器接,寄存器相加得到结果后,又从寄存器写入到dest地址,所以dest读n次,写n次。这有何必呢?读写浪费很浪费,下面的代码会不会好点:

    sum(* src,n,* dest)    //    src向量相加结果放入dest
        temp = *dest;
        for i=[0,n)
            temp += src + i
        *dest = temp
  • 循环展开 
    引用《深入理解》P348 "首先,它减少了不直接有助于程序结果的操作的数量,例如循环索引计算和条件分支。其次,它提供了一些方法(重结合变换),可以进一步变化代码,减少整个计算中关键路径上的操作数量。"关键路径,书上对它的解释是执行一组机器指令所需时钟周期的一个下界。比如乘法和加法的周期,乘法就应该成为这组执行机器指令的关键路径。  
    什么是循环开销?比如:

    for i=(0,n]

    每一循环的开始都要对i做判断,以及对i自增,所以循环展开能降低这些开销。二路循环展开的伪代码:

    sum(* src,n,* dest)    //    src向量相加结果放入dest
        temp = *dest
        for i=[0,n-2+1),i+=2
            temp += src+i + src+i+1
        for i=[i,n),i+=1
            temp += src+i

    但是浮点的乘法没有得到效率的提高,是因为浮点乘法这个关键路径是循环展开的限制因素,即使循环展开,也需要执行n次的乘法。而有疑问,为什么整数乘法就能得到提高呢,那是因为编译器作了“重关联变换”的优化,改变了乘法的结合顺序(有兴趣看看下面的第6点,我带过了)。又有疑问了,为什么浮点不能作想整数乘法这样的优化呢?因为浮点乘法加法是不可结合的,记住“编译器怕得罪你”。 
    珠玑中第九章的顺序搜索就是用了这种优化。

  • 多个累积变量
    这是提高并行操作的方法,同时达到了循环展开的效果。

    sum(* src,n,* dest)    //    src向量相加结果放入dest
        temp1 = *dest
        temp2 = 0
        for i=[0,n-2+1),i+=2
            temp1 += src+i    //    提高并行性,temp1和temp2可以并行计算而毫不牵连
            temp2 += src+i+1
        for i=[i,n),i+=1
            temp += src+i
        *dest = temp1+temp2
    //temp1和temp2的加法操作是两条关键路径,而两条关键路径各执行了n/2个操作。 

    同样,乘法也能得到效率的提高。注意循环寄存器有两个,数据相关降低,两个循环寄存器的运算是并行的。

关于第六种优化方法——重新结合变换

书上还有说第六种优化——重新结合变换,如果大胆对浮点运算进行此类优化,性能有很大的提高。之所以没有标号,是因为笔者也不太能说清楚,说说笔者的理解。

sum(* src,n,* dest)    //    src向量相加结果放入dest
    temp = *dest
    for i=[0,n-2+1),i+=2
        temp = temp * (src+i * src+i+1)    //    假设原来是temp = (temp * src+i) * src+i+1
    for i=[i,n),i+=1
        temp += src+i

我的理解可以结合下图,下图是乘法的图,同样可以换成加法的图:

注意是temp = temp * (src+i * src+i+1)的图,而不是temp = (temp * src+i) * src+i+1的图,后者大家可以自己手动画画。

哈哈,书上说:未经训练的人员,上面的两条语句是一样的,“捣乱笑而不语啊!”我的理解是虽然循环寄存器只有一个,但是temp = temp * (src+i * src+i+1)中(src+i * src+i+1)的计算不依赖于循环寄存器的值即temp的值,而temp = (temp * src+i) * src+i+1会对temp产生依赖,结合顺序会对循环寄存器进行产生依赖,因此前者可以增加计算的并行性。

神马?什么是循环寄存器?对于某些循环来说,有些寄存器既作为源值又作为目的,一次循环的结果会在下一次循环中用到。循环寄存器之间的关联越大,那么,这种关联将是性能提升的瓶颈。

简单举个例子:

sum(* src,n,* dest)    //    src向量相加结果放入dest
    temp = *dest;
    for i=[0,n)
        temp += src + i
    *dest = temp

那么temp所在寄存器就是所谓的“循环寄存器”,它使得每一次循环都有很大的关联,所以,temp的加法操作(抑或是乘法操作)是关键路径,这也就是为什么累积变量能够提高程序的性能,它有两个循环寄存器,降低了循环关联

《珠玑》第九章的代码优化,印象比较深的:

  1. 整数取模
  2. 函数内敛
  3. 循环展开

跟上面的内容差不多。

关于哨兵

哨兵就是能帮助程序检测数组边界的东西,简化了数组边界的检测,从而使代码更加清晰简便。记得一开始接触哨兵是在顺序表查找的时候。

search(* arr,n,data)
    arr[n] = data
    for i=[0,n)    arr[i]!=data    //肯定会遇到data
        do nothing in for
    if(i==n)    return -1
    return i;

再来就是直接插入排序;如果不设置哨兵,不仅要检测下标是否下溢,而且要检测只有当满足arr[j]>data,才后移,这里会有两个判断。

insert_sort
    for i=[0,n)
        if arr[i]>arr[i+1]
            arr[0] = arr[i+1]                    //哨兵
            for j=[i+1)    arr[j]>arr[0]        //相比常规版本,只做n次判断。
                arr[j] = a[j-1],j--
            arr[j+1] = arr[0]

另外,用单链表存储一组顺序数据,对于这种问题,一般的单链表插入,都要考虑头插法和尾插法的情况,其他情况的代码可以是一致的;如果能够为单链表的最后添加哨兵,应该可以很大程度上简化代码。下面的代码比一般的单链表插入简洁而且应该更高效:

pre_insert:把maxval放置链表的最后
insert(* first,data)
    p = first->next
    while(p)
        if data < q->data
            end while
        p = p->next;
    p = new node(data,p)

《珠玑》第9章第8小题,“如何在程序中使用哨兵来找出数组中的最大元素”?
如果没有“哨兵”,大致的想法就是用一个变量max来存储数组的第一个值,然后从第二个开始“逐个”检验是否>max。加上循环中的下标检验,共有2n次的检测。
放置哨兵的做法:在数组的最后放置“已经找到的最大的元素”,然后逐个检验,

find_max(* arr)
    i = 0
    max = arr[i]
    while(i!=n)
        max = arr[i]
        arr[n] = max
        i++
        while(arr[i]<max)    //因为有哨兵的存在,此循环必定可以终结。
            i++

除非arr是严格递增或者每个元素值相等,否则总的检测次数会<2n。总之在考虑边界检测的时候,不妨考虑下用在边界上放置哨兵来简化清晰代码。

总结

不知道这些优化,在以后的做题能不能奏效了。对于一些程序,笔者认为上面的优化是无关痛痒的,或许编程的技巧会更突显重要性,当完成了大部分程序,而后考虑上面的优化也不为过;但养成“优化”的习惯不失为一个优秀程序员的品质。(以上是笔者的学习笔记,需要更深入了解上面的内容,阅读原著或许收益更多。)关于珠玑的第十章,笔者实在无能为力 - =,只写了第九章。

本文完 Sunday, April 15, 2012

捣乱小子 http://daoluanxiaozi.cnblogs.com/

转载于:https://www.cnblogs.com/daoluanxiaozi/archive/2012/04/15/2450134.html

《编程珠玑,字字珠玑》910读书笔记——代码优化相关推荐

  1. 《编程匠艺》读书笔记

    <编程匠艺>读书笔记之一 <编程匠艺>读书笔记之二 <编程匠艺>读书笔记之三 <编程匠艺>读书笔记之四 <编程匠艺>读书笔记之五 <编 ...

  2. 《编程之美》读书笔记19: 3.9 重建二叉树

    <编程之美>读书笔记19: 3.9 重建二叉树 对根节点a以及先序遍历次序P和中序遍历次序I,查找a在I中的位置,将I分为两部分,左边部分的元素都在a的左子树上,右边的元素都在a的右子树上 ...

  3. 《编程之美》读书笔记08:2.9 Fibonacci序列

    <编程之美>读书笔记08:2.9 Fibonacci序列 计算Fibonacci序列最直接的方法就是利用递推公式 F(n+2)=F(n+1)+F(n).而用通项公式来求解是错误的,用浮点数 ...

  4. 《Android编程权威指南》-读书笔记(七) -处理旋转设备

    <Android编程权威指南>-读书笔记(七) -处理旋转设备 旋转设备会改变设备配置(device configuration).设备配置是用来描述设备当前状态的一系列特征.这些特征包括 ...

  5. 《编程之美》读书笔记(三):烙饼问题与搜索树

    <编程之美>读书笔记三:烙饼问题与搜索树 薛笛 EMail:jxuedi#gmail.com 前面已经写了一些关于烙饼问题的简单分析,但因为那天太累有些意犹未尽,今天再充实一些内容那这个问 ...

  6. Spring Boot 核心编程思想-第二部分-读书笔记

    怕什么真理无穷 进一步有近一步的欢喜 说明 本文是Spring Boot核心编程思想记录的笔记,书籍地址:Spring Boot编程思想(核心篇): 这篇文档会记录这本我的一些读书的思考,内容可能比较 ...

  7. 《编程之美》读书笔记(四): 卖书折扣问题的贪心解法

    <编程之美>读书笔记(四):卖书折扣问题的贪心解法 每次看完<编程之美>中的问题,想要亲自演算一下或深入思考的时候,都觉得时间过得很快,动辄一两个小时,如果再把代码敲一遍的话, ...

  8. 【Java并发编程的艺术】读书笔记——Java并发编程基础

    学习参考资料:<Java并发编程的艺术> 文章目录 1.线程的几种状态 2.如何安全的终止线程 3.线程间通信(重要) 3.1共享内存 3.2消息传递 1.线程的几种状态 线程在运行的生命 ...

  9. 《Python黑帽子:黑客与渗透测试编程之道》读书笔记(三):scapy——网络的掌控者

    目录 前言 1.窃取email认证 2.ARP缓存投毒 3.PCAP文件处理 结语 前言 <Python黑帽子:黑客与渗透测试编程之道>的读书笔记,会包括书中源码,并自己将其中一些改写成P ...

最新文章

  1. AngularJS如何在filter中相互调用filter
  2. OpenCV 3最新模块介绍
  3. maven 多模块项目关系
  4. Zeppelin-源码编译
  5. jhope代码分析以及网站结构
  6. sam格式的结构和意义_各种格式的练字本,对写字真有帮助吗
  7. 管理者必看!深度剖析BI与数据仓库,企业能否成功转型就看它
  8. 从一个表查询数据插入另一个表
  9. python 获取当前月份月初日期和月末日期
  10. 同济大学转计算机专业绩点,关于同济大学研究生成绩计算方法的说明
  11. linux下mysql5.7以上my.cnf配置文件配置
  12. c++ 函数指针和指针函数
  13. Flutter 编译失败shared_preferences_macos
  14. win10彻底关闭自动更新
  15. Flink_Flink ON YARN containerized.heap-cutoff-min 内存调整
  16. 新型传感器将改变大脑控制的机器人技术
  17. LINQ的Order By (Descending) 操作
  18. 关于深度学习(deep learning)
  19. IBM Lotus Connections 2.5 评审指南
  20. 从屏下指纹到体感手机,vivo能否走出自己的创新之路?

热门文章

  1. 学以致用 知行合一 ——《产品管理与研发项目管理》课程有感
  2. C++随记总结(1)----关于C++中的大小端、位段(惑位域)和内存对齐
  3. 笔记下UltraEdit的一些常用使用技巧
  4. 本地socket通讯 -转
  5. python3简明教程-实验楼_#python实验楼教程#学Python哪里有一问一答的Python学习?求具体的~...
  6. 【去重和排序】同一个list下,Map或对象内相同属性值取另一个属性值的最小值或最大值
  7. linux 下运行 jar包 java.lang.ClassNotFoundException: 解决办法
  8. Thumbelina,摘自iOS应用Snow White and more stories
  9. 使用 Swiftype 给 Hexo 搭建的博客添加站内搜索功能
  10. 传感器 Sensor 加速度【示例】