📄 ch12.htm
字号:
<tt>}</tt></pre><p>When the program is linked, the linker is faced with two identical copies of <tt>Time::Show()</tt>. Normally, function redefinition causes a link-time error. Un-inlined functions are a special case, however. Older implementations of C++ coped with this situation by treating an un-inlined function as if it had been declared <tt>static</tt>. Consequently, each copy of the compiled function was only visible within the translation unit in which it was declared. This solved the name clashing problem at the cost of multiple local copies of the same function. In this case, the <tt>inline</tt> declaration did not boost performance; on the contrary, every call of the un-inlined function was resolved as an ordinary function call with the regular overhead. Even worse, the multiple copies of the function code increased compilation and linkage time and bloated the size of the executable. Ironically, <i>not</i> declaring <tt>Time::Show()</tt> <tt>inline</tt> might have yielded better performance! Remember that the programmer is generally not aware of all the actual costs of this -- the compiler strains quietly, the linker sighs silently, and the resultant executable is more bloated and sluggish than ever. But it still works, and the users scratch their heads, saying, "This object-oriented programming is really awful! I'm sure this app would run much faster if I'd written it in C!". </p><p>Fortunately, the Standard's specification regarding un-inlined functions was recently changed. A Standard compliant implementation generates only a single copy of such a function, regardless of the number of translation units that define it. In other words, an un-inlined function is treated similarly to an ordinary function. However, it might take some time for all compiler vendors to adopt the new specifications.</p><h3> <a name="Heading13"> Additional Issues of Concern</a></h3><p>There are two more conundrums that are associated with inline functions. The first has to do with maintenance. A function can begin its life as a slim inline function, offering the benefits that were previously described. At a later phase in the lifetime of the system, the function body can be extended to include additional functionality, resulting from changes in the implementation of its class. Suddenly, the inline substitution can become inefficient or even impossible. It is therefore important to reconsider the removal of the <tt>inline</tt> specifier from such functions. For member functions that are defined in the class body, the change is more complicated because the function definition has to be moved to a separate source file.</p><p>Another problem might arise when inline functions are used in code libraries. It is impossible to maintain binary compatibility if the definition of an inline function changes. In this case, the users must recompile their code to reflect the change. For a non-inline function, the users only need to relink their code, which is considerably less of a burden than a massive recompilation and relink.</p><h3> <a name="Heading14"> The Do's and Don'ts of inline</a></h3><p>The lesson here is that <tt>inline</tt> is not a magical potion for enhancing performance. For very short functions -- for example, accessors, mutators, and function wrappers (see Chapter 13, "C Language Compatibility Issues") -- the <tt>inline</tt> specifier can be profitable in terms of both execution speed and program size. If the inlined function is not very short and it is called extensively, however, the result can be an increase in the size of the executable. Furthermore, many processors cache the machine instructions of frequently used parts of the program; excessive inlining can cause a reduced instruction cache hit and, consequently, poorer overall performance. The real annoyance occurs when the compiler refuses to inline a function even though it was declared <tt>inline</tt>. On older implementations, the result was quite painful. On Standard compliant implementations, the consequences of un-inlining are less detrimental, but they are still undesirable. Some compilers are clever enough to figure out on their own which functions are to be inlined. However, most compilers are less inline-savvy so it is best to examine the effect of an inline declaration empirically. If the inline declaration does not enhance performance, avoid it. </p><h2> <a name="Heading15"> Optimizing Memory Usage</a></h2><p>Optimization has several aspects: faster execution speed, efficient usage of system resources, and minimal usage of memory. In general, code optimization attempts to improve all these aspects. The declaration relocation technique that was demonstrated earlier eliminates the unnecessary creation and destruction of objects, thereby reducing the program's size and accelerating its runtime speed. However, other optimization techniques are heavily biased toward one direction -- speedier code or a smaller memory footprint. Sometimes, though, these goals are mutually exclusive; that is, compacting the memory footprint engenders slower code, whereas a faster code implies a larger memory footprint. This section presents various techniques for optimizing, or compacting, the memory requirements of a program.</p><h3> <a name="Heading16"> Bit Fields</a></h3><p>In both C and C++ it is possible to store and access data directly in the tiniest possible unit: a single bit. Because a bit is not the natural storage unit for C/C++ implementations, the use of bit fields can increase the size of the executable due to the additional maneuvers that are exercised by the processor in accessing a sequence of one or more bits. This is a clear-cut case of sacrificing runtime speed for the sake of minimizing memory usage.</p><blockquote> <hr> <strong>NOTE: </strong> Note, however, that some hardware architectures provide special processor instructions for accessing bits. Therefore, whether bit fields affect the program's speed or not is very much platform-dependent. <hr></blockquote><p>Normally, you don't use bit fields just to save a few more bytes. For some applications, however, the tradeoff between execution speed and storage compaction is definitely worth its while. For example, the billing system of an average international telephone company stores every phone call as a record in a relational database. These records are processed in batch periodically to calculate the customer's monthly bill. The database stores millions of new records every day, and it has to keep the customer's billing information for at least one year. The complete database contains around one billion records at any given time. Because the database is also backed up periodically, and because it might also be a distributed database, every record is stored in more than one physical location. In fact, there might be 20 billion records stored in different backup generations and distributed portions of the database at any given time. A minimal billing record contains the customer's ID, a timestamp, codes that indicate the type of the call (for example, local or long distance) and the tariff (off peak, peak time). Literally, every bit counts here -- one redundant bit implies 2.5GB of wasted storage! </p><p>A non-space-optimizing definition of the billing record might look like this:</p><pre><tt>struct BillingRec</tt><tt>{</tt><tt> long cust_id;</tt><tt> long timestamp;</tt><tt> enum CallType</tt><tt> {</tt><tt> toll_free,</tt><tt> local,</tt><tt> regional,</tt><tt> long_distance,</tt><tt> international,</tt><tt> cellular</tt><tt> } type;</tt><tt> enum CallTariff</tt><tt> {</tt><tt> off_peak,</tt><tt> medium_rate,</tt><tt> peak_time</tt><tt> } tariff;</tt><tt>};</tt></pre><p>A <tt>BillingRec</tt> occupies no fewer than 16 bytes of memory on my 32-bit machine. Clearly, space is wasted here. The first two fields occupy four bytes each, as expected. However, the two <tt>enum</tt> variables occupy an additional eight bytes, even though they both can be safely represented in less than a single byte. A tweaked version of <tt>BillingRec</tt> can squeeze the <tt>enum</tt> values into two bit fields:</p><pre><tt>struct BillingRec</tt><tt>{</tt><tt> long cust_id;</tt><tt> long timestamp;</tt><tt> enum CallType</tt><tt> {</tt><tt> toll_free,</tt><tt> local,</tt><tt> regional,</tt><tt> long_distance,</tt><tt> international,</tt><tt> cellular</tt><tt> };</tt><tt> enum CallTariff</tt><tt> {</tt><tt> off_peak,</tt><tt> medium_rate,</tt><tt> peak_time</tt><tt> };</tt><tt> unsigned call: 3; //three bits </tt><tt> unsigned tariff: 2; //two bits</tt><tt>};</tt></pre><p>The size of <tt>BillingRec</tt> is now 12 bytes. The four bytes that are saved are equal to megabytes of data storage per day. Still, the size can be reduced even more. The two bit fields occupy five bits in total, which is less than a byte. One might therefore expect <tt>BillingRec</tt> to occupy 9 bytes rather than 12. The problem is that the compiler inserts three additional padding bytes after the bit fields to align the size of <tt>BillingRec</tt> on a word boundary (more on member alignment in Chapter 11, "Memory<i> </i>Management"). The additional padding bytes ensure faster access time -- at the cost of three wasted bytes. There are two ways to overcome this problem: You can change the compiler's setting to allow alignment on a byte boundary, or you can change the size of the other members so that -- in total -- it reaches exactly eight bytes.</p><blockquote> <hr> <strong>NOTE: </strong> Note that both solutions might not be portable, and on some hardware architectures, the compiler will nonetheless insist on word boundary alignment. Check your compiler's specifications regarding member alignment settings. <hr></blockquote><p>Changing the size of the members is somewhat tricky because the first two members have to become bit fields as well:</p><pre><tt>struct BillingRec</tt><tt>{</tt><tt> int cust_id: 24; // 23 bits + 1 sign bit</tt><tt> int timestamp: 24;</tt><tt> enum CallType</tt><tt> {//</tt>...<tt> };</tt><tt> enum CallTariff</tt><tt> {//</tt>...<tt> };</tt><tt> unsigned call: 3;</tt><tt> unsigned tariff: 2;</tt><tt>};</tt></pre><p>This time, <tt>BillingRec</tt> occupies eight bytes in total, which is half of its original size. The storage that is saved in this example can amount to 10GB annually. Considering the cheap prices of magnetic storage media these days, saving a few thousand dollars might not seem to be a compelling argument -- but there is another reason for favoring smaller data storage: the costs of digital communication. A distributed database has synchronized copies in multiple sites. The synchronization process is usually done by means of digital data transfer from the central database to its synchronized copies, and vice versa. The transmission of millions of records on leased lines is pretty expensive. But for a phone company that owns these lines, this is not an issue of special concern; suppose, however, that the company is an international bank that pays hundreds of dollars for every hour of data transfer. In this case, halving the data volume is unquestionably profitable. Another point to remember is the Web; if the telephone company has a Web site that enables its customers to view their billing information online, the download time of hundreds of records through analog dialup lines can be cut in half by this tweak. </p><h3> <a name="Heading17"> Unions</a></h3><p>Unions can also be used to minimize memory waste by locating two or more data members at the same memory address, where the value of (at most) one of the data members is active at any time. The size of a union is sufficient to hold the largest of its data members. A union can have member functions, including a constructor and destructor, but it cannot have virtual member functions. A union cannot serve as a base class of, nor can it inherit from, another class. In addition, a union cannot store objects that have nontrivial special member functions. C++ also supports <i>anonymous unions</i>. An anonymous union is an unnamed object of an unnamed type (anonymous unions are also discussed in Chapter 11). For example</p><pre><tt>union { long n; void * p}; // anonymous</tt><tt>n = 1000L; // members are directly accessed</tt><tt>p = 0; // n is now also 0 </tt></pre><p>Unlike a named union, an anonymous one cannot have member functions or nonpublic data members. </p><p>When are unions useful? The following class retrieves a person's data from a database. The key can be either a unique ID number or a person's last name, but never both at once:</p><pre><tt>class PersonalDetails </tt><tt>{</tt><tt>private:</tt><tt> char * name;</tt><tt> long ID;</tt><tt> //...</tt><tt>public:</tt><tt> PersonalDetails(const char *nm); //key is of type char * used</tt><tt> PersonalDetails(long id) : ID(id) {} //numeric key used </tt><tt>};</tt></pre><p>Memory is wasted here because only one of the keys can be used at a time. An anonymous union can be used in this case to minimize memory usage. For example</p><pre><tt>class PersonalDetails </tt><tt>{</tt><tt>private:</tt><tt> union //anonymous</tt><tt> { </tt><tt> char * name; </tt><tt> long ID; </tt>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -