Inapoi
Inainte
Cuprins
Stash & Stack with inlines
Armed with inlines, we can now convert
the Stash and Stack classes to be more
efficient:
//: C09:Stash4.h
// Inline functions
#ifndef STASH4_H
#define STASH4_H
#include "../require.h"
class Stash {
int size; // Size of each space
int quantity; // Number of storage spaces
int next; // Next empty space
// Dynamically allocated array of bytes:
unsigned char* storage;
void inflate(int increase);
public:
Stash(int sz) : size(sz), quantity(0),
next(0), storage(0) {}
Stash(int sz, int initQuantity) : size(sz),
quantity(0), next(0), storage(0) {
inflate(initQuantity);
}
Stash::~Stash() {
if(storage != 0)
delete []storage;
}
int add(void* element);
void* fetch(int index) const {
require(0 <= index, "Stash::fetch (-)index");
if(index >= next)
return 0; // To indicate the end
// Produce pointer to desired element:
return &(storage[index * size]);
}
int count() const { return next; }
};
#endif // STASH4_H ///:~
The small functions obviously work well
as inlines, but notice that the two largest functions are still left as
non-inlines, since inlining them probably wouldn’t cause any performance
gains:
//: C09:Stash4.cpp {O}
#include "Stash4.h"
#include <iostream>
#include <cassert>
using namespace std;
const int increment = 100;
int Stash::add(void* element) {
if(next >= quantity) // Enough space left?
inflate(increment);
// Copy element into storage,
// starting at next empty space:
int startBytes = next * size;
unsigned char* e = (unsigned char*)element;
for(int i = 0; i < size; i++)
storage[startBytes + i] = e[i];
next++;
return(next - 1); // Index number
}
void Stash::inflate(int increase) {
assert(increase >= 0);
if(increase == 0) return;
int newQuantity = quantity + increase;
int newBytes = newQuantity * size;
int oldBytes = quantity * size;
unsigned char* b = new unsigned char[newBytes];
for(int i = 0; i < oldBytes; i++)
b[i] = storage[i]; // Copy old to new
delete [](storage); // Release old storage
storage = b; // Point to new memory
quantity = newQuantity; // Adjust the size
} ///:~
Once again, the test program verifies
that everything is working correctly:
//: C09:Stash4Test.cpp
//{L} Stash4
#include "Stash4.h"
#include "../require.h"
#include <fstream>
#include <iostream>
#include <string>
using namespace std;
int main() {
Stash intStash(sizeof(int));
for(int i = 0; i < 100; i++)
intStash.add(&i);
for(int j = 0; j < intStash.count(); j++)
cout << "intStash.fetch(" << j << ") = "
<< *(int*)intStash.fetch(j)
<< endl;
const int bufsize = 80;
Stash stringStash(sizeof(char) * bufsize, 100);
ifstream in("Stash4Test.cpp");
assure(in, "Stash4Test.cpp");
string line;
while(getline(in, line))
stringStash.add((char*)line.c_str());
int k = 0;
char* cp;
while((cp = (char*)stringStash.fetch(k++))!=0)
cout << "stringStash.fetch(" << k << ") = "
<< cp << endl;
} ///:~
This is the same test program that was
used before, so the output should be basically the same.
The Stack class makes even better
use of inlines:
//: C09:Stack4.h
// With inlines
#ifndef STACK4_H
#define STACK4_H
#include "../require.h"
class Stack {
struct Link {
void* data;
Link* next;
Link(void* dat, Link* nxt):
data(dat), next(nxt) {}
}* head;
public:
Stack() : head(0) {}
~Stack() {
require(head == 0, "Stack not empty");
}
void push(void* dat) {
head = new Link(dat, head);
}
void* peek() const {
return head ? head->data : 0;
}
void* pop() {
if(head == 0) return 0;
void* result = head->data;
Link* oldHead = head;
head = head->next;
delete oldHead;
return result;
}
};
#endif // STACK4_H ///:~
Notice that the Link destructor
that was present but empty in the previous version of Stack has been
removed. In pop( ), the expression delete oldHead simply
releases the memory used by that Link (it does not destroy the data
object pointed to by the Link).
Most of the functions inline quite nicely
and obviously, especially for Link. Even pop( ) seems
legitimate, although anytime you have conditionals or local variables it’s
not clear that inlines will be that beneficial. Here, the function is small
enough that it probably won’t hurt anything.
If all your functions are inlined,
using the library becomes quite simple because there’s no linking
necessary, as you can see in the test example (notice that there’s no
Stack4.cpp):
//: C09:Stack4Test.cpp
//{T} Stack4Test.cpp
#include "Stack4.h"
#include "../require.h"
#include <fstream>
#include <iostream>
#include <string>
using namespace std;
int main(int argc, char* argv[]) {
requireArgs(argc, 1); // File name is argument
ifstream in(argv[1]);
assure(in, argv[1]);
Stack textlines;
string line;
// Read file and store lines in the stack:
while(getline(in, line))
textlines.push(new string(line));
// Pop the lines from the stack and print them:
string* s;
while((s = (string*)textlines.pop()) != 0) {
cout << *s << endl;
delete s;
}
} ///:~
People will sometimes write classes with
all inline functions so that the whole class will be in the header file
(you’ll see in this book that I step over the line myself). During program
development this is probably harmless, although sometimes it can make for longer
compilations. Once the program stabilizes a bit, you’ll probably want to
go back and make functions non-inline where
appropriate.
Inlines & the compiler
To understand when inlining
is effective, it’s helpful to know what the
compiler does when it encounters an inline. As with any function, the compiler
holds the function type
(that is, the function prototype including the name and argument types, in
combination with the function return value) in its symbol table. In addition,
when the compiler sees that the inline’s function type and the
function body parses without error, the code for the function body is also
brought into the symbol table. Whether the code is stored in source form,
compiled assembly instructions, or some other representation is up to the
compiler.
When you make a call to an inline
function, the compiler first ensures that the call can be correctly made. That
is, all the argument types must either be the exact types in the
function’s argument list, or the compiler must be able to make a type
conversion to the proper types and the return value must be the correct type (or
convertible to the correct type) in the destination expression. This, of course,
is exactly what the compiler does for any function and is markedly different
from what the preprocessor does because the preprocessor cannot check types or
make conversions.
If all the function type information fits
the context of the call, then the inline code is substituted directly for the
function call, eliminating the call overhead and allowing for further
optimizations by the compiler. Also, if the inline is a member function, the
address of the object (this) is put in the appropriate place(s), which of
course is another action the preprocessor is unable to
perform.
Limitations
There are two situations in which the
compiler cannot perform inlining. In these cases, it simply reverts to the
ordinary form of a function by taking the inline definition and creating storage
for the function just as it does for a non-inline. If it must do this in
multiple translation units (which would normally cause a multiple definition
error), the linker is told to ignore the multiple definitions.
The compiler cannot perform inlining if
the function is too complicated. This depends upon the particular compiler, but
at the point most compilers give up, the inline probably wouldn’t gain you
any efficiency. In general, any sort of looping is considered too complicated to
expand as an inline, and if you think about it, looping probably entails much
more time inside the function than what is required for the function call
overhead. If the function is just a collection of simple statements, the
compiler probably won’t have any trouble inlining it, but if there are a
lot of statements, the overhead of the function call will be much less than the
cost of executing the body. And remember, every time you call a big inline
function, the entire function body is inserted in place of each call, so you can
easily get code bloat without
any noticeable performance improvement. (Note that some of the examples in this
book may exceed reasonable inline sizes in favor of conserving screen real
estate.)
The compiler also cannot perform inlining
if the address of the function
is taken implicitly or explicitly. If the compiler must produce an address, then
it will allocate storage for the function code and use the resulting address.
However, where an address is not required, the compiler will probably still
inline the code.
It is important to understand that an
inline is just a suggestion to the compiler; the compiler is not forced to
inline anything at all. A good compiler will inline small, simple functions
while intelligently ignoring inlines that are too complicated. This will give
you the results you want – the true semantics of a function call with the
efficiency of a macro.
Forward references
If you’re imagining what the
compiler is doing to implement inlines, you can confuse yourself into thinking
there are more limitations than actually exist. In particular, if an inline
makes a forward reference
to a function that hasn’t yet been declared in the
class (whether that function is inline or not), it can seem like the compiler
won’t be able to handle it:
//: C09:EvaluationOrder.cpp
// Inline evaluation order
class Forward {
int i;
public:
Forward() : i(0) {}
// Call to undeclared function:
int f() const { return g() + 1; }
int g() const { return i; }
};
int main() {
Forward frwd;
frwd.f();
} ///:~
In f( ), a call is made to
g( ), although g( ) has not yet been declared. This
works because the language definition states that no inline functions in a class
shall be evaluated until the closing brace of the class
declaration.
Of course, if g( ) in turn
called f( ), you’d end up with a set of recursive calls, which
are too complicated for the compiler to inline. (Also, you’d have to
perform some test in f( ) or g( ) to force one of them
to “bottom out,” or the recursion would be
infinite.)
Hidden activities in constructors & destructors
Constructors
and destructors
are two places where you can be
fooled into thinking that an inline
is more efficient than it
actually is. Constructors and destructors may have hidden activities, because
the class can contain subobjects whose constructors and destructors must be
called. These subobjects may be member objects, or they may exist because of
inheritance (covered in Chapter 14). As an example of a class with member
objects:
//: C09:Hidden.cpp
// Hidden activities in inlines
#include <iostream>
using namespace std;
class Member {
int i, j, k;
public:
Member(int x = 0) : i(x), j(x), k(x) {}
~Member() { cout << "~Member" << endl; }
};
class WithMembers {
Member q, r, s; // Have constructors
int i;
public:
WithMembers(int ii) : i(ii) {} // Trivial?
~WithMembers() {
cout << "~WithMembers" << endl;
}
};
int main() {
WithMembers wm(1);
} ///:~
The constructor for Member is
simple enough to inline, since there’s nothing special going on – no
inheritance or member objects are causing extra hidden activities. But in
class WithMembers there’s more going on than meets the eye. The
constructors and destructors for the member objects q, r, and
s are being called automatically, and those constructors and
destructors are also inline, so the difference is significant from normal member
functions. This doesn’t necessarily mean that you should always make
constructor and destructor definitions non-inline; there are cases in which it
makes sense. Also, when you’re making an initial “sketch” of a
program by quickly writing code, it’s often more
convenient to use inlines. But if you’re concerned
about efficiency, it’s a place to
look.
 |
|