📄 mc4.htm
字号:
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; }}Try not to get bogged down in the details of the STL code (which will be clearer after you've read Item 35). Instead, focus on the general strategy embodied by this function. That strategy is to use a local cache to replace comparatively expensive database queries with comparatively inexpensive lookups in an in-memory data structure. Provided we're correct in assuming that cubicle numbers will frequently be requested more than once, the use of a cache in findCubicleNumber should reduce the average cost of returning an employee's cubicle number.(One detail of the code requires explanation. The final statement returns (*it).second instead of the more conventional it->second. Why? The answer has to do with the conventions followed by the STL. In brief, the iterator it is an object, not a pointer, so there is no guarantee that "->" can be applied to it.6 The STL does require that "." and "*" be valid for iterators, however, so (*it).second, though syntactically clumsy, is guaranteed to work.)Caching is one way to amortize the cost of anticipated computations. Prefetching is another. You can think of prefetching as the computational equivalent of a discount for buying in bulk. Disk controllers, for example, read entire blocks or sectors of data when they read from disk, even if a program asks for only a small amount of data. That's because it's faster to read a big chunk once than to read two or three small chunks at different times. Furthermore, experience has shown that if data in one place is requested, it's quite common to want nearby data, too. This is the infamous locality of reference phenomenon, and systems designers rely on it to justify disk caches, memory caches for both instructions and data, and instruction prefetches.Excuse me? You say you don't worry about such low-level things as disk controllers or CPU caches? No problem. Prefetching can yield dividends for even one as high-level as you. Imagine, for example, you'd like to implement a template for dynamic arrays, i.e., arrays that start with a size of one and automatically extend themselves so that all nonnegative indices are valid: template<class T> // template for dynamicclass DynArray { ... }; // array-of-T classesDynArray<double> a; // at this point, only a[0] // is a legitimate array // elementa[22] = 3.5; // a is automatically // extended: valid indices // are now 0-22a[32] = 0; // a extends itself again; // now a[0]-a[32] are validHow does a DynArray object go about extending itself when it needs to? A straightforward strategy would be to allocate only as much additional memory as needed, something like this: template<class T>T& DynArray<T>::operator[](int index){ if (index < 0) { throw an exception; // negative indices are } // still invalid if (index > the current maximum index value) { call new to allocate enough additional memory so that index is valid; } return the indexth element of the array;}This approach simply calls new each time it needs to increase the size of the array, but calls to new invoke operator new (see Item 8), and calls to operator new (and operator delete) are usually expensive. That's because they typically result in calls to the underlying operating system, and system calls are generally slower than are in-process function calls. As a result, we'd like to make as few system calls as possible.An over-eager evaluation strategy employs this reasoning: if we have to increase the size of the array now to accommodate index i, the locality of reference principle suggests we'll probably have to increase it in the future to accommodate some other index a bit larger than i. To avoid the cost of the memory allocation for the second (anticipated) expansion, we'll increase the size of the DynArray now by more than is required to make i valid, and we'll hope that future expansions occur within the range we have thereby provided for. For example, we could write DynArray::operator[] like this: template<class T>T& DynArray<T>::operator[](int index){ if (index < 0) throw an exception; if (index > the current maximum index value) { int diff = index - the current maximum index value; call new to allocate enough additional memory so that index+diff is valid; } return the indexth element of the array;}This function allocates twice as much memory as needed each time the array must be extended. If we look again at the usage scenario we saw earlier, we note that the DynArray must allocate additional memory only once, even though its logical size is extended twice: DynArray<double> a; // only a[0] is valida[22] = 3.5; // new is called to expand // a's storage through // index 44; a's logical // size becomes 23a[32] = 0; // a's logical size is // changed to allow a[32], // but new isn't calledIf a needs to be extended again, that extension, too, will be inexpensive, provided the new maximum index is no greater than 44.There is a common theme running through this Item, and that's that greater speed can often be purchased at a cost of increased memory usage. Keeping track of running minima, maxima, and averages requires extra space, but it saves time. Caching results necessitates greater memory usage but reduces the time needed to regenerate the results once they've been cached. Prefetching demands a place to put the things that are prefetched, but it reduces the time needed to access those things. The story is as old as Computer Science: you can often trade space for time. (Not always, however. Using larger objects means fewer fit on a virtual memory or cache page. In rare cases, making objects bigger reduces the performance of your software, because your paging activity increases, your cache hit rate decreases, or both. How do you find out if you're suffering from such problems? You profile, profile, profile (see Item 16).)The advice I proffer in this Item that you amortize the cost of anticipated computations through over-eager strategies like caching and prefetching is not contradictory to the advice on lazy evaluation I put forth in Item 17. Lazy evaluation is a technique for improving the efficiency of programs when you must support operations whose results are not always needed. Over-eager evaluation is a technique for improving the efficiency of programs when you must support operations whose results are almost always needed or whose results are often needed more than once. Both are more difficult to implement than run-of-the-mill eager evaluation, but both can yield significant performance improvements in programs whose behavioral characteristics justify the extra programming effort. Back to Item 18: Amortize the cost of expected computationsContinue to Item 20: Facilitate the return value optimizationItem 19: Understand the origin of temporary objects.When programmers speak amongst themselves, they often refer to variables that are needed for only a short while as "temporaries." For example, in this swap routine, template<class T>void swap(T& object1, T& object2){ T temp = object1; object1 = object2; object2 = temp;}it's common to call temp a "temporary." As far as C++ is concerned, however, temp is not a temporary at all. It's simply an object local to a function.True temporary objects in C++ are invisible they don't appear in your source code. They arise whenever a non-heap object is created but not named. Such unnamed objects usually arise in one of two situations: when implicit type conversions are applied to make function calls succeed and when functions return objects. It's important to understand how and why these temporary objects are created and destroyed, because the attendant costs of their construction and destruction can have a noticeable impact on the performance of your programs.Consider first the case in which temporary objects are created to make function calls succeed. This happens when the type of object passed to a function is not the same as the type of the parameter to which it is being bound. For example, consider a function that counts the number of occurrences of a character in a string: // returns the number of occurrences of ch in strsize_t countChar(const string& str, char ch);char buffer[MAX_STRING_LEN];char c;// read in a char and a string; use setw to avoid// overflowing buffer when reading the stringcin >> c >> setw(MAX_STRING_LEN) >> buffer;cout << "There are " << countChar(buffer, c)
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -