"Be creates software platforms that enable rich media and web experiences on personal computers and Internet appliances."
You've probably seen statements similar to this many times during the last few months. In engineering terms, the last two words—"Internet appliances"—imply a new and significant constraint: memory footprint. This is because many Internet appliances have no hard disk. Rather, they have only a small, compact flash, usually with an IDE interface, and only a limited amount of memory. Typically, this means 16-32MB of main memory, and 8-16MB of storage space, at least for devices trying to be much more than a cellular phone, text-based Web browser. As you can imagine, swap space isn't even an option with devices such as these.
One consequence is that, more than ever, you need to be careful about memory usage. Unfortunately, this is often difficult to measure, since things can be shared by multiple teams or hidden by VM or heap fragmentation. Also, there are two types of memory allocation: visible and hidden. Visible memory allocation stores user data (like the buffer of a Bitmap, the messages of a MessageQueue, the points of a Polygon, and so on). Hidden memory allocation is used by BeOS objects and components themselves, so you don't really know what's there. And that's what this article is about—measuring the memory footprint of empty objects, or how much does it cost to set up a mechanism even before sending any data through it...
But enough talk, here are the numbers (tested under R4.5):
<less than 16 bytes range> 1 or 2 byte(s) per 1K for areas: VM maps each 4KB page using a 4 byte value. For very large areas, a second word may also be allocated for virtual page info (some are preallocated at boot time). <16 to 32 bytes range> creating aBMessenger
creating an emptyBPolygon
<32 to 64 bytes range> creating a port creating a semaphore with short names (<= 15 chars) creating aBMessageFilter
filtering a single command code creating aBFont
<64 to 128 bytes range> creating a semaphore with long names (<= 31 chars): as a general comment, long names are not free (no miracles here) creating aBHandler
creating an emptyBPicture
creating an emptyBMessageQueue
creating an emptyBShape
creating aBLocker
: please notice it's a bit more expensive than a semaphore. Not a big issue if you don't use a lot of them... creating an emptyBMessage
<128 to 256 bytes range> ~150 bytes: creating an emptyBRegion
<256 to 512 bytes range> ~350 bytes: creating a bitmap (not accepting view), not counting the buffer itself ~375 bytes: creating an area (not including the 4 bytes per 4K page previously discussed) <1KB to 2KB range> ~1600 bytes: creating a view ~1750 bytes: creating a view and setting a font context. As you can see, the font context on the server side is optional and gets added lazily. <16KB and more...> ~16 1/4 K: spawning a thread. At this point, the main hit is the kernel stack (16K of locked memory). ~20 1/4 K: spawning and resuming a thread. At this point, the thread starts to use another 4K page for the user stack. ~20 3/4 K: creating and running aBLooper
. That's essentially the cost of having a thread waiting to dispatch messages. ~32 K: creating a bitmap accepting view, not counting the buffer itself. Most of it is used by the thread on the server side handling the drawing commands. That's also the reason why you can't have too many bitmap accepting views (the app_server team runs out of thread). ~34 K: creating a window, without running the looper. This only creates one of the two threads required by a fully functional window. ~37 K: creating a TCP socket. This includes a server side thread and some buffers. ~56 K: creating a window and running the looper by hand (you don't need to do that usually). In this case, we effectively created both threads (client and server sides). The rest (around 15K) is used by the client and server side structs. ~70 K: creating a window, and showing it (outside the display area). As you see, the memory footprint of the window jumps again one more time. At this point the window is ready to go, but nothing is drawn on the screen yet... ~79 K: creating a new team (doing nothing, linking against none of the shared libraries except libroot). It's essentially: ~32K for the MMU tables ~20K for the thread ~16K for loader structures ~8K for team ~214 K: creating a new team with aBApplication
object, but doing nothing else but initialization. As expected, theBApplication
is a fairly respectable beast. Not very surprising, as it includes a couple of threads, needs a relocated copy of parts of libbe, and provides many services.
Remember, these numbers don't include whatever memory will be used by the objects once you start using them (and that can be a lot, depending on what you do).
If you've ever ported a large UNIX application to BeOS you may have run into the problem of building shared library versions of a set of interdependent static libraries. One way to do this is to build a shareable file for each library that contains only the exported symbols of that library. The symbol files can be used to link the other shared libraries in the set.
Here is a bash script that automates the process. It takes a list of .a
files and builds a corresponding .so
file for each one. The only thing
missing from this listing is the extract_symbols
function, which is
described later.
TMP_DIR=/tmp/so-builder.$$ GCC_ARGS=-nostart ARCHIVES= SYMBOLS= while [ $# != 0 ] do case "$1" in -*) GCC_ARGS="$GCC_ARGS $1";; /*.a) ARCHIVES="$ARCHIVES $1";; *.a) ARCHIVES="$ARCHIVES 'pwd'/$1";; *) GCC_ARGS="$GCC_ARGS $1";; esac shift done rm -rf $TMP_DIR mkdir $TMP_DIR pushd $TMP_DIR for arch in $ARCHIVES; do tail=${arch##*/} base=${tail%.a} extract_symbols $arch >$base.s soname="-Xlinker -soname=$base.so" echo gcc -nostdlib $soname -o $base.syms $base.s gcc -nostdlib $soname -o $base.syms $base.s SYMBOLS="$SYMBOLS $base.syms" done for arch in $ARCHIVES; do ar x $arch echo gcc $GCC_ARGS -o ${arch%.a}.so *.o $SYMBOLS gcc $GCC_ARGS -o ${arch%.a}.so *.o $SYMBOLS rm -f *.o done popd rm -rf $TMP_DIR
The while
loop scans the shell arguments, putting an absolute path to
each .a
file in $ARCHIVES
. The rest of the arguments are passed to the
gcc command which builds the .so
files.
In a temporary directory, the first for
loop builds a symbol file for
each archive by compiling the result of the extract_symbols
function. The
-soname argument to the linker provides the name of
the .so
file in which the symbols will ultimately be found.
The second for
loop relinks each archive into a shared library using
the symbol files just created.
The extract_symbols
function takes an archive and creates an assembler
file which contains only symbols. One way to do that is to use a sed
script on the output of nm:
extract_symbols () { nm $1 | sed -n ' s/^[0-9a-f]* T \(..*\)/\ .text\ .globl \1\ \1:/p s/^[0-9a-f]* D \(..*\)/\ .data\ .globl \1\ .size \1,4\ \1:/p s/^[0-9a-f]* C \(..*\)/\ .comm \1,4,4/p' }
In this week's installment I intend to put a certain small function in the Be API to rest. Although it was created with the best of intentions, there are several subtle problems with its use, so we're deprecating it. By describing these problems in detail, I hope to illustrate some of the pitfalls of C++ syntax that may be haunting your code.
Our victim for solemn interment today is BMessage
::BMessage
(BMessage
*).
Now, ever since the good old days, this function has lurked in the shadow
of its more hygienic partner, the standard BMessage
copy constructor:
BMessage
::BMessage
(const BMessage
&).
Recall that the copy constructor is called in either of the following two cases:
BMessage
a
;BMessage
b
(a
); // copy constructor specified explicitlyBMessage
c
=a
; // also calls the copy constructor, // NOT operator=() !
This works, looks nice, and is the C++ Approved Way of Doing Things. So
why have a redundant variant that takes a BMessage
*
instead of a const BMessage
&?
In a word, sloth. Consider the following code that uses the
copy constructor:
MyHandler
::MessageReceived
(BMessage
*msg
) {BMessage
tmpMsg
(*msg
); // do something with tmpMsg }
With BMessage
(BMessage
*) we could get away with this:
MyHandler
::MessageReceived
(BMessage
*msg
) {BMessage
tmpMsg
(msg
); // do something with tmpMsg }
As you can see, by using the BMessage
(BMessage
*) constructor, instead of
the widely acclaimed copy constructor, we get one really minimal gain --
we save an asterisk. (Note that dereferencing the pointer here and taking
a reference does not incur a performance penalty, as the compiler can
easily optimize it.) Nevertheless, since much of the Be API centers
around passing pointers to BMessage
s around, it seems the most natural
way to express the copying of one message to another. So, its use is
pandemic in both our code and—I suspect—in many of your apps as
well.
Now, where are the subtle problems I spoke of? First, there is the
question of constness. Since copying a BMessage
does not alter the
BMessage
, we should be able to pass a
const BMessage
* into the
constructor. Sadly, though, we overlooked this in the initial
implementation of the BMessage
API, and we can't fix it because of
backwards compatibility issues. So if you have a const BMessage
, you have
to work around this bug in the API by casting away the constness of your
BMessage
:
constBMessage
*a
;BMessage
b
(const_cast<BMessage
*>(a));
The C++-style cast I've used here looks ugly for a reason: a const_cast
is a potentially dangerous operation, and should not be invoked
carelessly. Because you're bypassing the compiler's enforcement of
constness, you have to blindly trust our claim that the BMessage
constructor doesn't modify the original message. Because the regular
BMessage
copy constructor handles const correctly, it's much easier to
use in this case.
Secondly, there are a couple of cases where a subtle error in your code will result in an unwanted side effect which is difficult to debug. First, there is the following code:
BMessage
msg
= newBMessage
;
In this case, I forgot to type an asterisk, and would hope that the compiler would catch the error. But not only does this code compile, it seems to run fine!
Why does this compile? It compiles because initialization invokes the copy constructor:
BMessage
msg
(newBMessage
);
There are two undesirable results of this code:
There is a redundant copy of data from the message on the heap
to msg
.
The pointer to the message you just created goes out of scope when the constructor exits, meaning that you've just leaked memory.
If this wasn't bad enough, things get even worse. There is also the possibility of unwanted implicit conversions.
As a quick review of implicit conversions, recall that a constructor with a single argument defines an implicit conversion from the argument type to the class type. For example, you can define a conversion from a double to a complex number in the following manner:
structComplex
{Complex
(doubleaReal
) : real(aReal
), imag(0.0) {} doublereal
; doubleimag
; };
Then, you can write the following:
double mag(Complexc
) { return sqrt(c
.real
*c
.real
+c
.imag
*c
.imag
); } doublea
; doubleaMag
= mag(a
);
because the double will automatically be converted into a Complex
object
for you.
Let's say we have the following function which takes a reference to
BMessage
and modifies the message:
voidFillMessage
(BMessage
&msg
) {msg
.AddString
("password", "metatron"); }
But when we use this function, we forget that it takes a reference, and we pass it a pointer instead:
BMessage
*myMsg
= newBMessage
;FillMessage
(myMsg
);
Again, this is probably a simple typo in your code, and you'd want this
code to refuse to compile. But before the compiler rejects this code, it
will look for some way to turn your BMessage
*
into a BMessage
&. It can't
do this directly, but it can turn a BMessage
*
into a BMessage
, and then
it can create a reference to a BMessage
from this resulting object. This
would be the same as writing:
BMessage
*myMsg
= newBMessage
;FillMessage
(BMessage
(myMsg
));
So, the code will compile and run with nary a complaint. The subtle and
dangerous problem with this conversion, as the latter piece of code
illustrates, is that you are creating a reference to a temporarily
constructed BMessage
, NOT the original message itself. FillMessage
ends
up modifying a temporary BMessage
that gets thrown away, and the original
message is left untouched. This is almost certainly not what you want!
Both this problem and the memory leak could be solved by declaring the
constructor to be explicit
. With the proper constness declared, this
would look something like:
classBMessage
{ public: explicitBMessage
(constBMessage
*); ... };
This prevents the constructor from being used in implicit conversions and
initialization—you have to call the constructor explicitly. However,
between these issues, the const
issue, and redundancy, we chose to
deprecate this constructor instead.
We're bound to run into questions, some teasing, and the odd bit of trouble. One question I'm often asked is how the name came to Be. (I forgot to mention feeble puns in the preceding list.) When Steve Sakoman and I started the company, I wanted to call it UT, as in the music note the Latin conjunction... and as in United Technoids. Steve was his usual diplomatic self, not voicing such thoughts as "here we go again." Instead, he offered to investigate other options by looking through the dictionary with his kids over the weekend.
The following Monday, I called Steve from the road. How did it go? We stopped at "be," he said. The letter "b," I asked? No, "to be," he replied. And that was it. We looked for conflicting names and, to our surprise, found none we thought might be troublesome. I fired up MacDraw II and typed the two letters of the logo in 72-pt. Times Roman. The spacing between the "B" and the "e" looked awful. However, by typing each letter as a separate object and using the left cursor key, I was able to arrange the letters just so, and we had our first logo.
As for name conflicts, we'd been a little too sanguine. One company, Better Education, used its initials publicly. We visited them and reached an amicable arrangement. We'd chosen Be Labs for our legal name, because it sounded good to us. Bell Labs agreed that it sounded good; in fact, they thought it sounded too close to their former, prestigious name. We deferred, particularly since we didn't want a misunderstanding at a time when we were working with AT&T's Hobbit microprocessor. So Be Incorporated became our legal name and we kept Be for our logo and "common" name.
Naturally, we want to protect the value of our name, of our brand. This is a lot easier with a name like Exxon or Hewlett-Packard than with a word as common as "Be." If you call your windows replacement business Better Windows, chances are the Redmond company will leave you alone. But if you start a Windows management software company, or, worse, a Windows management utility business, calling it Better Windows could get you into trouble. Apple, for example, has had no end of trouble dealing with the use of "Mac" in the names of products, services, magazines, newsletters, stores...
Popularity is not a bad problem to have though, and on a smaller scale, we see good news in the occasional problematic use of our name. (Osborne has no such trouble.) Recently, we came to an amiable arrangement with BeComputing. The name was obviously meant to indicate support of the Be platform, but there was a problem with the perception it could create. BeDepot is part of Be, Inc., but what about BeComputing? In fact, it's not affiliated with us.
Fortunately, Eric Schlissel, the CEO of BeComputing, understood our concern. That is, confusion, in one way or another, lowers the value of the brand. This, in turn, hurts everyone associated with the platform -- probably what sages mean when they speak of "brand equity." BeComputing will rename itself Geek Teknologies. We appreciate their understanding of our efforts as well as their support of the Be platform.
And now, off to Comdex.