zoukankan      html  css  js  c++  java
  • 编码优化

    同深层次的设计问题相比,性能方面的编码问题更容易解决,这些问题的规模通常较小,在其解决方法中,所包含的代码量都很小。


    1 缓存

    缓存主要用来存储使用频繁而且代价高昂的计算结果,这样就可以避免对这些结果的重复计算。

    for(...; !done; ...)
    {
        done = patternMatch( pat1, pat2, isCaseSensitive() );
    }

    由于isCaseSensitive()的返回值独立于循环体,且不随迭代而改变,因此应该将将放在循环之外:

    int isSensitive = isCaseSensitive();
    for(...; !done; ... )
    { 
        done = patternMatch( pat1, pat2, isSensitive );
    }


    2 预先计算

    将预先计算放置在影响性能的关键路径之外(例如初始化阶段),就可以避免在性能关键路径上进行代价高昂的计算(连缓存的那一次计算此时也不需要了)。

    比如要将大小写字母混合的字符串转换成大写,即使将转换的函数写成内联,仍将包含如下的条件语句:

    return ( c >= 'a' && c <= 'z' ) ? c - ( 'a' - 'A' ) : c;

    如果在影响性能的关键路径上使用这个函数,代价可能是无法接受的。我们可以预先计算出每个可能出现的字符相应的大写:

    void initLookupTable()
    {
        for( int i = 0; i < 256; i++ )
        {
            uppercaseTable[i] = toUpper(i);
        }
    }

    然后在使用时,只需要使用 *p = uppercaseTable[*p]就完成转换了,这将显著的提高字符串操作的效率。


    3 降低灵活性

    例如,当客户端发起请求时WEB服务器需要知道其IP地址。对于每一个请求,需要程序分配足够多的空间来容纳一个客户端的的IP地址,请求结果时再释放该空间。调用函数new()和delete()来处理存储空间的代价很高,尤其是每次请求均需要调用这两个函数的情况下将更加糟糕。即使使用内存池,也无法完全消除对new和delete的调用。

    如果IP地址的长度没有限制,那么除了动态分配别无选择,但是,我们知道IP地址的长度不会15字节:xxx.xxx.xxx.xxx。如果再增加一个终结符NULL,长度变为16字节。即使IPV6的长度有所增加,但总归是有限的。既然IP地址的长度有限,那么以局部变量的形式将IP地址存储在堆栈上将更加高效。

    char clientIPAddr[256];


    4 80-20法则

    80-20法则适用于很多场合:80%的程序执行只遍历20%代码;80%的程序运行时间耗费在执行路径的所遇到的20%函数上。这个法则有力证明了过于草率的判断是一个错误。

    考虑表达式:

    if ( e1 || e2 ) { ... }

    e1在90%情况下为真,e2在50%情况下为真; e1执行时间为10个指令,e2执行时间为100个指令。那么期望代价为:10 * 100% + 100 * 10% = 20

    如果交换e1和e2的顺序,代价为 100 * 100% + 10 * 50% = 105

    性能差别显著。


    5 延迟计算

    不应该执行“只在某种情况下”才需要的昂贵计算,而应该只在必要的时候执行昂贵计算。这通常意味着把计算推迟到真正需要的时候才进行,因此称之为延迟计算。

    int route( Message *msg )
    {
        ExpensiveClass upstream( msg );
        
        if ( goingUpstream )
        {
            ... //处理对象upstream
        }
    
        //此处不处理对象upstream
    
        return SUCCESS;
    }

    对象upstream的构造和析构函数代价很昂贵,而程序中只有一半的时间goingUpstream才是true,这意味着另一半时间并不需要使用upstream,此时对象的计算完全是浪费的。在真正需要upstream对象的地方定义它:

    int route( Message *msg )
    {
        
        if ( goingUpstream )
        {
            ExpensiveClass upstream( msg );
    
            ... //处理对象upstream
        }
    
        //此处不处理对象upstream
    
        return SUCCESS;
    }



    6 无用计算

    顾名思义,无用计算就是那些根本无须执行的计算,无论执行流程如何,这些计算结果从不使用,因此它们是完全没有意义的。

    一个关于无用计算的精妙例子是成员对象的无用初始化。

    class Student
    {
    public:
        Student( char* nm );
    
    private:
        string name;
    };

    如果构造函数是下面的:

    Student::Student( char* nm )
    { 
        name = nm;
    }

    C++保证在Student的构造函数体执行之前,所有的成员对象已经创建完成,因此此处编译器会插入对string默认构造函数的调用,该调用在Student的构造函数体执行之前进行。事实上,对string默认构造函数的调用是没有必要的,可以通过构造函数初始化列表中显式指明string构造函数,来避免这种无效计算:

    Student::Student( char* nm ) : name( nm )
    {
    }


    7 系统体系结构

    内存访问的代价差别很大。在某个特定的RISC体系结构中,若数据位于缓存中,访问它只需要一个CPU周期;若位于主内中(缓存失败),则需要8个CPU周期;若位于硬盘上(页面错误),则需要400000个CPU周期。

    当访问数据时,最先搜索的是数据缓存。若数据不在缓存中,硬件产生缓存失败信号,该信号会从RAM或硬盘加载数据至缓存中。缓存以缓存行为单位,通常加载比我们所寻找的特定数据项更大的一块数据。这样的话,在4字节整数上的缓存失败可能导致加载128字节的缓存行到缓存中。由于相关数据项的位置在内存中很可能相邻,因此这对我们有用:如果随后的指令尝试访问相同缓存行中的其他数据项,那么第一次访问该数据时就将缓存成功,能较快地获取数据。我们应该尽可能帮助自己的代码显示这种引用的位置。

    以类X为例:

    class X
    {
    public:
        X() : a(1), c(2) {}
    
    private:
        int a;
        char b[4096];
        int c;
    }

    符合标准的编译器将按声明顺序来排列对象X:成员a和成员c被4096字节的成员b分离,因而不会出现在相同的缓存行内。当X::X构造函数访问c时有可能遇到缓存失败。

    既然a和c被相邻指令访问,那么将它们放在相邻位置,会使得缓存成功率更高。

    class X
    {
    public:
        X() : a(1), c(2) {}
    
    private:
        int a;
        int c;
        char b[4096];
    }



    8 内存管理

    动态分配和释放内存的代价比较昂贵。如果在函数中需要使用一个临时局部变量,一种办法是用new和delete来分配和释放堆内存,另一种方法直接在栈上分配这个变量,因此不需要先为其分配内存,也不用在函数退出时释放内存,函数退出时,栈内存会自动释放,从而避免new和delete的巨大代价。


    9 库和系统调用

    库和系统调用时经常会有背后隐藏让人无法觉察的检查,比如可能在调用pthread_mutex_lock时,pthreads库的实现会检查线程是否已经拥有锁,这样就防止了死锁。但或许我们在使用这个函数时,可以确保线程不会拥有锁,那么额外的检查会是一种负担。(库和系统调用可能封装了一些过多的操作,如果我们不需要其中的某些,我们可以按需要自己来调用底层的函数来实现想要的功能)。

    10 编译器优化

    一个优秀的编译器可以代替开发者实施一些重要的优化,而无须开发者对源代码进行任何干预。第一个想到的优化是寄存器分配。当变量位于寄存器中时,载入和存储该变量是最快的。对执行路径中的许多方法都可以使用寄存器分配,当遇到循环索引变量时,这种优化作用更是特别明显。因为每次循环迭代时都需要访问和更新这些变量。

    第二个是内联。



    编码优化在范围上是局部的,并且不需要对程序的整体设计有深入的理解。当您加入到一个正在进行的开发项目中,并且您对其设计还没有完全理解时,这会是一个很好的起点。

    最快的代码是从不执行的代码。




  • 相关阅读:
    uva 10369 Arctic Network
    uvalive 5834 Genghis Khan The Conqueror
    uvalive 4848 Tour Belt
    uvalive 4960 Sensor Network
    codeforces 798c Mike And Gcd Problem
    codeforces 796c Bank Hacking
    codeforces 768c Jon Snow And His Favourite Number
    hdu 1114 Piggy-Bank
    poj 1276 Cash Machine
    bzoj 2423 最长公共子序列
  • 原文地址:https://www.cnblogs.com/pangblog/p/3291940.html
Copyright © 2011-2022 走看看