More Effective C++ 条款17 考虑使用lazy evaluation(缓式评估)

  1. lazy evaluationg实际上是"拖延战术":延缓运算直到运算结果被需要为止.如果运算结果一直不被需要,运算也就不被执行,从而提高了效率.所谓的运算结果不被执行,有时指只有部分运算结果被需要,那么采用拖延战术,便可避免另一部分不被需要的运算,从而提高效率,以下是lazy evaluation的四种用途.

  2. Reference Counting(引用计数)

如果要自己实现一个string类,那么对于以下代码:

String s1="Hello";
String s2=s1;

最直接的是采用eager evalutation(急式评估):为s1做一个副本并放入s2内,尽管此时s2的内容和s1并没有不同.

采用lazy evaluation的思想,可以先让s2分享s1的值,这样就省去了"调用new"以及"复制任何东西"的高昂成本.唯一要做的是一些簿记工作,以记录共享同一内容的各个对象.对s2的任何读操作,只需要s1的值即可,然而,一旦需要对s2的值进行写操作,就不能再做任何拖延,必须为s2做一份真实副本并进行写操作.

这种"数据共享"的观念就是lazy evaluation:在真正需要之前,不为对象构造副本.在某些应用领域,可能永远也不需要提供那样一份副本,从而提高效率.

  1. 区分读和写

承接于2的策略,如果对自定义的string类进行以下操作:
More Effective C++ 条款17 考虑使用lazy evaluation(缓式评估)More Effective C++ 条款17 考虑使用lazy evaluation(缓式评估)

String s1="Hello World!";
String s2=s1;
cout<<s2[0];
cin>>s2[1];

View Code
那么两次对operator []的调用operator[]的实际行为实际上是不同的,前一个读操作只需要返回对应的引用即可,后一个写操作就需要先对s1做一个副本并放入s2中,然后再返回引用,也就是说,视operator[]用于读操作还是写操作,需要在operator内做不同事情,而要判断operator[]用于读操作还是写操作几乎是不可能的是,但如果利用lazy evaluation和条款30所描述的proxy classes,便可以延缓决定"究竟是读还是写",知道确定其答案.

  1. Lazy Fetching(缓式取出)

对于程序需要使用内含许多字段的大型对象,比如:
More Effective C++ 条款17 考虑使用lazy evaluation(缓式评估)More Effective C++ 条款17 考虑使用lazy evaluation(缓式评估)

class LargeObject{
public:
    LargeObject(ObjectID id);
    const string&field1()const;
    int field2()const;
    double field3()const;
    const string& field4()const;
    const string& field5()const;
    ...
}

View Code
那么对于从磁盘中回复一个LargeObject对象,如果要取出此对象的所有字段,数据库相关操作成本可能极高,尤其是如果这些数据需要从远程数据库跨越网络而来.但如果只需要该对象的某个或某几个字段,那么读取所有数据的操作其实是不必要的.

采用Lazy evaluation的思想,在产生一个LargeObject对象时,可以只产生该对象的"外壳",而不从磁盘读取任何数据.只有当该对象的某个字段被需要时,才从数据库中取回对应的数据,以下做法可以实现这种"demand-page"式的对象初始化行为:
More Effective C++ 条款17 考虑使用lazy evaluation(缓式评估)More Effective C++ 条款17 考虑使用lazy evaluation(缓式评估)

class LargeObject{
public:
    LargeObject(ObjectID id);
    const string&field1()const;
    int field2()const;
    double field3()const;
    const string& field4()const;
    const string& field5()const;
    ...
private:
    ObjectID oid;
    mutable string *field1Vaule;//注意使用了mutable修饰符
    mutable int *field2Value;
    mutable double *field3Value;
    mutable string *field4Value;
    ...
}
LargeObject::LargeObject(Object  id):oid(id),field1Value(0),field2Value(0),field3Value(3)...{}
const string& LargeObject::field1()const{
    if(field1Value==0){
        read the data from field 1 from the database and make field1Vaule point to it;
    }
    return *field1Value;
}

View Code
对象内的每个字段都是指针,指向必要的数据,NULL初始值表示该字段未被读入,需要先从数据库读入.将字段指针声明为mutable,可以保证字段指针可以在任何时候都能被更改以指向实际数据,即使是在const成员函数内也一样.有些编译器厂商可能不支持mutable的使用,在const成员函数内可以采用const_cast甚至C转型操作移除this的常量特性并构造一个冒牌this并进行相关操作:
More Effective C++ 条款17 考虑使用lazy evaluation(缓式评估)More Effective C++ 条款17 考虑使用lazy evaluation(缓式评估)

const string& LargeObject::field1()const{
    LargeObject* const fakeThis=const_cast<LargeObject*const>(this);
    if(field1Value==0){
       fakeThis->field1Value=the proper data from the database;
    }
    return *field1Value;
}

View Code
5. Lazy Expression Evaluation(表达式缓评估)

对于以下代码:
More Effective C++ 条款17 考虑使用lazy evaluation(缓式评估)More Effective C++ 条款17 考虑使用lazy evaluation(缓式评估)

template<class T>
class Matrix{...}
Matrix<int> m1(1000,1000);
Matrix<int> m2(1000,1000);
...
Matrix<int>m3=m1+m2;

View Code
对于operator+,通常的做法是eager ecaluation:计算并返回m1和m2的和,这是一个大规模运算,并需要大量内存分配成本.

采用lazy evaluation的思想,可以先设一个数据结构于m3中,用于标记m3是m1和m2的总和,这个数据结构可能只由两个指针和一个enum组成,前者指向m1和m2,后者用来表示运算动作.假设在m3被使用之前,程序又执行以下动作:
More Effective C++ 条款17 考虑使用lazy evaluation(缓式评估)More Effective C++ 条款17 考虑使用lazy evaluation(缓式评估)

Matrix m4(1000,1000);
...
m3=m4*m1;

View Code
那么便可直接将m3定位为m4和m1的乘积,之前没有用到的矩阵加法操作实际上并没有进行.当然,对m1和m2加和却没有用到的情况比较夸张,但不排除维护过程中程序员更改代码使得m1+m2不被用到的请况出现.

当然,lazy evaluation在此处还有更大用法——只计算大型运算中需要的部分运算结果.

对于以下代码:

cout<<m3[4];

此时不能再使用拖延战术,但也只需要计算m3第四行的值,除此以外,不需要计算其他任何值.实际上,正是这种策略使得APL(20世纪60年代的一款如软件,允许用户以交谈方式使用软件执行矩阵运算)能够快速处理加法,减法,乘法甚至除法.

当然,有时lazy evaluation并不能起作用,比如如下操作:

cout<<m3;

或者:

m3=m1+m2;
m1=m4;

这时就要采取某些措施以确保对m1的改变不会影响m3的值,可以在对m1进行改变之前先对m3求解,也可以将m1的旧值复制一份,然后令m3依从该值等,其他可能会修改矩阵值的情况,也要采取类似措施.

此外,由于存储数值间的相依关系,必须维护一些数据结构一存取数值,相依关系等,此外还必须将赋值,复制,加法等操作符进行重载,因此lazy evaluation用于数值运算领域也有许多工作要做,但与节省的时间和空间相比可能是微不足道的.

  1. "Lazy evaluation在许多领域中都可能有用途:可避免非必要的对象复制,可区别operator[]的读取和写操作,可避免非必要的数据库读取动作,可避免非必要的数值计算动作."但其提升效率的前提是(部分)计算可能可以被避免,否则,在计算绝对必要的情况下,使用lazy evaluation不仅不能提升效率,还需要付出为lazy evaluation而设计的额外的数据结构等代价.

  2. "Lazy evaluation并非C++的专属技能.这项技术可以用任何一种程序语言完成,有数种语言——APL,某些Lisp版本,以及几乎所有的数据流(dataflow)语言——已接受这个观念成为语言的一个基础部分."不过由于C++对封装性质的支持使得将lazy evaluation加入某个类内而对客户隐藏具体实现成为可能.
    原文链接: https://www.cnblogs.com/reasno/p/4830677.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/222165

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月13日 上午11:37
下一篇 2023年2月13日 上午11:37

相关推荐