More Effective C++ 条款18

原创 2001年10月18日 23:13:00

 条款18:分期摊还期望的计算

在条款17中,我极力称赞懒惰的优点,尽可能地拖延时间,并且我解释说懒惰如何提高程序的运行效率。在这个条款里我将采用一种不同的态度。这里将不存在懒惰。我鼓励你让程序做的事情比被要求的还要多,通过这种方式来提高软件的性能。这个条款的核心就是over-eager evaluation(过度热情计算法):在要求你做某些事情以前就完成它们。例如下面这个模板类,用来表示放有大量数字型数据的一个集合:

template<class NumericalType>

class DataCollection {

public:

  NumericalType min() const;

  NumericalType max() const;

  NumericalType avg() const;

  ...

};

假设min,max和avg函数分别返回现在这个集合的最小值,最大值和平均值,有三种方法实现这三种函数。使用eager evaluation(热情计算法),当min,max和avg函数被调用时,我们检测集合内所有的数值,然后返回一个合适的值。使用lazy evaluation(懒惰计算法),只有确实需要函数的返回值时我们才要求函数返回能用来确定准确数值的数据结构。使用 over-eager evaluation(过度热情计算法),我们随时跟踪目前集合的最小值,最大值和平均值,这样当min,max或avg被调用时,我们可以不用计算就立刻返回正确的数值。如果频繁调用min,max和avg,我们把跟踪集合最小值、最大值和平均值的开销分摊到所有这些函数的调用上,每次函数调用所分摊的开销比eager evaluation或lazy evaluation要小。

隐藏在over-eager evaluation后面的思想是如果你认为一个计算需要频繁进行。你就可以设计一个数据结构高效地处理这些计算需求,这样可以降低每次计算需求的开销。

采用over-eager最简单的方法就是caching(缓存)那些已经被计算出来而以后还有可能需要的值。例如你编写了一个程序,用来提供有关雇员的信息,这些信息中的经常被需要的部分是雇员的办公隔间号码。而假设雇员信息存储在数据库里,但是对于大多数应用程序来说,雇员隔间号都是不相关的,所以数据库不对查抄它们进行优化。为了避免你的程序给数据库造成沉重的负担,可以编写一个函数findCubicleNumber,用来cache查找的数据。以后需要已经被获取的隔间号时,可以在cache里找到,而不用向数据库查询。

以下是实现findCubicleNumber的一种方法:它使用了标准模板库(STL)里的map对象(有关STL参见条款35)。

int findCubicleNumber(const string& employeeName)

{

  // 定义静态map,存储 (employee name, cubicle number)

  // pairs. 这个 map local cache

  typedef map<string, int> CubicleMap;

  static CubicleMap cubes;

 

  // try to find an entry for employeeName in the cache;

  // the STL iterator "it" will then point to the found

  // entry, if there is one (see Item 35 for details)

  CubicleMap::iterator it = cubes.find(employeeName);

 

  // "it"'s value will be cubes.end() if no entry was

  // found (this is standard STL behavior). If this is

  // the case, consult the database for the cubicle

  // number, then add it to the cache

  if (it == cubes.end()) {

    int cubicle =

      the result of looking up employeeName's cubicle

      number in the database;

 

    cubes[employeeName] = cubicle;           // add the pair

                                             // (employeeName, cubicle)

                                             // to the cache

    return cubicle;

  }

  else {

    // "it" points to the correct cache entry, which is a

    // (employee name, cubicle number) pair. We want only

    // the second component of this pair, and the member

    // "second" will give it to us

    return (*it).second;

  }

}

不要陷入STL代码的实现细节里(你读完条款35以后,你会比较清楚)。应该把注意力放在这个函数蕴含的方法上。这个方法是使用local cache,用开销相对不大的内存中查询来替代开销较大的数据库查询。假如隔间号被不止一次地频繁需要,在findCubicleNumber内使用cache会减少返回隔间号的平均开销。

(上述代码里有一个细节需要解释一下,最后一个语句返回的是(*it).second,而不是常用的it->second。为什么?答案是这是为了遵守STL的规则。简单地说,iterator是一个对象,不是指针,所以不能保证”->”被正确应用到它上面。不过STL要求”.””*”iterator上是合法的,所以(*it).second在语法上虽然比较繁琐,但是保证能运行。)

catching是一种分摊期望的计算开销的方法。Prefetching(预提取)是另一种方法。你可以把prefech想象成购买大批商品而获得的折扣。例如磁盘控制器从磁盘读取数据时,它们会读取一整块或整个扇区的数据,即使程序仅需要一小块数据。这是因为一次读取一大块数据比在不同时间读取两个或三个小块数据要快。而且经验显示如果需要一个地方的数据,则很可能也需要它旁边的数据。这是位置相关现象,正因为这种现象,系统设计者才有理由为指令和数据使用磁盘cache和内存cache,还有使用指令prefetch

你说你不关心象磁盘控制器或CPU cache这样低级的东西。没有问题。prefetch在高端应用里也有优点。例如你为dynamic数组实现一个模板,dynamic就是开始时具有一定的尺寸,以后可以自动扩展的数组,所以所有非负的索引都是合法的:

template<class T>                            // dynamic数组

class DynArray { ... };                      // 模板

 

DynArray<double> a;                          // 在这时, 只有 a[0]

                                             // 是合法的数组元素

                                            

 

a[22] = 3.5;                                 // a 自动扩展

                                             //: 现在索引022

                                             // 是合法的

 

a[32] = 0;                              // 有自行扩展;

                                      // 现在 a[0]-a[32]是合法的

一个DynArray对象如何在需要时自行扩展呢?一种直接的方法是分配所需的额外的内存。就象这样:

template<class T>

T& DynArray<T>::operator[](int index)

{

  if (index < 0) {

    throw an exception;                     // 负数索引仍不

  }                                         // 合法

 

  if (index >当前最大的索引值) {

    调用new分配足够的额外内存,以使得

   索引合法;

  }

 

  返回index位置上的数组元素;

}

每次需要增加数组长度时,这种方法都要调用new,但是调用new会触发operator new(参见条款8)operator new (operator delete)的调用通常开销很大。因为它们将导致底层操作系统的调用,系统调用的速度一般比进程内函数调用的速度慢。因此我们应该尽量少使用系统调用。

使用Over-eager evaluation方法,其原因我们现在必须增加数组的尺寸以容纳索引i,那么根据位置相关性原则我们可能还会增加数组尺寸以在未来容纳比i 大的其它索引。为了避免为扩展而进行第二次(预料中的)内存分配,我们现在增加DynArray的尺寸比能使i 合法的尺寸要大,我们希望未来的扩展将被包含在我们提供的范围内。例如我们可以这样编写DynArray::operator[]

template<class T>

T& DynArray<T>::operator[](int index)

{

  if (index < 0) throw an exception;

 

  if (index > 当前最大的索引值) {

    int diff = index – 当前最大的索引值;

 

    调用new分配足够的额外内存,使得

    index+diff合法;

  }

 

  返回index位置上的数组元素;

}

这个函数每次分配的内存是数组扩展所需内存的两倍。如果我们再来看一下前面遇到的那种情况,就会注意到DynArray只分配了一次额外内存,即使它的逻辑尺寸被扩展了两次:

 

DynArray<double> a;                          // 仅仅a[0]是合法的

 

a[22] = 3.5;                                 // 调用new扩展

                                             // a的存储空间到索引44

                                             // a的逻辑尺寸

                                             // 变为23

 

a[32] = 0;                                   // a的逻辑尺寸

                                             // 被改变,允许使用a[32],

                                             // 但是没有调用new

如果再次需要扩展a,只要提供的新索引不大于44,扩展的开销就不大。

贯穿本条款的是一个常见的主题,更快的速度经常会消耗更多的内存。跟踪运行时的最小值、最大值和平均值,这需要额外的空间,但是能节省时间。Cache运算结果需要更多的内存,但是一旦需要被cache的结果时就能减少需要重新生成的时间。Prefetch需要空间放置被prefetch的东西,但是它减少了访问它们所需的时间。自从有了计算机就有这样的描述:你能以空间换时间。(然而不总是这样,使用大型对象意味着不适合虚拟内存或cache 页。在一些罕见的情况下,建立大对象会降低软件的性能,因为分页操作的增加(详见操作系统中内存管理  译者注),cache命中率降低,或者两者都同时发生。如何发现你正遭遇这样的问题呢?你必须profile, profile, profile(参见条款16)

在本条款中我提出的建议,即通过over-eager方法分摊预期计算的开销,例如cachingprefething,这并不与我在条款17中提出的有关lazy evaluation的建议相矛盾。当你必须支持某些操作而不总需要其结果时,可以使用lazy evaluation用以提高程序运行效率。当你必须支持某些操作而其结果几乎总是被需要或被不止一次地需要时,可以使用over-eager用以提高程序运行效率。它们对性能的巨大提高证明在这方面花些精力是值得的。

《More Effective C++》条款27:如何让类对象只在栈(堆)上分配空间?

昨天一个同学去网易面试C++研发,问到了这么一个问题:如何限制一个类对象只在栈(堆)上分配空间? 一般情况下,编写一个类,是可以在栈或者堆分配空间。但有些时候,你想编写一个只能在栈或者只能在堆上面分...
  • hxz_qlh
  • hxz_qlh
  • 2013年10月26日 21:27
  • 6092

《Effective C++》:条款28-条款29

条款28避免返回handles指向对象内部成分:指的是不能返回对象内部数据/函数的引用、指针等。 条款29为异常安全而努力是值得的:指的是要有异常处理机制,避免发生异常时造成资源泄露等问题。...
  • KangRoger
  • KangRoger
  • 2015年02月19日 19:47
  • 1395

Effective C++ 条款2

尽量以const、enum、inline替换#define首先,大家要明白一个道理。#define是什么,有什么作用。很简单,大家都知道#define实现宏定义,如下代码:#define Flag 1...
  • u011058765
  • u011058765
  • 2015年06月19日 12:06
  • 501

《Effective C++》:条款41-条款42

条款41了解隐式接口和编译期多态 条款42了解typename的双重意义条款
  • KangRoger
  • KangRoger
  • 2015年03月10日 22:13
  • 1247

《Effective C++》:条款44-条款45

条款44将与参数无关的代码抽离templates 条款45运用成员函数模板接受所有兼容类型...
  • KangRoger
  • KangRoger
  • 2015年03月12日 22:01
  • 1509

《Effective C++》资源管理:条款13-条款15

在系统中,资源是有限的,一旦用完必须归还给系统,否则可能会造成资源耗尽或其他问题。例如,动态分配的内存如果用完不释放会造成内存泄漏。 这里说的资源不仅仅是指内存,还包括其他,例如文件描述符、网络连接、...
  • KangRoger
  • KangRoger
  • 2015年01月14日 21:46
  • 1330

[More Effective C++]尽量使用C++风格的类型转换

Item M2:尽量使用C++风格的类型转换 1、C不支持一下转换: 1)把一个指向const 对象的指针(pointer-to-const-object)转换成指向const 对象的指针(poin...
  • AlphaGQ
  • AlphaGQ
  • 2017年04月10日 21:27
  • 280

Effective Modern C++ 条款23 理解std::move和std::forward

Effective Modern C++ 条款23
  • big_yellow_duck
  • big_yellow_duck
  • 2016年08月30日 17:11
  • 1215

Effective C++ 条款15

在资源管理类中提供对原始资源的访问前面两节都在讨论如何管理资源,一般情况下,使用资源管理类来屏蔽原始资源,对抗内存泄露等问题,避免使用原始资源。这样我们就无法直接访问原本的原始资源。毕竟程序在有些时候...
  • u011058765
  • u011058765
  • 2015年06月24日 10:52
  • 670

《Effective C++》:条款38-条款39

条款38通过复合塑模树has-a 或根据某物实现出 条款39明智而审慎的使用private继承...
  • KangRoger
  • KangRoger
  • 2015年03月08日 21:32
  • 1160
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:More Effective C++ 条款18
举报原因:
原因补充:

(最多只允许输入30个字)