37
Small Data Structures UNTITLED by Weir, Noble © 1999 Charles Weir, James Noble Page 1 Small Data Structures Version 13/06/00 02:15 - 33 by Charles Weir How can you reduce the memory needed for your data? The memory requirements of the data exceed the memory available to the system. You want to increase usability by allowing users to store as much of their data as possible. You need to be able to predict the program’s use of memory. You cannot delete some of the data from the program. The fundamental difference between code and data is that programmers care about code while users care about data. Programmers have some direct control over the size of their code (after all, they write it!), but the data size is often completely out of the programmers’control. Indeed, given that a system is supposed to store users’data, any memory allocated to code, buffers, or other housekeeping is really overhead as far as the user is concerned. Often the amount of memory available to users can make or break a systems usability a word processor which can store a hundred thousand word document is much more useful than one which are only store a hundred words. Data structures that are appropriate where memory is unrestricted may be far too prodigal where memory is limited. For example, a typical implementation of an address database might store copies of information in indexes as well as the actual data, effectively storing everything in the database twice. Porting such an approach to the Strap-It-On wrist-top PC would halve the number of addresses that could be stored in the database. Techniques like COMPRESSION and using SECONDARY STORAGE can reduce a program’s main memory requirements, but both have significant liabilities when used to manage the data a program needs to work on. Many kinds of compressed data cannot be accessed randomly; if random access is required the data must be uncompressed first, costing time, and requiring a large amount of buffer memory for the uncompressed data. Data stored on secondary storage is similarly inaccessible, and needs to be copied into main memory buffers before it can be accessed. Therefore: Choose the smallest structure that supports the operations you need. For any given data set there are many different possible data structures that might support it. Suppose, for example, you need an unordered collection of object references with no duplicates – in mathematical terms, a set. You could implement it using a linear array of pointers, using a hash table, or using a variety of tree structures. Most class libraries will provide several different implementations of such collections; the best one to choose depends on your requirements. Where memory is limited, therefore, you must be particularly careful to choose a structure to minimise the program’s memory requirements. You can think of data structure design as a three-stage process. First analyse the program’s requirements to determine the information the program needs to store; unnecessary information requires no memory! Second, analyse the characteristics of the data; what’s its total volume; how does it vary over a single program run and across different runs; and what’s its granularity – does it consist of a few large objects or many small objects? You can also analyse the way you’ll access the data: whether it is read and written, or only ever read; whether it is accessed sequentially or randomly; whether elements are inserted into the middle of the data or only added at the end.

Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

  • Upload
    others

  • View
    0

  • Download
    0

Embed Size (px)

Citation preview

Page 1: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Small Data Structures UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 1

Small Data StructuresVersion 13/06/00 02:15 - 33 by Charles Weir

How can you reduce the memory needed for your data?

• The memory requirements of the data exceed the memory available to the system.

• You want to increase usability by allowing users to store as much of their data aspossible.

• You need to be able to predict the program’s use of memory.

• You cannot delete some of the data from the program.

The fundamental difference between code and data is that programmers care about code whileusers care about data. Programmers have some direct control over the size of their code (afterall, they write it!), but the data size is often completely out of the programmers’ control.Indeed, given that a system is supposed to store users’ data, any memory allocated to code,buffers, or other housekeeping is really overhead as far as the user is concerned. Often theamount of memory available to users can make or break a systems usability — a wordprocessor which can store a hundred thousand word document is much more useful than onewhich are only store a hundred words.

Data structures that are appropriate where memory is unrestricted may be far too prodigalwhere memory is limited. For example, a typical implementation of an address database mightstore copies of information in indexes as well as the actual data, effectively storing everythingin the database twice. Porting such an approach to the Strap-It-On wrist-top PC would halvethe number of addresses that could be stored in the database.

Techniques like COMPRESSION and using SECONDARY STORAGE can reduce a program’s mainmemory requirements, but both have significant liabilities when used to manage the data aprogram needs to work on. Many kinds of compressed data cannot be accessed randomly; ifrandom access is required the data must be uncompressed first, costing time, and requiring alarge amount of buffer memory for the uncompressed data. Data stored on secondary storage issimilarly inaccessible, and needs to be copied into main memory buffers before it can beaccessed.

Therefore: Choose the smallest structure that supports the operations you need.

For any given data set there are many different possible data structures that might support it.Suppose, for example, you need an unordered collection of object references with no duplicates– in mathematical terms, a set. You could implement it using a linear array of pointers, using ahash table, or using a variety of tree structures. Most class libraries will provide severaldifferent implementations of such collections; the best one to choose depends on yourrequirements. Where memory is limited, therefore, you must be particularly careful to choosea structure to minimise the program’s memory requirements.

You can think of data structure design as a three-stage process. First analyse the program’srequirements to determine the information the program needs to store; unnecessary informationrequires no memory!

Second, analyse the characteristics of the data; what’s its total volume; how does it vary over asingle program run and across different runs; and what’s its granularity – does it consist of afew large objects or many small objects? You can also analyse the way you’ll access the data:whether it is read and written, or only ever read; whether it is accessed sequentially orrandomly; whether elements are inserted into the middle of the data or only added at the end.

Page 2: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Small Data Structures UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 2

Third, choose the data structures. Consider as many different possibilities as you can – yourstandard class libraries will provide some basic building blocks, but consider also options likeembedding objects (FIXED ALLOCATION), EMBEDDING POINTERS, or PACKING the DATA. For eachcandidate data structure design, work out the amount of memory it will require to store the datayou need, and check that it can support all the operations you need to perform. Then considerthe benefits and disadvantages of each design: for example a smaller data structure may requiremore processing time to access, provide insufficient flexibility or give insufficient real-timeperformance. You’ll need also to evaluate the resulting memory requirements for eachpossibility against the total amount of memory available – in some cases you may need to dosimple trials using scratch code. If none of the solutions are satisfactory you may need to goback and reconsider your earlier analysis, or even the requirements of the system as a whole.On the other hand there’s no need to optimise memory use beyond your given requirements (seethe THRESHOLD SWITCH pattern [Auer and Beck 1996]).

For example, the Strap-It-OnTM address program has enough memory to store the addressrecords but not indexes. So its version of the address program uses a sorted data structure thatdoes not need extra space for an index but that is slower to access than the indexed version.

ConsequencesChoosing suitable data structures can reduce a program’s memory requirements, and the timespent can increase the quality of the program’s design.

By increasing the amount of users’ information the program can store, careful data structuredesign can increase a program’s usability.

However: analysing a program’s requirements and optimising data structure design takes programmerdiscipline to do, and programmer effort and time to do well.

Optimising data structure designs to suit limited memory situations can restrict a program’sscalability should more memory become available.

The predictability of the program’s memory use, the testing costs, and the program’s timeperformance may or may not be affected, depending upon the chosen structures.

v v v

Implementation

Every data structure design for any program must trade off several fundamental forces:memory requirements, time performance, and programmer effort being the most important.Designing data structures for a system with tight memory constraints is no different in theoryfrom designing data structures in other environments, but the practical tradeoffs can result indifferent solutions. Typically you are prepared to sacrifice time performance and put in moreprogrammer effort than in an unconstrained system, in order to reduce the memoryrequirements of the data structure.

There are several particular issues to consider when designing data structures to minimisememory use:

1. Predictability versus Exhaustion

The predictability of a data structure’s memory use, and ultimately of the whole program canbe as important as the structure’s overall memory requirements, because making memory usemore predictable makes it easier to manage. Predictability is closely related to the need to dealwith memory exhaustion: if you can predict the program’s maximum memory use in advancethen you can use FIXED ALLOCATION to ensure the program will never run out of memory.

Page 3: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Small Data Structures UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 3

2. Space versus Flexibility

Simple, static, inflexible data structures usually require less memory than more complex,dynamic, and flexibly data structures. For example, you can implement a one-to-onerelationship with a single pointer or even an inline object (see FIXED ALLOCATION), while a one-to-many relationship will require collection classes, arrays, or EMBEDDED POINTERS. Similarly,flexible collection classes require more memory than simple fixed sized arrays, and objectswith methods or virtual functions require more memory than simple records (C++ structs)without them. If you don’t need flexibility, don’t pay for it; use simple data structures thatneed less memory.

3. Calculate versus Store

Often you can reduce the amount of main memory you need by calculating information ratherthan storing it. Calculating information reduces a program’s time performance and canincrease its power consumption, but can also reduce its memory requirements. For example,rather than keeping an index into a data structure, you can traverse the whole data structureusing a linear search. Similarly, the PRESENTER pattern [Vlissides 1998] describes howgraphical displays can be redrawn from scratch rather than being updated incrementally usinga complex object structure.

v v v

Specialised PatternsThis chapter contains five specialised patterns that describe a range of techniques for designingdata structures to minimise memory requirements. The following figure shows the relationshipsbetween the patterns.

ReferenceCounting

SecondaryStorage Compression

EmbeddedPointer

MultipleRepresentations

FixedAllocation

GarbageCollection

Packed Data Sharing Copy on Write

Data Structures MemoryAllocation

Figure 1: Data Structure Pattern Relationships

PACKED DATA selects suitable internal representations of the data elements in an object, toreduce its memory footprint.

SHARING removes redundant duplicated data. Rather than using multiple copies of functions,resources or data, the programmer can arrange to store only one copy, and use thatcopy wherever it is needed.

COPY-ON-WRITE extends SHARING so that shared objects can be modified without affecting otherclient objects that use the shared objects.

Page 4: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Small Data Structures UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 4

EMBEDDED POINTERS reduce the memory requirements for collection data structures, byeliminating auxiliary link objects and moving pointers into the data objects stored inthe structures.

MULTIPLE REPRESENTATIONS are effective when no single representation is simultaneouslycompact enough for storage yet efficient enough for actual use.

Known UsesLike Elvis, data structures are everywhere.

The classic example of designing data structures to save memory is the technique of allocatingonly two BCD digits to record the year when storing dates [Yourdon 2000]. This hadunfortunate consequences, although not the disasters predicted in the lead-up to the millennium[Berry, Buck, Mills, Stipe 1987]. Of course these data structure designs were often made forthe best of motives: in the 1960s disk and memory was much more expensive than it is today;and allocating two extra characters per record could cost millions.

An object-oriented database built using Smalltalk needed to be scaled up to cope with millionsof objects, rather than several thousand. Unfortunately, a back-of-the envelope calculationshowed that the existing design would require a ridiculous amount of disk space and thusbuffer memory. Examination of the database design showed that Smalltalk Dictionary (hashtable) objects occupied a large proportion of its memory; further investigation showed and thatthese Dictionaries contained only two elements: a date and a time. Redesigning the database touse Smalltalk Timestamp objects that stored a date and time directly, rather than the dictionary,reduced the number of objects needed to store each timestamp from at least eight to three, andmade the scaled-up database project feasible.

See alsoOnce you have designed your data structures, you then have to allocate the memory to storethem. The MEMORY ALLOCATION chapter presents a series of patterns describing how you canallocate memory in your programs.

In many cases, good data structure design alone is insufficient to manage your program’smemory. The COMPRESSION chapter describes how memory requirements can be reduced byexplicitly spending processor time to build very compact representations of data that generallycannot be used directly in computations. Moving less important data into SECONDARY STORAGE

and constant data into READ-ONLY MEMORY can reduce the demand for writable primary storagefurther.

There are many good books describing data structure design in depth. Knuth [1997] remains aclassic, though its examples are, effectively, in assembly language. Hoare [1972] is anotherseminal work, though nowadays difficult to find. Aho, Hopcroft and Ullman [1983] is astandard text for university courses, with examples in a Pascal-style pseudo-code, Cormen et al[1990] is a more in-depth Computer Science text, emphasising the mathematical analysis ofalgorithms. Finally Segewick’s series beginning with Algorithms [1988] provide a moreapproachable treatment, with editions quoting source code in different languages – for exampleAlgorithms in C++: Fundamentals, Data Structures, Sorting, Searching, [Segewick 1999]

Page 5: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Packed Data UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 5

Packed DataAlso known as: Bit Packing

How can you reduce the memory needed to store a data structure?

• You have a data structure (a collection of objects) that has significant memoryrequirements.

• You need fast random access to every part of every object in the structure.

• You need to store the data in these objects in main memory.

No matter what else you do in your system, sooner or later you end up having to design thelow-level data structures to hold the information your program needs. In an object-orientedlanguage, you have to design some key classes whose objects store the basic data and providethe fundamental operations on that data. In a program of any size, although there may be onlya few key data storage classes, there can be a large number of instances of these classes.Storing all these objects can require large amounts of memory, certainly much more thanstoring the code to implement the classes.

For example, the Strap-It-On’s Insanity-Phone application needs to store all of the names andnumbers in an entire local telephone directory (200,000 personal subscribers). All these namesand numbers should just about fit into the Strap-It-On’s memory, but would leave no room forthe program than displayed the directory, let alone any other program in the Wrist-Top PC.

Because these objects (or data structures) are the core of your program, they need to be easilyaccessible as your program runs. In a program of any complexity, the objects will need to beaccessed randomly (rather than in any particular order) and then updated. Taken together,random access with updating requires that the objects are stored in main memory.

You might consider using COMPRESSION on each object or on a set of objects, but this wouldmake processing slow and difficult, and makes random access to the objects using referencesalmost impossible. Similarly, moving objects into SECONDARY STORAGE is not feasible if theobjects need to be accessed rapidly and frequently. Considering the Insanity-Phone exampleagain, the data cannot be placed in the Strap-It-On’s secondary memory because that would betoo slow to access; and the data cannot be compressed effectively while maintaining randomaccess because each record is too small to compressed individually using standard adaptivecompression algorithms.

Therefore: Pack data items within the structure so that they occupy the minimum space.

There are two ways to reduce the amount of memory occupied by an object:

1. Reduce the amount of memory required by each field.

2. Reduce the amount of unused memory allocated between fields.

Consider each individual field in turn, and consider how much information that field reallyneeds to store. Then, chose the smallest possible representation for that information. This maybe the smallest suitable language level-data type, or even smaller, using different bits within,say, a machine word to encode different data items.

Once you have analysed each field, analyse the class as a whole to ensure that extra memory isnot allocated between fields. Compilers or assemblers often ensure that fields are aligned totake advantage of CPU instructions that make it easier to access aligned data, so, for example,all two-byte fields be stored at even addresses, and all four-byte fields at addresses that are

Page 6: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Packed Data UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 6

multiples of four. Aligning fields wastes the memory space between the end of one field and thestart of the next.

A

This is dead space and can't be used

The figure below shows how packing an object can almost halve the amount of memory that itrequires. The normal representation on the left allocates four bytes for each Boolean variable(presumably to use faster CPU instructions) and aligns two and four-byte variables to two orfour-byte boundaries; the packed representation allocates only one byte for each Booleanvariable and dispenses with alignment for longer variables.

Cha

r

Int

Boo

l

Sho

rt

Boo

l

Cha

r

Int

Boo

l

Sho

rtB

ool

Normal representation Packed representation

Considering the Insanity-Phone again, the designers realised that local phone books never covermore than 32 area codes – so each entry requires only 5 bits to store the area code. A seven-digit decimal number requires 24 bits. Surnames are duplicated many times, so Insanity-Phonestores each surname just once – an example of SHARING – and therefore gets less than 30,000unique names in each book; this requires 18 bits. Storing up to three initials (5 bits each – seeSTRING COMPRESSION) costs a further 15 bits. The total is 62 bits, and this can be stored in one64 bit long integer for each entry.

ConsequencesEach instance occupies less memory reducing the total memory requirements of the system,even though the same amount of data can be stored, updated, and accessed randomly.Choosing to pack one data structure is usually a local decision, with little global effects on theprogram as a whole.

However: The time performance of a system suffers, because CPUs are slower at accessingunaligned data. If accessing unaligned data requires many more instructions than aligned data,it can impact the program’s power consumption. More complicated packing schemes like bitpacking can have even higher overheads.

Packing data requires programmer effort to implement, produces less intuitive code which isharder to maintain, especially if you use non-standard data representations. More complicatedtechniques can increase testing costs.

Packing schemes that rely on particular aspects of a machine’s architecture, such as word sizesor pointer formats, will reduce portability. If you’re using non-standard internalrepresentations, it is harder to exchange objects with other programs that expect standardrepresentations.

Finally, packing can reduce scalability, because it can be difficult to unpack data structuresthroughout a system if more memory becomes available.

v v v

Page 7: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Packed Data UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 7

ImplementationThe default implementation of a basic type is usually chosen for time performance rather thanspeed. For example, Boolean variables are often allocated as much space as integer variables,even though they need only a single bit for their representation. You can pack data by choosingsmaller data types for variables; for example, you can represent Booleans using single byteintegers or bit flags, and you may be able to replace full-size integers (32 or 64 bits) with 16 oreven 8-bit integers (C++’s short and char types).

Compilers tend to align data members on machine-word boundaries which wastes space (seethe figure on p.N above). Rearranging the order of the fields can minimise this padding, andcan reduce the overhead when accessing non-aligned fields. A simple approach is to allocatefields within words in decreasing order of size.

Because packing has significant overheads in speed and maintainability, it is not worthwhileunless it will materially reduce the program’s memory requirements. So, pack only the datastructures that consume significant amounts of memory, rather than packing every classindiscriminately.

Here are some other issues to consider when applying the PACKED DATA pattern.

1. Compiler and Language Support

Compilers, assemblers and some programming language definitions support packing directly.

Many compilers provide a compilation flag or directive that ensures all data structures use thetightest alignment for each item, to avoid wasted memory at the cost of slower run-timeperformance. Microsoft C++, the directive:

#pragma pack( n )

sets the packing alignment to be based on n-byte boundaries, so pack(1) gives the tightestpacking; the default packing 8 [Microsoft 1997]. G++ provides a pack attribute for individualfields to ensure they are allocated directly after the preceding field [Stallman 1999].

2. Packing Objects into Basic Types

Objects can impose a large memory overhead, especially when they contain only a smallamount of data. Java objects impose an allocation overhead of at least one an additionalpointer, and C++ objects with virtual functions require a pointer to a virtual function table.You can save memory by replacing objects by more primitive types (such as integers orpointers), an example of MULTIPLE REPRESENTATIONS.

When you need to process the data, wrap each primitive type in a first-class object, and use theobject to process the data; when you’ve completed processing, discard the object, recover thebasic type, and store it once again. To avoid allocating lots of wrapper objects, you can reusethe same wrapper for each primitive data item. The following Java code sketches how aBigObject can be repeatedly initialised from an array of integers for processing. The becomemethod reinitialises a BigObject from its argument, and the process method does the work.

BigObject obj = new BigObject(0);

for (int i=1; i<10; i++) { obj.become(bigarray[i]); obj.process(); }

In C++, we can define operators to convert between objects and basic types, so that the twocan be used interchangeably:

Page 8: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Packed Data UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 8

class BigIntegerObject{public:

BigIntegerObject( int anInt=0 ) : i( anInt ) {}operator int() { return i; }

private:int i;

};

int main(){

BigIntegerObject i( 2 ), j( 3 );BigIntegerObject k = i*j; // Uses conversion operators

3. Packing Pointers using Tables

Packing pointers is more difficult, because they don’t obviously contain redundant data. Topack pointers you need to look at what they reference.

If a given pointer may point to only one of a given set of then it may be possible to replace thepointer with an index into an array; often, an array of pointers to the original item. Since thesize of the array is usually much less than the size of all the memory in the system, the numberof bits needed for an array index can be much less than the number of bits needed for a generalpointer.

index:[0..arraySize]

012345

etc.

Item

Item

Item

Item

Uses

Take, for example, the Insanity-phone application above. Each entry apparently needs apointer to the corresponding surname string: 32 bits, say. But if we implement an additionalarray of pointers into the string table, then each entry need only store an index into this array(16 bits). The additional index costs memory: 120Kb using 4-byte pointers.

If you know that each item is guaranteed to be within a specific area of memory, then you canjust store offsets within this memory. This might happen if you’ve built a string table, or ifyou’re using POOLED ALLOCATION, or VARIABLE ALLOCATION within a heap of known size. Forexample, if all the Insanity-Phone surname strings are stored in a contiguous table (requiringless than 200K with STRING COMPRESSION), the packed pointer needs only hold the offset fromthe start of the table: 18 bits rather than 32.

4. Packing Pointers using Bitwise Operations

If you are prepared to sacrifice portability, and have an environment like C++ that allows youto manipulate pointers as integers, then you have several possible ways to pack pointers. Insome architectures, pointers contain redundant bits that you do not need to store. Long pointersin the 8086 architecture had at least 8 redundant bits, for example, so could be stored in threebytes rather than four.

You can further reduce the size of a pointer if you can use knowledge about the heap allocationmechanism, especially about alignment. Most memory managers allocate memory blocksaligned on word boundaries; if this is an 8-byte boundary, for example, then you can know thatany pointer to a heap object will be a multiple of eight. In this case, the lowest three bits of

Page 9: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Packed Data UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 9

each pointer are redundant, and can be reused or not stored. Many garbage collectors, forexample, pack tag information into the low bits of their pointers [Jones and Lins 1996].

ExampleConsider the following simple C++ class:

class MessageHeader { bool inUse; int messageLength; char priority; unsigned short channelNumber; bool waitingToSend;};

With 8-byte alignment, this occupies 16 bytes, using Microsoft C++ on Windows NT. With thecompiler packing option turned on it occupies just 9 bytes. Note that the packed structure doesnot align the integer i1 to a four-byte boundary, so on some processors it will take longer toload and store.

Even without compiler packing, we can still improve the memory use just by reordering thedata items within the structure to minimise the gaps. If we sort the fields in decreasing order ofsize

class ReorderedMessageHeader { int messageLength; unsigned short channelNumber; char priority; bool inUse; bool waitingToSend;};

the class occupies just 12 bytes, a saving of four bytes. If you’re using compiler field packing,both MessageHeader and ReorderedMessageHeader occupy 9 bytes, but there’s still abenefit to latter since it puts all the member items on the correct machine boundaries wherethey can be manipulated fast.

We can optimise the structure even more using bitfields. The following version contains thesame data as before:

class BitfieldMessageHeader { int messageLength; unsigned channelNumber: 16; unsigned priority: 8; unsigned inUse: 1; unsigned waitingToSend: 1;

public: bool IsInUse() { return inUse; } void SetInUseFlag( bool isInUse ) { inUse = isInUse; } char Priority() { return priority; } void SetPriority( char newPriority ) { priority = newPriority; } // etc.};

but occupies just 8 bytes, a further saving of four bytes – or one byte if you’re using compilerpacking.

Unfortunately compiler support for booleans in bitfields tends to be inefficient. This problemisn’t actually a sad reflection on the quality of C++ compiler writers today; the real reason isthat it requires a surprising amount of code to implement the semantics of, say, the setB1function above. We can improve performance significantly by using bitwise operations insteadof bitfields, and implement the member functions directly to expose these operations:

Page 10: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Packed Data UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 10

class BitwiseMessageHeader { int messageLength; unsigned short channelNumber; char priority; unsigned char flags;public: enum FlagName { InUse = 0x01, WaitingToSend = 0x02 }; bool GetFlag( FlagName f ) { return (flags & f) != 0; } void SetFlag( FlagName f ) { flags |= f; } void ResetFlag( FlagName f ) { flags &= ~f; }};

This optimises performance, at the cost of exposing some of the implementation.

v v v

Known UsesPacked data is ubiquitous in memory-limited systems. For example virtually all Booleans inthe EPOC system are stored as bit flags packed into integers. The Pascal language standardincludes a special PACKED data type qualifier, used to implement the original Pascal compiler.

To support dynamic binding, a C++ object normally requires a vtbl pointer to support virtualfunctions [Stroustrup 1995, Stroupstrup 1997]. EPOC requires dynamic binding to supportMultiple Representations for its string classes, but a vtbl pointer would impose a four bytesoverhead on every string. The EPOC string base class (TDesC) uses the top 4 bits of its ‘stringlength’ data member to identify the class of each object:

class TDesC8 { private:unsigned int iLength:28;unsigned int iType:4;/* etc... */

Dynamically bound functions that depend on the actual string type are called from TDesC usinga switch statements on the value of the iType bitfield.

Bit array classes are available in both the C++ Standard Template Library and the JavaStandard Library. Both implement arrays of bits using array of machine words. Goodimplementations of the C++ STL also provide a template specialisation to optimise the specialcase of an array of Booleans by using a bitset [Stroustrup 1997, Chan et al 1998].

Java supports object wrapper versions for many primitive types (Integer, Float). Programmerstypically use the basic types for storage and the object versions for complicated operations.Unfortunately Java collections store objects, not basic types, so every basic type must bewrapped before it is stored into a collection [Gosling, Joy, Steele 1996]. To avoid storingmultiple copies of the same information the Palm Spotless JVM carefully shares whateverobjects it can, such as constant strings defined in different class files [Taivalsaari et al 1999].

See AlsoEMBEDDED POINTERS provides a way to limit the space overhead of collections and similar datastructures. FIXED ALLOCATION and POOLED ALLOCATION provide ways to reduce any additionalmemory management overhead.

Packing string data often requires STRING COMPRESSION.

The VISITOR and PRESENTER [Vlissides 1996] patterns can provide behaviour for collections ofprimitive types (bit arrays etc.) without having to make each basic data item into an object.The FLYWEIGHT PATTERN [Gamma et al 1995] allows you to process each item of a collection ofpacked data within the context of its neighbours.

Page 11: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Sharing UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 11

SharingAlso Known As: Normalisation.

How can you avoid multiple copies of the same information?

• The same information is repeated multiple times.

• Very similar objects are used in different components.

• The same functions can be included in multiple libraries

• The same literal strings are repeated throughout a program.

• Every copy of the same information uses memory.

Sometimes the same information occurs many times throughout a program, increasing theprogram’s memory requirements. For example, the Strap-It-On user interface design includesmany icons showing the company’s bespectacled founder. Every component displaying theicon needs to have it available, but every copy of that particular gargoyle wastes memory.

Duplication can also enter the program from outside. Data loaded from RESOURCE FILES orfrom an external database must be recreated inside a program, so loading the same resource ordata twice (possibly in different parts of the program) will also result in two copies of the sameinformation.

Copying objects has several benefits. Architecturally, it is important that components takeresponsibility for the objects they use, so copying objects between components can simplifyownership relationships. Some language constructs (such as C++ value semantics andSmalltalk cloning) assume object copying; and sometimes copying objects to where they arerequired can avoid indirection, making systems run faster.

Unwanted duplication doesn’t just affect data objects. Unless care is taken, every time aseparately compiled or built component of the program uses a library routine, a copy of thatroutine will be incorporated into the program. Similarly every time a component uses a stringor a constant a copy of that string may be made and stored somewhere.

Unfortunately, for whatever reason information is duplicated, every copy takes up memory thatcould otherwise be put to better use.

Therefore: Store the information once, and share it everywhere it is needed.

Analyse your program to determine which information is duplicated, and which informationcan be safely shared. Any kind of information can be duplicated — images, sounds, multimediaresources, fonts, character tables, objects, and functions, as well as application data.

Once you have found common information, check that it can be shared. In particular, ensurethat it never needs to be changed, or that all its clients can cope whenever it is changed.Modify the information’s clients so that they all refer to a single shared copy of theinformation, typically by accessing the information through a pointer rather than directly.

If the shared information can be discarded by its clients, you may need to use REFERENCE

COUNTING or GARBAGE COLLECTION so that it is only released once it is no longer neededanywhere in the program. If individual clients may want to change the data, you may need touse COPY-ON-WRITE.

For example, the Strap-It-On PC really only needs one copy of Our Founder’s bitmap. Thisbitmap is never modified, so a single in-memory copy of the bitmap is shared everywhere it isneeded in the system.

Page 12: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Sharing UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 12

ConsequencesJudicious sharing can reduce a program’s memory requirements, because only one copy isrequired of a shared object. SHARING also increases the quality of the design, since there’s lesschance of code duplication. SHARING generally does not affect a program’s scalability whenmore memory is made available to the system, nor its portability. Since there’s no need toallocate space for extra duplicate copies, SHARING can reduce start-up times, and to a lesserextent run-time performance.

However: programmer effort and discipline, and team co-ordination is required to design programs totake advantage of sharing. Designing sharing also increases the complexity of the resultingsystem, adding to maintenance and testing costs since shared objects create interactionsbetween otherwise independent components.

Although it does not affect the scalability of centralised systems, sharing can reduce thescalability of distributed systems, since it can be more efficient to make one copy of eachshared object for each processor.

Sharing can introduce many kinds of aliasing problems, especially when read-only data ischanged accidentally [Hogg 1991, Noble et al 1998], and so can increase testing costs. Ingeneral, sharing imposes a global cost on programs to achieve local goals, as manycomponents may have to be modified to share a duplicated data structure.

v v v

ImplementationSharing effectively changes one-to-one (or one-to-many) relationships into many-to-one (ormany-to-many) relationships. Consider the example below, part of a simple word processor, inUML notation [Fowler 1997]. A Document is made up of many Paragraphs, and eachParagraph has a ParagraphFormat.

Document

Paragraph ParagraphFormat

*

1

1 1

Considering each class in turn, it is unlikely that Documents or Paragraphs will be duplicated,unless, for example, many documents have many identical paragraphs. Yet many paragraphswithin a document will have the same format. This design, however, gives each Paragraphobject has its own ParagraphFormat object. This means that the program will contain manyParagraphFormat objects (one for each Paragraph) whilst many of these objects will haveexactly the same contents. ParagraphFormats are obvious candidates for sharing betweendifferent Paragraphs.

We can show this sharing as a one-to-many relationship.

Page 13: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Sharing UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 13

ParagraphParagraph

Format* 1

Document

*

1

In the revised design, there will only be a few ParagraphFormat objects, each one different,and many Paragraphs will share the same ParagraphFormat object.

The design activity of detecting repeated data and splitting it into a separate object is callednormalisation. Normalisation is an essential part of relational database design theory, andboasts an extensive literature [Connolly and Begg 1999; Date 1999; Elmasri and Navathe2000].

You should consider the following other issues when applying the SHARING pattern.

1. Making Objects Shareable

Aliasing problems make it difficult to share objects in object-oriented programs [Hogg 1991,Noble, Vitek, Potter 1998]. Aliasing problems are the side effects caused by changing a sharedobject: if a shared object is changed by one of its clients the change will affect any other clientof the shared object, and such changes can cause errors in clients that do not expect them. Forexample, changing the font in a shared Paragraph Format object will change the fonts for allParagraphs that share that format. If the new font is, say, a printer-only font, and is changed tosuit one particular paragraph that will never be displayed on screen, it will break otherparagraphs using that format which do need to be displayed on screen, because a printer-onlyfont will not work for them.

The only kinds of objects that can be shared safely without side effects are immutable objects,objects that can never be changed. The immutability applies to the object itself – it’s notenough just to make some clients read-only, since other, writing, clients may still change theshared object ‘behind their back’. To share objects safely you typically have to change clientsso that they make new objects rather than changing existing ones (see COPY-ON-WRITE). Youshould consider removing any public methods or fields that can be used to change sharedobjects’ state: in general, every field should only be initialised in the object’s constructor (inJava, all fields should be final). The FLYWEIGHT pattern [Gamma 1995] can be used to movedynamic state out of shared objects and into their clients.

2. Establishing Sharing

For two or more components to be able to share some information, each component must beable to find the information that is shared.

In small systems, where only a few distinguished objects are being shared, you can often use aglobal variable to store each shared object, or store shared instances within the objects’ classesusing the SINGLETON pattern [Gamma 1995]. The shared global variables can be initialisedstatically when the program starts, or the first time a shared objects is accessed using LAZY

INITIALISATION [Beck 1997].

To provide a more general mechanism you can implement a shared cache, an in-memorydatabase mapping from keys to shared objects. To find a shared object, a component checksthe cache. If the shared object is already in the cache you use it directly; if not you create the

Page 14: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Sharing UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 14

object and store it back into the cache. For this to work you need a unique key for each sharedobject to identify it in the cache. A shared cache works particularly well when severalcomponents are loading the same objects from resource files databases, or networks like theworld wide web. Typical keys could be the fully qualified file names, web page URLs, or acombination of database table and database key within that table – the same keys that identifythe data being loaded in the first place.

3. Deleting Shared Objects

Once you’ve created shared objects, you may need to be able to delete them when no longerrequired.

There are three standard approaches to deleting shared objects: REFERENCE COUNTING, GARBAGE

COLLECTION and object ownership. REFERENCE COUNTING keeps a count of the number ofobjects interested in a shared object; when this becomes zero the object can be released (byremoving the reference from the cache and, in C++, deleting the object). A GARBAGE

COLLECTOR can detect and remove shared objects without requiring reference counts. Note thatif a shared object is accessed via a cache, the cache will always have a reference to the sharedobject, preventing the garbage collector from deleting it, unless you can use some form of weakreference [Jones and Lins 1996].

With object ownership, you can identify one other single object or component that has theresponsibility of managing the shared object (see the SMALL ARCHITECTURE pattern). Theobject’s owner accepts the responsibility of deleting the shared object at the appropriate time;generally it needs to be an object with an overview of all the objects that ‘use’ the shared object[Weir 1996, Cargill 1996].

4. Sharing Literals and Strings

In many programs literals occupy more space than variables, so you can assign often usedliterals to variables and then replace the literals by the variables, effectively SHARING one literalin many places. For example, LaTeX uses this technique, coding common literals such as one,two, and minus one as the macros ‘\@ne’, ‘\tw@’, and ‘\m@on’. Smalltalk shares literals aspart of the language environment, by representing strings as ‘symbols’. A symbol represents asingle unique string, but can be stored internally as if it were an integer. The Smalltalk systemmaintains a ‘symbol table’ that maps all known symbols to the strings they represent, and thecompilation and runtime system must search this table to encode each string as a symbol,potentially adding a new entry if the string has not been presented before. The Smalltalkenvironment uses symbols for all method names, which both compresses code and increases thespeed of method lookup.

5. Sharing across components and processes

It’s more difficult to implement sharing between several components in different addressspaces. Most operating systems provide some kind of shared memory, but this is often difficultto use. In concurrent systems, you need to prevent one thread from modifying shared data whileanother thread is accessing it. Typically this requires at least one semaphore, and increasescode complexity and testing cost.

Alternatively, especially when data is shared between many components, you can considerencapsulating the shared data in a component of its own and use client-server techniques toaccess it. For example, EPOC accesses its relational databases through a single ‘databaseserver’ process. This server keeps a cache of indexes for its open databases; if twoapplications use the same database they share the same index.

Page 15: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Sharing UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 15

ExampleThis Java example outlines part of the simple word processor described above. Documentscontain Paragraphs, each of which has a ParagraphFormat. ParagraphFormats arecomplex objects, so to save memory several Paragraphs share a single ParagraphFormat.The code shows two mechanisms to ensure this:

• When we duplicate a Paragraph, both the original and the new Paragraph share asingle ParagraphFormat instance.

• ParagraphFormats are referenced by name, like “bold”, “normal” or “heading 2”. ASingleton ParagraphFormatCatalog contains a map of all the names toParagraphFormat objects, so when we request a ParagraphFormat by name, theresult is the single, shared, instance with that name.

The most important class in the word processor is Document: basically a sequence ofParagraphs, each of which as a (shared) ParagraphFormat.

class Document { Vector paragraphs = new Vector(); int currentParagraph = -1;

The Paragraph class uses a StringBuffer to store the text of the paragraph, and also storesa reference to a ParagraphFormat object.

class Paragraph implements Cloneable { ParagraphFormat format; StringBuffer text = new StringBuffer();

A new Paragraph can be constructed either by giving a reference to a format object (which isthen stored, without being copied, as the new Paragraph’s format) or by giving a format name,which is then looked up in the ParagraphFormatCatalog. Note that neither initialising oraccessing a paragraph’s format copies the ParagraphFormat object, rather it is passed byreference.

Paragraph(ParagraphFormat format) { this.format = format; }

Paragraph(String formatName) { this(ParagraphFormatCatalog.catalog().findFormat(formatName)); }

ParagraphFormat format() {return format;}

Paragraphs are copied using the clone method (used by the word-processor to implement itscut-and-paste feature). The clone method only copies one object, so the new clone’s fieldsautomatically point to exactly the same objects as the old object’s fields. We don’t want aParagraph and its clone to share the StringBuffer, so we must clone that explicitly andinstall the cloned StringBuffer into the cloned Paragraph; however we don’t want to clonethe ParagraphFormat reference, because ParagraphFormats can be shared.

public Object clone() { try { Paragraph myClone = (Paragraph) super.clone(); myClone.text = new StringBuffer(text.toString()); return myClone; } catch (CloneNotSupportedException ex) { return null; } }

Paragraphs find their formats using the ParagraphFormatCatalog, The catalog is aSINGLETON [Gamma et al 1995].

Page 16: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Sharing UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 16

class ParagraphFormatCatalog { private static ParagraphFormatCatalog systemWideCatalog = new ParagraphFormatCatalog(); public static ParagraphFormatCatalog catalog() { return systemWideCatalog; }

that implements a map from format names to shared ParagraphFormat objects:Hashtable theCatalog = new Hashtable(); public void addNewNamedFormat(String name, ParagraphFormat format) { theCatalog.put(name,format); }

public ParagraphFormat findFormat(String name) { return (ParagraphFormat) theCatalog.get(name); }}

Since the ParagraphFormat objects are shared, we want to restrict what the clients can dowith them. So ParagraphFormat itself is just an interface that does not permit clients tochange the underlying object.

interface ParagraphFormat { ParagraphFormat nextParagraphFormat(); String defaultFont(); int fontSize(); int spacing();}

The class ParagraphFormatImplementation actually implements the ParagraphFormatobjects, and includes a variety of accessor methods and constructors for these variables:

class ParagraphFormatImplementation implements ParagraphFormat { String defaultFont; int fontSize; int spacing; String nextParagraphFormat;

Each ParagraphFormat object stores the name of the ParagraphFormat to be used for thenext paragraph. This makes it easier to initialise the ParagraphFormat objects, and will givethe correct behaviour if we replace a specific ParagraphFormat in the catalogue with another.

To find the corresponding ParagraphFormat object, it must also refer to the cataloguepublic ParagraphFormat nextParagraphFormat() { return ParagraphFormatCatalog.catalog(). findFormat(nextParagraphFormat);}

When the Document class creates a new paragraph, it use the shared ParagraphFormatreturned by the format of the current paragraph: Note that ParagraphFormat objects arenever copied, so the will be shared between all paragraphs that have the same format.

public Paragraph newParagraph() { ParagraphFormat nextParagraphFormat = currentParagraph().format().nextParagraphFormat(); Paragraph newParagraph = new Paragraph(nextParagraphFormat); insertParagraph(newParagraph); return newParagraph; }

v v v

Known UsesJava’s String instances are immutable, so implementations share a single underlying bufferbetween any number of copies of the same String object [Gosling et al 1996]. And allimplementations of Smalltalk use tokens for pre-compiled strings, as discussed above.

Page 17: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Sharing UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 17

C++’s template feature often generate many copies of very similar code, leading to ‘codebloat’. Some C++ linkers detect and share such instances of duplicated object code –Microsoft, for example, call this ‘COMDAT folding’ [Microsoft 1997]. Most modernoperating systems provide Dynamic Link Libraries (DLLs) or Shared libraries that allowdifferent processes to use the same code without needing to duplicate it in every executable[Kenah and Bate 1984; Card et al1998].

The EPOC Font and Bitmap server stores font and image data loaded from RESOURCE FILES inshared memory [Symbian 1999]. These are used both by applications and by the WindowServer that handles screen output for all applications. Each client requests and releases thefont and bitmap data using remote procedure calls to the server process; the server loads thedata into shared memory or locates an already-loaded item, and thereafter the application canaccess it directly (read-only). The server uses reference counting to decide when to delete eachitem; an application will normally release each item explicitly but the EPOC operating systemwill also notify the server if the application terminates abnormally, preventing memory leaks.

See AlsoSHARING was first described as a pattern in the Design Patterns Smalltalk Companion [Alpert,Brown, Woolf 1998].

COPY-ON-WRITE provides a mechanism to change a shared object as seen by one object, withoutimpacting any other objects that rely on it. The READ-ONLY MEMORY pattern describes how youcan ensure that objects supposed to be read-only cannot be modified. Shared things are oftenREAD-ONLY, and so often end up stored on SECONDARY STORAGE.

The FLYWEIGHT PATTERN [Gamma et al 1995] describes how to make objects read-only so thatthey can be shared safely. Objects may also need to be moved into a SINGLE PLACE formodelling reasons [Noble 1997]. Ken Auer and Kent Beck [1996] describe techniques to avoidsharing Smalltalk objects by accident.

Page 18: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Copy-on-Write UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 18

Copy-on-WriteHow can you change a shared object without affecting its other clients?

• You need the system to behave as if each client has its own mutable copy of someshared data.

• To save memory you want to share data, or

• You need to modify data in Read-Only Memory.

Often you want the system to behave as though there are lots of copies of a piece of shareddata, each individually modifiable, even though there is only one shared instance of the data.For example, in the Word-O-Matic word processor, each paragraph has its own format, whichusers can change independently of any other paragraph. Giving every paragraph its ownParagraphFormat object ensures this flexibility, but duplicates data unnecessarily becausethere are only a few different paragraph formats used in most documents.

We can use the SHARING pattern instead, so that each paragraph format object describes severalparagraphs. Unfortunately, a change to one shared paragraph format will change all the otherparagraphs that share that format, not just the single paragraph the user is trying to change.

There’s a similar problem if you’re using READ-ONLY MEMORY (ROM). Many operating systemsload program code and read-only data into memory marked as ‘read-only’, allowing it to beshared between processes; In palmtops and embedded systems the code may be loaded intoROM or flash RAM. Clients may want changeable copies of such data; but making anautomatic copy by default for every client will waste memory.

Therefore: Share the object until you need to change it, then copy it and use the copy in future.

Maintain a flag or reference count in each sharable object, and ensure it’s set as soon as there’smore than one client to the object. When a client calls any method that modifies a sharedobject’s externally visible state, create a duplicate of some or all of the object’s state in a newobject, delegate the operation to that new object, and ensure that the the client uses the newobject from then on. The new object will initially not be shared (with flag unset or referencecount of one), so further modifications won’t cause a copy until the new object in turn then getsmultiple clients.

You can implement COPY-ON-WRITE for specific objects in the system, or implement it as part ofthe operating system infrastructure using PAGING techniques. The latter approach isparticularly used with code, which is normally read-only but allows a program to modify itsown code on occasion, in which case a paging system can make a copy of part of the code forthat specific program instance.

Thus Word-O-Matic keeps a reference count of the number of clients sharing eachParagraphFormat object. In normal use many Document objects will share the sameParagraphFormat, but on the few occasions that a user modifies the format of a paragraph,Word-O-Matic makes a copy of its ParagraphFormat and keeps that separate to the modifiedParagraph and to any other Paragraphs with the new format.

ConsequencesCOPY-ON-WRITE gives programmers the illusion of many copies of a piece of data, without thewaste of memory that would imply. So it reduces the memory requirements of the system. Insome cases it increases a program’s execution speed, and particularly its start-up time, sincecopying can be a slow operation.

Page 19: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Copy-on-Write UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 19

COPY-ON-WRITE also allows you to make it appear that data stored in read-only storage can bechanged. So you can move infrequently changed data into read-only storage, reducing theprogram’s memory requirements.

Once COPY-ON-WRITE has been implemented it requires little programmer discipline to use,since clients don’t need to be directly aware of it.

However: COPY-ON-WRITE requires programmer effort or hardware or operating system support toimplement, because the system must intercept writes to the data, make the copy and thencontinue the write as if nothing had happened.

If there are many write accesses to the data, then COPY-ON-WRITE can decrease timeperformance, since each write access must ensure the data’s not shared. COPY-ON-WRITE canalso lead to lots of copies of the same thing cluttering up the system, decreasing thepredictability of the system’s performance, making it harder to test, and ultimately increasingthe system’s memory requirements.

COPY-ON-WRITE can cause problems for object identity if the identity of the copy and theoriginal storage is supposed to be the same.

v v v

ImplementationHere are some issues to consider when implementing the COPY-ON-WRITE pattern.

1. Copy-On-Write Proxies

The most common approach to implementing COPY-ON-WRITE is to use a variant of the PROXY

Pattern [Gamma et al 1995, Buschmann et al 1996, Coplien 1994]. Using the terminology ofBushman et al [1996], a PROXY references an underlying Original object and forwards all themessages it receives to that object.

To use PROXY to implement COPY-ON-WRITE, every client uses a different PROXY object, whichdistinguishes accessors, methods that merely read data, from mutators that modify it. TheOriginal Object contains a flag that records whether it has more than one Proxy sharing it.Any updator method checks this flag and if the flag is set, makes a (new, unshared) copy of therepresentation, installs it in the proxy, and forwards the mutator to the that copy instead.

mutate()access()

Proxyoriginal

11..*

access()mutate()

Original clone()

shared: bool

Original

Client Uses

if (original.shared) original = original.clone()original.mutate()

original.access()

In this design, the shared flag is stored in the Original object, and it’s the responsibility of therepresentation’s clone method to create an object with the flag unset. A valid alternativeimplementation is to place the flag into the Proxy object; in this case the Proxy must reset theflag after creating and installing a new Original object. As a third option, you can combine theClient and Proxy object, if the Client knows about the use of COPY-ON-WRITE, and if no otherobjects need to use the Original (other than via the combined Client/Proxy, of course).

Page 20: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Copy-on-Write UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 20

You can also combine the function of COPY-ON-WRITE with managing the lifetime of the underlyingrepresentation object by replacing the shared flag in the Representation object with a reference count.A reference count of exactly one implies the object is not shared and can be modified. See theREFERENCE COUNTING pattern for a discussion of reference counting in detail.

2. Copying Changes to Objects

You do not have to copy all of any object when it is changed. Instead you can create a ‘delta’object that stores only the changes to the object, and delegates requests for unchanged databack to the main object. For example, when a user changes the font in a paragraph format, youcan create a FontChange delta object that returns the new font when it is asked, but forwardsall other requests to the underlying, and unchanged, ParagraphFormat object. A delta objectcan be implement as a DECORATOR on the original object [Gamma et al 1995]. The diagrambelow uses UML shows a possible implementation as a UML Collaboration Diagram [Fowler1997].

Before:

: ParagraphFormat Proxy

: ParagraphFormat1. Font

2. Spacing1.1. Font

2.1 Spacing

: ParagraphFormat Proxy1. Font

2. Spacing1.1. Font

2.1 Spacing

: Font Change : ParagraphFormat

2.1.1 Spacing

After:

3. Writing to Objects in Read-Only Memory

You can use COPY-ON-WRITE so that shared representations in ROM can be updated. Clearlythe shared flag must be set in the ROM instance and cleared in the copy, but otherwise this isno different from a RAM version of the pattern.

In C++ the rule is that only instances of classes without a constructor may be placed in ROM.So a typical implementation must use static initialisation for the flag, and must therefore havepublic data members. The restriction on constructors means that you can’t implement a copyconstructor and assignment operator; instead you’ll need to write a function that uses thedefault copy constructor to copy the data.

ExampleThis example extends the word processor implementation from the SHARING pattern, to allowthe user to change the format of an individual paragraph. In this example the Paragraphobject combines the role of Proxy and Client, since we’ve restricted all access to theParagraphFormat object to via the Paragraph object. We don’t need to separate out the read-only aspects to a separate interface as no clients will ever see ParagraphFormats directly.

The Document class remains unchanged, being essentially a list of paragraphs. TheParagraphFormat class is also straightforward, but now it supports mutator methods andneeds to implement the clone method. For simplicity we only show one mutator – to set thefont.

Page 21: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Copy-on-Write UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 21

class ParagraphFormat implements Cloneable { String defaultFont; int fontSize; int spacing; String nextParagraphFormat;

public Object clone() throws CloneNotSupportedException { return super.clone(); }

void privateSetFont(String aFont) {defaultFont = aFont;}}

As in the previous example, the Paragraph class must also implement cloning. Thisimplementation keeps the shared flag in the Paragraph class (i.e. in the Proxy), as the memberparagraphFormatIsUnique.

class Paragraph implements Cloneable { ParagraphFormat format; boolean paragraphFormatIsUnique = false;

StringBuffer text = new StringBuffer();

Paragraph(ParagraphFormat format) { this.format = format; } Paragraph(String formatName) { this(ParagraphFormatCatalog.catalog().findFormat(formatName)); }

The Paragraph implementation provides two private utility functions:aboutToShareParagraphFormat and aboutToChangeParagraphFormat. The methodaboutToShareParagraphFormat should be invoked whenever we believe it’s possible that wemay be referencing a ParagraphFormat object known to any other object.

protected void aboutToShareParagraphFormat() { paragraphFormatIsUnique = false; }

If any external client obtains a reference to our ParagraphFormat object, or passes in oneexternally, then we must assume that it’s shared:

ParagraphFormat format() { aboutToShareParagraphFormat(); return format; }

public void setFormat(ParagraphFormat aParagraphFormat) { aboutToShareParagraphFormat(); format = aParagraphFormat; }

And similarly, if a client clones this Paragraph object, we don’t want to clone the format, butinstead simply note that we’re sharing it:

public Object clone() { try { aboutToShareParagraphFormat(); Paragraph myClone = (Paragraph) super.clone(); myClone.text = new StringBuffer(text.toString()); return myClone; } catch (CloneNotSupportedException ex) { return null; } }

Meanwhile any method that modifies the ParagraphFormat object must first callaboutToChangeParagraphFormat. This method makes sure the ParagraphFormat object isunique to this Paragraph, cloning it if necessary.

Page 22: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Copy-on-Write UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 22

protected void aboutToChangeParagraphFormat() { if (!paragraphFormatIsUnique) { try { format = (ParagraphFormat) format().clone(); } catch (CloneNotSupportedException e) {} paragraphFormatIsUnique = true; } }

Here’s a simple example of a method that modifies a ParagraphFormat: void setFont(String fontName) { aboutToChangeParagraphFormat(); format.privateSetFont(fontName); }

v v v

Known UsesMany operating systems use COPY-ON-WRITE in their paging systems. Executable code is veryrarely modified, so it’s usually SHARED between all processes using it, but this pattern allowsmodification when processes need it. By default each page out of an executable file is flaggedas read-only and shared between all processes that use it. If a client writes to a shared page,the hardware generates an exception, and operating system exception handler then creates awritable copy for that process alone. [Kenah and Bate 1984; Goodheart and Cox 1994]

RogueWave’s Tools.h++ library uses COPY-ON-WRITE for its CString class [RogueWave1994]. A CString object represents a dynamically allocated string. C++’s pass-by-valuesemantics mean that the CString objects are copied frequently, but very seldom modified. Soeach CString object is simply a wrapper referring to a shared implementation. CString’scopy constructor and related operators manipulate a reference count in the sharedimplementation. If any client does an operation to change the content of the string; theCString object simply makes a copy and does the operation on the copy. One interestingdetail is that there is only one instance of the null string, which is always shared. All attemptsto create a null string, for example by initialising a zero length string, simply access that sharedobject.

Because modifiable strings are relatively rare in programs, Sun Java implements them using aseparate class, StringBuffer. However StringBuffer permits it’s clients to retrieve Stringobjects with the method toString. To save memory and speed up performance the resultingString uses the underlying buffer already created by StringBuffer. However theStringBuffer object has a flag to indicate that the buffer is now shared; if a client attemptsto make further changes to the buffer, StringBuffer creates a copy and uses that. [Chan et al1998]

Objects in NewtonScript were defined using inheritance, so that common features could bedeclared in a parent object and then shared by all child objects that needed them. Default valuesfor objects’ fields were defined using copy-on-write slots. If a child object didn’t define a fieldit would inherit that field’s value from its parent object, but when a child object wrote to ashared field a local copy of the field was automatically created in the child object. [Smith1999].

See AlsoHOOKS provide an alternative technique for changing the contents of read-only storage.

Page 23: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Embedded Pointer UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 23

Embedded PointerHow can you reduce the space used by a collection of objects?

• Linked data structures are built out of pointers to objects

• Collection objects (and their internal link objects) occupy large amounts of memory tostore large collections.

• Traversing through a linked data structure can require temporary memory, especially ifthe traversal is recursive.

Object-Oriented programs implement relationships between objects by using collection objectsthat store pointers to other objects. Unfortunately, collection objects and the objects they useinternally can require a large amount of memory. For example, the Strap-It-On’s ‘MindReader’ brainwave analysis program must receive brainwave data in real-time from aninterrupt routine, and store it in a list for later analysis. Because brainwaves have to besampled many times every second, a large amount of data can accumulate before it can beanalysed, even though each brainwave sample is relatively small (just a couple of integers).Simple collection implementations based on linked lists can impose an overhead of at leastthree pointers for every object they store, so storing a sequence of two-word samples in such alist more than doubles the sequence’s intrinsic memory requirements – see figure xx below.

List Header

First

Last

Collected Object Collected Object Collected Object

Next

ObjectPtr

Next

ObjectPtr

Next

ObjectPtr

Figure 2: Linked list using external pointers

As well as this memory overhead, linked data structures have other disadvantages. They canuse large numbers of small internal objects, increasing the possibility of fragmentation (seeChapter N). Allocating all these objects takes an unpredictable amount of time, making itunsuitable for real-time work. Traversing the structure requires following large numbers ofpointer links; this also takes time, but more importantly, traversals of recursive structures likegraphs and trees can also require an unbounded amount of temporary memory; in some cases,similar amounts of memory to that required to store the structure itself. Finally, any functionthat adds an object to such a collection may fail if there is insufficient memory, and so mustcarry all the costs of PARTIAL FAILURE.

Of course, linked structures have many compensating advantages. They can describe manydifferent kinds of structures, including linked lists, trees, queues, all of a wide variety ofdifferent subtypes [Knuth 1997]. These structures can support a wide variety of operationsquite efficiently (especially insertions and deletions in the middle of the data). Linked structures

Page 24: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Embedded Pointer UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 24

support VARIABLE ALLOCATION, so they never need to allocate memory that is subsequentlyunused. The only real alternative to building linked structures is to use some kind of FIXED

ALLOCATION, such as a fixed-size array. But fixed structures place arbitrary limits on thenumber of objects in the collection, waste memory if they are not fully occupied, and insertionand deletion operations can be very expensive. So, how can you keep the benefits of linkeddata structures while minimising the disadvantages?

Therefore: Embed the pointers maintaining the collection into each object.

Design the collection data structure to store its pointers within the objects that are contained inthe structure, rather than in internal link objects. You will need to change the definitions of theobjects that are to be stored in the collection to include these pointers (and possibly to includeother collection-related information as well).

You will also need to change the implementation of the collection object to use the pointersstored directly in objects. For a collection that is used by only one external client object, youcan even dispense completely with the object that represents the collection, and incorporate itsdata and operations directly into the client. To traverse the data structure, use iteration ratherthan recursion to avoid allocating stack frames for every recursive call, and use extra (or reuseexisting) pointer fields in the objects to store any state related to the traversal.

So, for example, rather than store the Brainwave sample objects in a collection, Strap-it-On’sBrainwave Driver uses an embedded linked list. Each Brainwave sample object has an extrapointer field, called Next, that is used to link brainwave samples into a linked list. As eachsample is received, the interrupt routine adjusts its Next field to link it into the list. The mainanalysis routine adjusts the sample object’s pointers to remove each from the list in its owntime for processing.

List Header

First

Last

Collected Object

Next Collected Object

Next Collected Object

Next

Figure 3: Linked list using embedded pointers

ConsequencesEmbedded pointers remove the need for internal link objects in collections, reducing the numberof objects in the system and thus the system’s memory requirements, while increasing thepredictability of the systems memory use (especially if traversals are iterative rather thanrecursive). The routines to add and remove items from the linked list cannot suffer memoryallocation failure.

Using embedded pointers reduces or removes the need for dynamic memory allocation,improving the real-time performance of the system. Some operations may have better run-time performance; for example with an embedded doubly-linked list you can remove anelement in the collection simply by using a pointer to that element directly. With animplementation using external pointers (such as STL’s Deque [Austern 1998]) you’d need firstto set an iterator to refer to the right element, which requires a linear search.

Page 25: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Embedded Pointer UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 25

However: Embedded pointers don’t really belong to the objects they are embedded inside. Thispattern reduces those objects’ encapsulation, gaining a local benefit but reducing thelocalisation of the design. The pattern tightly couples objects to the container class that holdsthem, making it more difficult to reuse either class independently, increasing the programmereffort required because specialised collections often have to be written from scratch, reducingthe design quality of the system and making the program harder to maintain.

In many cases a given collected object it will often need to be in several different collections atdifferent times during its lifetime. It requires programmer discipline to ensure that the samepointer is never used by two collections simultaneously.

v v v

ImplementationApplying the Embedded Pointer pattern is straightforward: place pointer members into objectsand build up linked data structures using those pointers, instead of using external collectionobjects. You can find the details in any decent textbook on data structures from Knuth [1997],which will describe the details, advantages, and disadvantages of the classical linked datastructure designs, from simple singly and doubly linked lists to subtle complex balanced trees.See the SMALL DATA STRUCTURES pattern for a list of such textbooks.

1. Reuse

The main practical issue when using embedded pointers is how to incorporate the pointers intoobjects in a way that provides some measure of reuse, to avoid re-implementing all thecollection operations for every single list. The key idea is for objects to somehow present aconsistent interface for accessing the embedded pointers to the collection class (or the functionsthat implement the collection operations). In this way, the collection can be used with anyobject that provides a compatible interface. There are three common techniques forestablishing interfaces to embedded pointers: inheritance, inline objects, and preprocessorconstructs.

1.1. Inheritance. You can put the pointers and accessing functionality into a superclass, andmake the objects to be stored in a collection inherit from this class. This is straightforward,and provides a measure of reuse. However: you can’t have more than one instance of such apointer for a given object; if the pointer is implementing a collection, this would limit. Insingle-inheritance languages like Smalltalk it also prevents any other use of inheritance for thesame object, and so limits any object to be in only one collection at a time. In languages withmultiple inheritance objects could be in multiple collections provided each collection accessesthe embedded pointers through a unique interface, supplied by a unique base class (C++) orinterface (Java).

supportFunctions()

embeddedPointer

EmbeddableObject

Client Object

Page 26: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Embedded Pointer UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 26

1.2. Inline Objects. In languages with inline objects, like C and C++, you can embedded aseparate ‘link’ object that contains the pointers directly into the client object. This doesn’tsuffer from the disadvantages of using Inheritance, but you need to be able to find the clientobject from a given link object and vice versa. In C++ this can be implemented using pointersto members, or (more commonly) as an offset in bytes.

Offset

Client Object

embedded Pointer

Link Object

For example, EPOC’s collection libraries find embedded pointers using byte offsets. Whenevera new collection is created, it must be initialised with the offset inside its client objects whereits pointers are embedded.

1.3. Preprocessors. C++ provides two kinds of preprocessing: the standard preprocessorcpp, and the C++ template mechanisms. So in C++ a good approach is to include theembedded pointers as normal (possibly public) data members, and to reuse the managementcode via preprocessing. You can also preprocess code in almost any other language given asuitable preprocessor , which could be either a special purpose program like m4, or a generalpurpose program like perl.

2. Pointer Differences

Sometimes an object needs to store two or more pointers; for example a circular doubly-linkedlist node needs pointers to the previous and next item in the list. You can reduce the amount ofmemory needed to store by storing the difference (or the bitwise exclusive or) of the twopointers, rather than the pointer itself. When you are traversing the structure forwards, forexample, you take the address of the previous node and add the stored difference to find theaddress of the next node; reverse traversals work similarly.

For example, in Figure XXX, rather than node c storing the dotted forward and back pointers(i.e. the addresses of nodes b and d) node c stores only the difference between these twoaddresses. Given a pointer to node b and the difference stored within c, you can calculate theaddress of node d as (b – (d-c)). Similarly, traversing the list the other way, given the addressof node d and (b-c) you can calculate the address of node b as (d+(b-d)). For this to work, youneed to store two initial pointers, typically a head and tail pointer for a circular doubly-linkedlist [Knuth 1997].

b

(a-c)

c

(b-d)

d

(c-e)

(3) Traversals

A related, but different, problem happens when you need to traverse an arbitrarily deepstructure, especially if the traversal has to be recursive. Suppose for example you have anunbalanced binary tree, and you need to traverse through all the elements in order. A traversal

Page 27: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Embedded Pointer UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 27

beginning at E will recursively visit C, then F; the traversal at C will visit B and D, and so on.Every recursive call requires extra memory to store activation records on the stack, sotraversing larger structures can easily exhaust a process’s stack space.

C

B

A

E

D

F

3.1. Iterative traversals using extra embedded pointers. Consider the traversal moreclosely: at each object it needs to store one thing on the stack: the identity of the object itscoming from (and possibly any working data or parameters passed through the iteration). Sofor example, when C invokes the operation on D, it must store that it needs to return to E oncompletion. You can use Embedded Pointers in each object to store this data (the parent, andthe traversal state — two Boolean flags that remember whether the left and right leaves havebeen processed). This allows you to iterate over the structure using a loop rather thanrecursion, but imposes the overhead of an extra pointer or two in each object.

doAction()static iterate()

previousObjectcurrentlyProcessing

Collected Object

left right

current = TopElementwhile (true) { if (we've processed left and not right) doAction();

if (left exists && we've not processed it) start processing left object; else if (right exists && we've not processed it) start processing right object else (if previous exists) return to processing previous object else

return; }

3.2. Iterative traversals using pointer reversal. Consider the iteration process further. Atany time one of the three pointers in each element, left leaf, right leaf or parent, is redundant.If there is no iteration, the parent pointer is redundant; if a left or right left is currently beingprocessed that leaf pointer is redundant (because the traversal has already reached that leaf).Pointer reversal allows iterative traversals of linked structures by temporarily using pointers toleaf nodes as parent pointers: as the traversal proceeds around the object, the pointers currentlybeing followed are ‘reversed’, that is, used to point to parent objects.

In the figure above, for example, when a traversal is at node A, node B’s left leaf pointer wouldbe reversed to point to its parent, node C; because B is C’s left child, C’s left child pointerwould also be reversed to point to node E.

Page 28: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Embedded Pointer UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 28

C

B

A

E

D

F

ExampleHere’s an example using Embedded Pointers to store a data structure and traverse it usingpointer reversal. The example program implements a sorting algorithm using a simple binarytree. To start with we’ll give the objects that are stored in the tree (the BinaryTreeObjects)a single character of local data. We also need a greaterThan operation and a operationcalled to do the action we need (doIt).

class BinaryTreeObject { char data;

BinaryTreeObject(char data) { this.data = data; }

Object doIt(Object param) { return ((String) param + data); }

boolean greaterThan(BinaryTreeObject other) { return data > other.data; }

A binary tree needs a left pointer and a right pointer corresponding to each node. Using theEmbedded Pointers pattern, we implement each within the structure itself:

BinaryTreeObject left; BinaryTreeObject right;

Adding an element to the binary tree is fairly easy. We can traverse the tree starting at the top,going left when our new element is less than the current item, greater when it’s greater, until weget to a vacant position in the tree.

static void insert(BinaryTreeObject top, BinaryTreeObject newItem) { BinaryTreeObject current = top;

for (;;) { if (current.greaterThan(newItem)) { if (current.left == null) { current.left = newItem; return; } else { current = current.left; } } else { if (current.right == null) { current.right = newItem; return; } else { current = current.right; } } } }

Page 29: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Embedded Pointer UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 29

Note that this method is not recursive and so should allocate no memory other than one stackframe with one local variable (current).

Traversing the tree is more difficult, because the traversal has to visit all the elements in thetree, and this means backtracking up the tree when it reaches a bottom-level node. To traversethe tree without using recursion, we can add two embedded pointers to every tree node: apointer to the previous (parent) item in the tree, and a marker noting which action, left node orright node, the algorithm is currently processing.

BinaryTreeObject previous; static final int Inactive = 0, GoingLeft = 1, GoingRight = 2; int action = Inactive;

The traversal method, then, must move through each node in infix order. Each iterationvisits one node; however this may mean up to three visits to any given node (from parent goingleft, from left going right, and from right back to parent); we use the stored action data for thenode to see which visit this one is. The traversal method must also call the doIt method atthe correct point – after processing the left node, if any.

static Object traversal(BinaryTreeObject start, Object param) { BinaryTreeObject current = start;

for (;;) {

if (current.action == GoingLeft || (current.action == Inactive && current.left == null)) { param = current.doIt(param); }

if (current.action == Inactive && current.left != null) { current.action = GoingLeft; current.left.previous = current; current = current.left; } else if (current.action != GoingRight && current.right != null) {

current.action = GoingRight;current.right.previous = current;current = current.right;

} else {current.action = Inactive;if (current.previous == null) { break;}current = current.previous;

}

} return param;}

Of course, a practical implementation would improve this example in two ways. First we canput the left, right, action, previous pointers and the greaterThan stub into a base class(SortableObject, perhaps) or into a separate object. Second we can make the traversal()method into a separate ITERATOR object [Gamma et al 1995], avoiding the need to hard-code thedoIt method.

We can extend this example further, to remove the parent pointer from the data structure usingPointer Reversal. First, we’ll need two additional methods, to save the parent pointer in eitherthe left or the right pointer:

Page 30: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Embedded Pointer UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 30

BinaryTreeObject saveParentReturningLeaf(BinaryTreeObject parent) { BinaryTreeObject leaf;

if (action == GoingLeft) { leaf = left; left = parent; } else { leaf = right; right = parent; } return leaf; }

and then to restore it as required:BinaryTreeObject restoreLeafReturningParent(BinaryTreeObject leafJustDone) { BinaryTreeObject parent;

if (action == GoingLeft) { parent = left; left = leafJustDone; } else { parent = right; right = leafJustDone; } return parent; }

Now we can rewrite the traversal method to remember the previous item processed, whetherit’s the parent of the current item or a leaf node, and to reverse the left and right pointers usingthe methods above:

static Object reversingTraversal(BinaryTreeObject top, Object param) {

BinaryTreeObject current = top; BinaryTreeObject leafJustDone = null; BinaryTreeObject parentOfCurrent = null;

for (;;) {

if (current.action == GoingLeft || (current.action == Inactive && current.left == null)) { param = current.doIt(param); }

if (current.action != Inactive) parentOfCurrent = current.restoreLeafReturningParent(leafJustDone);

if (current.action == Inactive && current.left != null) { current.action = GoingLeft; BinaryTreeObject p = current; current = current.saveParentReturningLeaf(parentOfCurrent); parentOfCurrent = p; } else if (current.action != GoingRight && current.right != null) {

current.action = GoingRight;BinaryTreeObject p = current;current = current.saveParentReturningLeaf(parentOfCurrent);parentOfCurrent = p;

} else { current.action = Inactive; if (parentOfCurrent == null) { break; } leafJustDone = current; current = parentOfCurrent; }

} return param; }

We’re still wasting a word in each object for the ‘action’ parameter. In Java we could perhapsreduce this to a byte but no further. In a C++ implementation we could use the low bits of,

Page 31: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Embedded Pointer UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 31

say, the left pointer to store it (see PACKED DATA – packing pointers), thereby reducing theoverhead of the traversing algorithm to nothing at all.

v v v

Known UsesEPOC provides at least three different ‘linked list’ collection classes using embedded pointers[Symbian 1999]. The embedded pointers are instances of provided classes (TSglQueLink, forexample) accessed via offsets; the main collection logic are in separate classes, which use the‘thin template idiom’ to provide type safety: TSglQueue<MyClass>. EPOC applications, andoperating system components, use these classes extensively. The most common reason forpreferring them over collections requiring heap memory is that operations using them cannotfail; this is a significant benefit in situations where failure handling is not provided.

The Smalltalk LinkedList class uses inheritance to mix in the pointers; the only things youcan store into a LinkedList are objects that inherit from class Link [Goldberg and Robson1983]. Class Link contains two fields and appropriate accessors (previous and next) toallow double linking. Compared with other Smalltalk collections, for each element you saveone word of memory by using concatenation instead of pointers, plus you save the memoryoverhead of creating a new object (two words or so) and the overhead of doing the allocation.

See AlsoYou may be able to use FIXED ALLOCATION to embed objects directly into other objects, ratherthan just embedding pointers to objects.

Pointer reversal was first described by Peter Deutsch [Knuth 1997] and Schorr and Waite[1967]. Embedded Pointers and pointer reversal are used together in many implementations ofGARBAGE COLLECTION [Goldberg and Robson 1983, Jones and Lins 1996]. Jiri Soukupdiscusses using preprocessors to implement linked data structures in much more detail [1994].

Page 32: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Multiple Representations UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 32

Multiple RepresentationsHow can you support several different implementations of an object?

• There are several possible implementations of a class, with different trade-offs betweensize and behaviour.

• Different parts of your system, or different uses of the class, require different choices ofimplementation. One size doesn’t fit all.

• There are enough instances of the class to justify extra code to reduce RAM usage.

Often when you design a class, you find there can be several suitable representations for itsinternal data structures. For example, in the Strap-It-On’s word-processor (Word-O-Matic) aword may be represented as a series of characters, a bitmap, or a sequence of phonemes.Depending on the current output mechanism (a file, the screen, or the vocaliser) each of theserepresentations might be appropriate.

Having to choose between several possible representations is quite common. Somerepresentations may have small memory requirements, but be costly in processing time or otherresources; others may be the opposite. In most cases you can examine the demands of thesystem and decide on a best SMALL DATA STRUCTURE. But what do you do when there’s nosingle ‘best’ implementation?

Therefore: Make each implementation satisfy a common interface.

Design a common abstract interface that suits all the implementations without depending on aparticular one, and ensure every implementation meets the interface. Access implementationsvia an ABSTRACT CLASS [Woolf 2000] or use ADAPTERS to access existing representations[Gamma et al 1995] so clients don’t have to be aware of the underlying implementation.

Foo()Bar()

Common Interface

Client

Foo()Bar()

ImplementationA

Foo()Bar()

ImplementationB

Uses

DifferentImplementations

For example, Word-O-Matic defines a single interface ‘Word’, which is used by much of theword-processing code. Several concrete classes, StorableWord, ViewableWord,SpokenWord, that implement the Word interface. Each implementation has different internaldata structures and different implementations of the operations that access those structures.The software creates whichever concrete class is appropriate for the current use of the Wordobject, but the distinction is only significant when it comes to outputting the object. Themultiple implementations are concealed from most of the client code.

Page 33: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Multiple Representations UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 33

ConsequencesThe system will use the most appropriate implementation for any task, reducing the memoryrequirements and processing time overheads that would be imposed by using an inappropriaterepresentation. Code using each instance will use the common interface and need not know theimplementation, reducing programmer effort on the client side and increasing design qualityand reusability.

Representations can be chosen locally for each data structure. More memory-intensiverepresentations can be used when more memory is available, adding to the scalability of thesystem.

However: The pattern can also increase total memory requirements, since the code occupies additionalmemory.

MULTIPLE REPRESENTATIONS increases programmer effort in the implementation of the objectconcerned, because multiple implementations are more complex than a single implementation,although this kind of complexity is often seen as a sign of high-quality design becausesubsequent changes to the representation will be easier. For the same reason, it increasestesting costs and maintenance costs overall, because each alternative implementation must betested and maintained separately.

Changing between representations imposes a space and time overhead. It also means morecomplexity in the code, and more complicated testing strategies, increasing programmer effortand making memory use harder to predict.

v v v

ImplementationThere are number of issues to take into account when you are using MULTIPLEREPRESENTATIONS.

1. Implementing the Interface.

In Java the standard implementation of dynamic binding means defining either a Java class or aJava interface. Which is more suitable? From the point of view of the client, it doesn’t matter;either can define an abstract interface. Using a Java interface gives you more flexibility,because each implementation may inherit from other existing classes as required; however,extending a common superclass allows several implementations to inherit commonfunctionality. In C++ there’s only the one conventional option for implementing the commoninterface: making all implementations inherit from a base class that defines the interface.

There’s a danger that clients may accidentally rely on features on a particular implementation –particularly non-functional ones – rather than of the common interface. D’Souza and Wills[1998] discuss design techniques to avoid such dependencies in components.

2. Binding clients to implementations.

Sometimes you need to support several implementations, though a given client may only everuse one. For example, the C++ Standard Template Library (STL) iterator classes work onseveral STL collections, but any given STL iterator object works with only one [Stroustrup1997, Austern 1998]. In this case, you can statically bind the client code to use only the oneimplementation in C++ you could store objects directly and use non-virtual functions. If,however, a client needs to use several different object representations interchangeably then youneed to use dynamic binding.

Page 34: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Multiple Representations UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 34

3. Creating dynamically bound implementations

The only place where you need to reference the true implementation classes in the code is wherethe objects are created. In many situations, it’s reasonable to hard code the class names in theclient, as in the following C++ example:

CommonInterface *anObject = new SpecificImplementation( parameters );

If there’s a good reason to hide even this mention of the classes from the client, then theABSTRACT FACTORY pattern [Gamma et al 1995] can implement a ‘virtual constructor’ [Coplien1994] so that the client can specify which object to create using just a parameter.

4. Changing between representations

In some cases, an object’s representation needs to change during its lifetime, usually because aclient needs some behaviour that is not supported well by the object’s current representation.Changes to an object’s representation can be explicitly requested by its client, or can betriggered automatically within the object itself. Changing representations automatically hasseveral benefits: the client doesn’t need knowledge of the internal implementation, improvingencapsulation, and you can tune the memory use entirely within the implementation of thespecific object, improving localisation. Changing representations automatically requiresdynamic binding, so clients will use the correct representation without being aware of it. Insome situations, however, the client can have a better knowledge of optimisation strategies thanis available to the object itself, typically because the client is in a better position to know whichoperations will be required.

4.1. Changing representations explicitly. It is straightforward for an object to let a clientchange its representation explicitly: the object should implement a conversion function (or aC++ constructor) that takes the common interface as parameter, and returns the newrepresentation.

class SpecificImplementation : public CommonInterface{ public: SpecificImplementation( CommonInterface c ) { // initialise this from c }};

4.2. Changing representations automatically. You can use the BRIDGE pattern to keep theinterface and identity of the object constant when its internal structure changes (strictlyspeaking a ‘half bridge’, since it varies only the object implementation and not the abstractionit supports) [Gamma et al 1995]. The client sees only the bridge object, which delegates all itsoperations to an implementation object through a common interface:

Page 35: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Multiple Representations UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 35

Function1()Function2()

CommonInterface

Client UsesFunction1()Function2()

Bridge 11

Function1()Function2()

ImplementationA

Function1()Function2()

ImplementationB

The bridge class needs the same methods as the common interface; so it’s reasonable (thoughunnecessary) in C++ and Java to make the Bridge class derive from the common interface.Each implementation object will need a construction function taking the common interface asparameter. Of course, some implementations may store more or less data, so there may bespecial cases with more specific constructors.

Some languages make it simple to implement the Bridge object itself. In Smalltalk, forexample, you can override the DoesNotUnderstand: method to pass any unrecognisedoperation on to the implementation object [Lalonde 1994]. In C++ you can implementoperator->() to do the same [Coplien 1994], or alternatively you can avoid deriving theBridge class from the common interface and by make all its functions non-virtual and inline.

ExampleThis Java example implements a Word object with two representations: as a simple string (thedefault), and as a string with an additional cached corresponding sound. Both theserepresentations implement the basic Word interface, which can return either a string or soundvalue, and allows clients to choose the most appropriate representation.

interface WordInterface{ public byte[] asSound(); public String asString(); public void becomeSound(); public void becomeString();}

The most important concrete class is the Word class, that acts as a bridge between the Wordabstraction an its two representations, as a sound and as a text string.

class Word implements WordInterface { private WordInterface rep;

public byte[] asSound() {return rep.asSound();} public String asString() {return rep.asString();} public void becomeSound() {rep.becomeSound();} public void becomeString() {rep.becomeString();}

The constructor of the Word class must select an implementation. It uses the method Become,which simply sets the implementation object.

Page 36: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Multiple Representations UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 36

public Word(String word) { become(new StringWordImplementation(this, word)); } public void become(WordInterface rep) { this.rep = rep; }

The default implementation stores Words as a text string. It also keeps a pointer to its WordBRIDGE object, and uses this pointer to automatically change a word’s representation into theother format. It has two constructors: one, taking a string, is used by the constructor for theWord object; the other, taking a WordInterface, is used to create itself from a differentrepresentation.

class StringWordImplementation implements WordInterface{ private String word; private Word bridge;

public StringWordImplementation(Word bridge, String word) { this.bridge = bridge; this.word = word; }

public StringWordImplementation(Word bridge, WordInterface rep) { this.bridge = bridge; this.word = rep.asString(); }

It must also provide implementations of all the WordInterface methods. Note how it mustchange its representation to return itself as a sound; once the asSound method returns thisobject will be garbage:

public byte[] asSound() { becomeSound(); return bridge.asSound(); } public String asString() {return word;}

public void becomeSound() { bridge.become(new SoundWordImplementation(bridge, this)); } public void becomeString() {}

Finally, the sound word class is similar to the text version, but also caches the soundrepresentation. Implementing the sound conversion function is left as an exercise for the reader!

class SoundWordImplementation implements WordInterface{ private String word; private Word bridge; private byte[] sound;

SoundWordImplementation(Word bridge, WordInterface rep) { this.bridge = bridge; this.word = rep.asString(); this.sound = privateConvertStringToSound(this.word); }

public String asString() {return word;} public byte[] asSound() {return sound;} public void becomeString() { bridge.become(new StringWordImplementation(bridge, this)); } public void becomeSound() {}}

v v v

Page 37: Small Data Structuressmallmemory.com/5_DataStructuresChapter.pdffrom designing data structures in other environments, but the practical tradeoffs can result in different solutions

Multiple Representations UNTITLED by Weir, Noble

© 1999 Charles Weir, James Noble Page 37

Known UsesSymbian’s EPOC C++ environment handles strings as Descriptors containing a buffer and alength. Descriptors provide many different representations of strings: in ROM, in a fixed-length buffer, in a variable length buffer and as a portion of another string. Each kind ofdescriptor has its own class, and users of the strings see only two base classes: one for a read-only string, the other for a writable string [Symbian 1999].

The Psion 5’s Word Editor has two internal representations of a document. When thedocument is small the editor keeps formatting information for the entire document; when thedocument is larger than a certain arbitrary size, the editor switches to storing information foronly the part of the document currently on display. The switch is handled internally to theEditor’s ‘Text View’ component; clients of the component (including other applications thatneed rich text) are unaware of the change in representation.

Smalltalk’s collection classes also use this pattern: all satisfy the same protocol, so a user neednot be aware of the particular implementation used for a given collection [Goldberg andRobson 1983]. Java’s standard collection classes have a similar design [Chan et al 1998].C++’s STL collections also use this pattern: STL defines the shared interface using templateclasses; all the collection classes support the same access functions and iterator operations[Stroustrup 1997, Austern 1998].

Rolfe&Nolan’s Lighthouse system has a ‘Deal’ class with two implementations: by default aninstance contains only basic data required for simple calculations; on demand, it extends itselfreading the entire deal information from its database. Since clients are aware when they aredoing more complex calculations, the change is explicit, implemented as a FattenDealmethod on the object.

MULTIPLE REPRESENTATIONS can also be useful to implement other memory saving patterns. Forexample the LOOM Virtual Memory system for Smalltalk uses two different representationsfor objects: one for objects completely in memory, and a second for objects PAGED out toSECONDARY STORAGE [Kaehler and Krasner 1983]. Format Software’s PLUS applicationimplements CAPTAIN OATES for images using three representations, which change dynamically: abitmap ready to bitblt to the screen, a compressed bitmap, and a reference to a representationin the database.

See AlsoThe BRIDGE pattern describes how abstractions and implementations can vary independently[Gamma et al 1994]. The MULTIPLE REPRESENTATIONS pattern typically uses only half of theBRIDGE pattern, because implementations can vary (to give the multiple representations) but theabstraction remains the same.

Various different representations can use explicit PACKED DATA or COMPRESSION, be stored inSECONDARY STORAGE, be READ-ONLY, or be SHARED. They may also use FIXED ALLOCATION orVARIABLE ALLOCATION.