| <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN"> |
| <HTML> |
| <head> |
| <META HTTP-EQUIV="CONTENT-TYPE" CONTENT="text/html; charset=iso-8859-1"> |
| <TITLE>Cachemaps</TITLE> |
| <STYLE> |
| <!-- |
| A:link { color: #444488 } |
| A:visited { color: #444488 } |
| --> |
| </STYLE> |
| </head> |
| <body LINK="#444488" VLINK="#444488"> |
| <TABLE WIDTH=100% BORDER=0 CELLPADDING=4 CELLSPACING=0 STYLE="page-break-before: always"> |
| <COL WIDTH=75> |
| <TR> |
| <TD BGCOLOR="#666699"> |
| <H1 ALIGN=CENTER STYLE="margin-top: 0cm; text-decoration: none"><A HREF="http://www.openoffice.org/"><IMG SRC="../images/open_office_org_logo.gif" NAME="Graphic1" ALT="OpenOffice.org" ALIGN=RIGHT WIDTH=126 HEIGHT=53 BORDER=0></A><FONT COLOR="#ffffff"><FONT SIZE=6>Cachemaps</FONT></FONT></H1> |
| </TD> |
| </TR> |
| </TABLE> |
| <HR SIZE=3 NOSHADE> |
| <TABLE WIDTH=100% BORDER=0 CELLPADDING=4 CELLSPACING=0 STYLE="page-break-before: always"> |
| <COL WIDTH=256*> |
| <TR> |
| <TD WIDTH=100% BGCOLOR="#666699"> |
| <H3 ALIGN=LEFT STYLE="margin-top: 0cm; text-decoration: none"><FONT COLOR="#ffffff"><FONT SIZE=4>Contents</FONT></FONT></H3> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <P STYLE="margin-bottom: 0cm"><A HREF="#Abstract">Abstract</A><BR> |
| <A HREF="#Solution1">Solution 1 (Single Mutex)</A><BR> |
| <A HREF="#Solution2">Solution 2 (Weak References)</A><BR> |
| <A HREF="#Solution3">Solution 3 (Best of Both Worlds)</A><BR> |
| <A HREF="#Measurements">Performance Measurements</A></P> |
| <P><BR></P> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100% BGCOLOR="#666699"> |
| <H3 ALIGN=LEFT STYLE="margin-top: 0cm; text-decoration: none"><A NAME="Abstract"></A> |
| <FONT COLOR="#ffffff"><FONT SIZE=4>Abstract</FONT></FONT></H3> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <P STYLE="margin-bottom: 0.1cm">I quite often have the need for a construct I will call a |
| <I>cachemap.</I> A cachemap is like an ordinary map (or hashmap), in |
| that it maps from keys (normally strings) to objects, but it also |
| controls the lifetime of the mapped objects. If a client requests an |
| object that is not yet instantiated, a (refcounted) instance is |
| created, inserted into the cachemap, and returned to the client. If |
| the client no longer needs the object (the refcount of the object |
| drops to zero), it is automatically removed from the cachemap. And if |
| a client requests an object that is already present in the cachemap |
| (because it was requested by a client before, and its refcount is |
| still positive), the present instance is returned. Skeleton C++ class |
| definitions for the cachemap and the objects it manages could look |
| like the following:</P> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <HR SIZE=1 NOSHADE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <PRE>class ObjectContainer // the cachemap |
| { |
| public: |
| rtl::Reference< Object > get(rtl::OUString const & rKey); |
| }; |
| |
| class Object // the objects managed by the cachemap |
| { |
| public: |
| void acquire() SAL_THROW(()); |
| |
| void release() SAL_THROW(()); |
| };</PRE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <HR SIZE=1 NOSHADE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <P STYLE="margin-bottom: 0.1cm">The problems in implementing this are that (1) the cachemap must not |
| <FONT SIZE=2><FONT FACE="Courier New, monospace">acquire()</FONT></FONT> |
| the objects it currently has in its map (otherwise, the refcounts of |
| those objects could never drop to zero, and they would never be |
| removed from the map), and (2) in a multi-threaded environment there |
| is a problem when one thread does a final <FONT SIZE=2><FONT FACE="Courier New, monospace">release()</FONT></FONT> |
| of an object (refcount drops to zero and object is removed from the |
| map) and at the same time another thread happens to call <FONT SIZE=2><FONT FACE="Courier New, monospace">get()</FONT></FONT> |
| to request that very object.</P> |
| <P STYLE="margin-bottom: 0.1cm">Below, I describe three different implementations that solve these |
| problems. The first solution was my initial idea, but it tends to be |
| slow. The second is based on weak references, but it turned out to be |
| slow, too. The third combines ideas from the other two |
| solutions---and it turned out to be fast. So, if you are interested |
| in boiler plate code for a fast implementation of the cachemap |
| pattern, turn to <A HREF="#Solution3">the third solution</A> |
| directly.</P> |
| <P STYLE="margin-bottom: 0.1cm">The three solutions all follow the same conventions. They |
| implement an <FONT SIZE=2><FONT FACE="Courier New, monospace">ObjectContainer<I>N</I></FONT></FONT> |
| (the cachemap, mapping from strings to objects) and a corresponding |
| <FONT SIZE=2><FONT FACE="Courier New, monospace">Object<I>N</I></FONT></FONT> |
| (the refcounted objects managed by the cachemap). The source code |
| presented here is also available within the ucb project |
| (<A HREF="http://www.openoffice.org/source/browse/ucb/ucb/workben/cachemap/"><FONT SIZE=2><FONT FACE="Courier New, monospace">ucb/workben/cachemap</FONT></FONT></A>), |
| and it is presented here without the (somewhat longish) copyright |
| headers.</P> |
| <P ALIGN=LEFT><BR></P> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100% BGCOLOR="#666699"> |
| <H3 ALIGN=LEFT STYLE="margin-top: 0cm; text-decoration: none"><A NAME="Solution1"></A> |
| <FONT COLOR="#ffffff"><FONT SIZE=4>Solution 1 (Single Mutex)</FONT></FONT></H3> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <P STYLE="margin-bottom: 0.1cm">The |
| combined header file for both <FONT SIZE=2><FONT FACE="Courier New, monospace">ObjectContainer1</FONT></FONT> |
| and <FONT SIZE=2><FONT FACE="Courier New, monospace">Object1</FONT></FONT> is |
| <A HREF="http://www.openoffice.org/source/browse/ucb/ucb/workben/cachemap/cachemapobject1.hxx"><FONT SIZE=2><FONT FACE="Courier New, monospace">cachemapobject1.hxx</FONT></FONT></A>:</P> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <HR SIZE=1 NOSHADE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <PRE>#ifndef <A HREF="#validguardnames">INCLUDED_UCB_CACHEMAPOBJECT1_HXX</A> |
| #define INCLUDED_UCB_CACHEMAPOBJECT1_HXX |
| |
| #ifndef _OSL_INTERLOCK_H_ |
| #include "osl/interlck.h" |
| #endif |
| #ifndef _OSL_MUTEX_HXX_ |
| #include "osl/mutex.hxx" |
| #endif |
| #ifndef _RTL_REF_HXX_ |
| #include "rtl/ref.hxx" |
| #endif |
| #ifndef _SAL_TYPES_H_ |
| #include "sal/types.h" |
| #endif |
| #ifndef _SALHELPER_SIMPLEREFERENCEOBJECT_HXX_ |
| #include "salhelper/simplereferenceobject.hxx" |
| #endif |
| |
| #ifndef <A HREF="#stdguards">INCLUDED_MAP</A> |
| #include <map> |
| #define INCLUDED_MAP |
| #endif |
| #ifndef INCLUDED_MEMORY |
| #include <memory> |
| #define INCLUDED_MEMORY |
| #endif |
| |
| namespace rtl { class OUString; } |
| namespace ucb { namespace cachemap { class Object1; } } |
| |
| namespace ucb { namespace cachemap { |
| |
| class ObjectContainer1: <A HREF="#refcountedcontainer">public salhelper::SimpleReferenceObject</A> |
| { |
| public: |
| ObjectContainer1(); |
| |
| virtual ~ObjectContainer1() SAL_THROW(()); |
| |
| rtl::Reference< Object1 > get(rtl::OUString const & rKey); |
| |
| private: |
| typedef <A HREF="#map">std::map</A>< rtl::OUString, Object1 * > Map; |
| |
| Map m_aMap; |
| osl::Mutex m_aMutex; |
| |
| void releaseElement(Object1 * pElement) SAL_THROW(()); |
| |
| <A HREF="#friends">friend class Object1;</A> // to access Map, releaseElement() |
| }; |
| |
| class Object1 |
| { |
| public: |
| inline void acquire() SAL_THROW(()) |
| { osl_incrementInterlockedCount(&m_nRefCount); } |
| |
| inline void release() SAL_THROW(()) |
| { m_xContainer->releaseElement(this); } |
| |
| private: |
| rtl::Reference< ObjectContainer1 > m_xContainer; |
| ObjectContainer1::Map::iterator m_aContainerIt; |
| oslInterlockedCount m_nRefCount; |
| |
| inline Object1(rtl::Reference< ObjectContainer1 > const & rContainer); |
| |
| inline ~Object1() SAL_THROW(()); |
| |
| Object1(Object1 &); // not implemented |
| void operator =(Object1); // not implemented |
| |
| friend class ObjectContainer1; |
| // to access m_aContainerIt, m_nRefCount, Object1(), ~Object1() |
| #if defined WNT |
| friend struct std::auto_ptr< Object1 > // to access ~Object |
| // work around compiler bug... |
| #else // WNT |
| <A HREF="#autoptrfriend">friend class std::auto_ptr< Object1 >;</A> // to access ~Object1() |
| #endif // WNT |
| }; |
| |
| } } |
| |
| #endif // INCLUDED_UCB_CACHEMAPOBJECT1_HXX</PRE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <HR SIZE=1 NOSHADE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <P STYLE="margin-bottom: 0.1cm">A few notes:</P> |
| <UL> |
| <LI><P STYLE="margin-bottom: 0.1cm"><A NAME="validguardnames"></A>It is a good idea to use valid |
| C++ identifiers as include guards for header files. That is, do not |
| use identifiers that start with an underline followed by an upper |
| case letter (e.g., <FONT SIZE=2><FONT FACE="Courier New, monospace">_UCB_CACHEMAPOBJECT1_HXX_</FONT></FONT>) |
| or that contain two underlines in a row, because those identifiers |
| are reserved for internal use by the compiler and standard library |
| implementation. (<A HREF="http://cseng.aw.com/book/0,,0201633620,00.html">Lakos</A>, |
| section 2.4, suggests the <FONT SIZE=2><FONT FACE="Courier New, monospace">INCLUDED_<I>XXX</I></FONT></FONT> |
| style.)</P> |
| <LI><P STYLE="margin-bottom: 0.1cm"><A NAME="stdguards"></A>Use the convention of (redundant but |
| compilation time reducing) external include guards for standard |
| header files, too. For this to work, it is necessary that everybody |
| use the same identifiers for the various standard header files, of |
| course. (See <A HREF="http://cseng.aw.com/book/0,,0201633620,00.html">Lakos</A>, |
| section 2.5.)</P> |
| <LI><P STYLE="margin-bottom: 0.1cm"><A NAME="refcountedcontainer"></A><FONT SIZE=2><FONT FACE="Courier New, monospace">ObjectContainer1</FONT></FONT> |
| turns out to be a refcounted object. This was not required in the |
| introduction of the cachemap pattern, but it is rather a consequence |
| of the chosen implementation (and it should not hurt any client, |
| either).</P> |
| <LI><P STYLE="margin-bottom: 0.1cm"><A NAME="map"></A>In <FONT SIZE=2><FONT FACE="Courier New, monospace">ObjectContainer1</FONT></FONT>, |
| a <FONT SIZE=2><FONT FACE="Courier New, monospace">std::map</FONT></FONT> |
| is used instead of a (de facto standard?) <FONT SIZE=2><FONT FACE="Courier New, monospace">std::hash_map</FONT></FONT> |
| that might be a bit faster. The implementation of <FONT SIZE=2><FONT FACE="Courier New, monospace">ObjectContainer1</FONT></FONT> |
| relies on the fact that iterators into a <FONT SIZE=2><FONT FACE="Courier New, monospace">std::map</FONT></FONT> |
| are not invalidated when calling <FONT SIZE=2><FONT FACE="Courier New, monospace">insert()</FONT></FONT> |
| or <FONT SIZE=2><FONT FACE="Courier New, monospace">erase()</FONT></FONT>. |
| There seems to be no corresponding guarantee for <FONT SIZE=2><FONT FACE="Courier New, monospace">std::hash_map</FONT></FONT>.</P> |
| <LI><P STYLE="margin-bottom: 0.1cm"><A NAME="friends"></A><FONT SIZE=2><FONT FACE="Courier New, monospace">ObjectContainer1</FONT></FONT> |
| and <FONT SIZE=2><FONT FACE="Courier New, monospace">Object1</FONT></FONT> |
| need to be mutual friends. If one entity is a friend of another, |
| these entities are strongly coupled, and it seems always best to |
| keep them together in a single header file (thereby breaking the |
| rule to have a separate header file for each class).</P> |
| <LI><P STYLE="margin-bottom: 0.1cm"><A NAME="autoptrfriend"></A>Logically, only <FONT SIZE=2><FONT FACE="Courier New, monospace">ObjectContainer1</FONT></FONT> |
| is allowed to create and destroy instances of <FONT SIZE=2><FONT FACE="Courier New, monospace">Object1</FONT></FONT>, |
| hence the design decision to make the constructor and destructor of |
| <FONT SIZE=2><FONT FACE="Courier New, monospace">Object1</FONT></FONT> |
| private and make <FONT SIZE=2><FONT FACE="Courier New, monospace">ObjectContainer1</FONT></FONT> |
| a friend of <FONT SIZE=2><FONT FACE="Courier New, monospace">Object1</FONT></FONT>. |
| But a <FONT SIZE=2><FONT FACE="Courier New, monospace">std::auto_ptr< |
| Object1 ></FONT></FONT> is needed at one place in the |
| implementation, and that <FONT SIZE=2><FONT FACE="Courier New, monospace">std::auto_ptr</FONT></FONT> |
| also needs access to the destructor of <FONT SIZE=2><FONT FACE="Courier New, monospace">Object1</FONT></FONT>. |
| It seemed best to simply make <FONT SIZE=2><FONT FACE="Courier New, monospace">std::auto_ptr< |
| Object1 ></FONT></FONT> a friend of <FONT SIZE=2><FONT FACE="Courier New, monospace">Object1</FONT></FONT>, |
| too, in this situation (though too many friends can be an indication |
| of bad design.) Alas, the Microsoft compiler used on the Windows x86 platform |
| (<FONT SIZE=2><FONT FACE="Courier New, monospace">WNT</FONT></FONT> define) |
| needs to see <FONT SIZE=2><FONT FACE="Courier New, monospace">struct</FONT></FONT> (or nothing) in this friend declaration, |
| instead of the correct <FONT SIZE=2><FONT FACE="Courier New, monospace">class</FONT></FONT>, so some |
| platform-dependent code is needed here (and in retrospect it might seem better to make the destructor of |
| <FONT SIZE=2><FONT FACE="Courier New, monospace">Object1</FONT></FONT> public and get rid of all this).</P> |
| </UL> |
| <P STYLE="margin-bottom: 0.1cm">The corresponding implementation file is |
| <A HREF="http://www.openoffice.org/source/browse/ucb/ucb/workben/cachemap/cachemapobject1.cxx"><FONT SIZE=2><FONT FACE="Courier New, monospace">cachemapobject1.cxx</FONT></FONT></A>:</P> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <HR SIZE=1 NOSHADE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <PRE>#ifndef INCLUDED_UCB_CACHEMAPOBJECT1_HXX |
| <A HREF="#orderofincludes">#include "cachemapobject1.hxx"</A> |
| #endif |
| |
| #ifndef _OSL_DIAGNOSE_H_ |
| #include "osl/diagnose.h" |
| #endif |
| #ifndef _OSL_INTERLOCK_H_ |
| #include "osl/interlck.h" |
| #endif |
| #ifndef _OSL_MUTEX_HXX_ |
| #include "osl/mutex.hxx" |
| #endif |
| #ifndef _RTL_REF_HXX_ |
| #include "rtl/ref.hxx" |
| #endif |
| #ifndef _RTL_USTRING_HXX_ |
| #include "rtl/ustring.hxx" |
| #endif |
| |
| #ifndef INCLUDED_MEMORY |
| #include <memory> |
| #define INCLUDED_MEMORY |
| #endif |
| |
| using ucb::cachemap::Object1; |
| using ucb::cachemap::ObjectContainer1; |
| |
| inline |
| Object1::Object1(rtl::Reference< ObjectContainer1 > const & rContainer): |
| m_xContainer(rContainer), |
| m_nRefCount(0) |
| { |
| OSL_ASSERT(m_xContainer.is()); |
| } |
| |
| inline Object1::~Object1() SAL_THROW(()) |
| {} |
| |
| void ObjectContainer1::releaseElement(Object1 * pElement) SAL_THROW(()) |
| { |
| OSL_ASSERT(pElement); |
| bool bDelete = false; |
| { |
| osl::MutexGuard aGuard(m_aMutex); |
| if (osl_decrementInterlockedCount(&pElement->m_nRefCount) == 0) |
| { |
| m_aMap.erase(pElement->m_aContainerIt); |
| bDelete = true; |
| } |
| } |
| <A HREF="#minimalmutex">if (bDelete)</A> |
| delete pElement; |
| } |
| |
| ObjectContainer1::ObjectContainer1() |
| {} |
| |
| ObjectContainer1::~ObjectContainer1() SAL_THROW(()) |
| {} |
| |
| rtl::Reference< Object1 > ObjectContainer1::get(rtl::OUString const & rKey) |
| { |
| osl::MutexGuard aGuard(m_aMutex); |
| Map::iterator aIt(m_aMap.find(rKey)); |
| if (aIt == m_aMap.end()) |
| { |
| <A HREF="#autoptr">std::auto_ptr< Object1 ></A> xElement(new Object1(this)); |
| aIt = m_aMap.insert(Map::value_type(rKey, xElement.get())).first; |
| aIt->second->m_aContainerIt = aIt; |
| xElement.release(); |
| } |
| return aIt->second; |
| }</PRE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <HR SIZE=1 NOSHADE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <UL> |
| <LI><P STYLE="margin-bottom: 0.1cm"><A NAME="orderofincludes"></A>It is always a good idea to |
| include the header file that belongs to an implementation file first |
| in that file. That way, there is an automatic check that each header |
| file is self-contained (for almost every header, there is a |
| corresponding implementation file, and compilation of that file |
| fails if the header is not self-contained). (See <A HREF="http://cseng.aw.com/book/0,,0201633620,00.html">Lakos</A>, |
| section 3.2.)</P> |
| <LI><P STYLE="margin-bottom: 0.1cm"><A NAME="minimalmutex"></A>Note how <FONT SIZE=2><FONT FACE="Courier New, monospace">releaseElement()</FONT></FONT> |
| deletes the element only after the mutex has been released. It is |
| good practice to do as little as absolutely necessary during the |
| time a mutex is locked.</P> |
| <LI><P STYLE="margin-bottom: 0.1cm"><A NAME="autoptr"></A>Note how a <FONT SIZE=2><FONT FACE="Courier New, monospace">std::auto_ptr< |
| Object1 ></FONT></FONT> is used to ensure the newly created |
| <FONT SIZE=2><FONT FACE="Courier New, monospace">Object1</FONT></FONT> |
| is properly deleted in case the <FONT SIZE=2><FONT FACE="Courier New, monospace">insert()</FONT></FONT> |
| happens to throw an exception.</P> |
| </UL> |
| <P STYLE="margin-bottom: 0.1cm">The way the problems mentionend in the introduction are solved |
| here is that <FONT SIZE=2><FONT FACE="Courier New, monospace">ObjectContainer1</FONT></FONT> |
| only holds pointers to instances of <FONT SIZE=2><FONT FACE="Courier New, monospace">Object1</FONT></FONT>, |
| not refcounting references, and that <FONT SIZE=2><FONT FACE="Courier New, monospace">Object1::release()</FONT></FONT> |
| is simply forwarded to <FONT SIZE=2><FONT FACE="Courier New, monospace">ObjectContainer1::releaseElement()</FONT></FONT>. |
| The <FONT SIZE=2><FONT FACE="Courier New, monospace">releaseElement()</FONT></FONT> |
| method checks whether the refcount of the object drops to zero and |
| removes it from the map, if appropriate. The methods |
| <FONT SIZE=2><FONT FACE="Courier New, monospace">ObjectContainer1::releaseElement()</FONT></FONT> |
| and <FONT SIZE=2><FONT FACE="Courier New, monospace">ObjectContainer1::get()</FONT></FONT> |
| share a mutex, so that there can never be the situation that one |
| thread tries to get an object another thread is about to delete. The |
| disadvantage of this solution is that every call to |
| <FONT SIZE=2><FONT FACE="Courier New, monospace">Object1::release()</FONT></FONT> |
| needs to acquire a mutex, which is quite expensive on typical |
| platforms.</P> |
| <P ALIGN=LEFT><BR></P> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100% BGCOLOR="#666699"> |
| <H3 ALIGN=LEFT STYLE="margin-top: 0cm; text-decoration: none"><A NAME="Solution2"></A> |
| <FONT COLOR="#ffffff"><FONT SIZE=4>Solution 2 (Weak References)</FONT></FONT></H3> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <P STYLE="margin-bottom: 0.1cm">The UNO concept of weak references offers itself as another |
| solution to the problem: <FONT SIZE=2><FONT FACE="Courier New, monospace">ObjectContainer2</FONT></FONT> |
| holds weak references to instances of <FONT SIZE=2><FONT FACE="Courier New, monospace">Object2</FONT></FONT>, |
| and the mechanism used in the <FONT SIZE=2><FONT FACE="Courier New, monospace">ObjectContainer2::get()</FONT></FONT> |
| method to translate a weak reference to an <FONT SIZE=2><FONT FACE="Courier New, monospace">Object2</FONT></FONT> |
| into a hard reference (that may then turn out to be null) solves all |
| the problems for us.</P> |
| <P STYLE="margin-bottom: 0.1cm">With this solution, only <FONT SIZE=2><FONT FACE="Courier New, monospace">ObjectContainer2</FONT></FONT> |
| needs to know <FONT SIZE=2><FONT FACE="Courier New, monospace">Object2</FONT></FONT>, |
| and not also the other way around (as it was in the first solution |
| due to the mutual friend-ness of the two classes). Therefore, there |
| are individual header files for <FONT SIZE=2><FONT FACE="Courier New, monospace">Object2</FONT></FONT> |
| and <FONT SIZE=2><FONT FACE="Courier New, monospace">ObjectContainer2</FONT></FONT>.</P> |
| <P STYLE="margin-bottom: 0.1cm">The header file |
| <A HREF="http://www.openoffice.org/source/browse/ucb/ucb/workben/cachemap/cachemapobject2.hxx"><FONT SIZE=2><FONT FACE="Courier New, monospace">cachemapobject2.hxx</FONT></FONT></A> |
| is nothing more than a skeleton (<FONT SIZE=2><FONT FACE="Courier New, monospace">Object2</FONT></FONT> |
| is so bare-bones it does not even need an implementation file):</P> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <HR SIZE=1 NOSHADE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <PRE>#ifndef INCLUDED_UCB_CACHEMAPOBJECT2_HXX |
| #define INCLUDED_UCB_CACHEMAPOBJECT2_HXX |
| |
| #ifndef _CPPUHELPER_WEAK_HXX_ |
| #include "cppuhelper/weak.hxx" |
| #endif |
| |
| namespace ucb { namespace cachemap { |
| |
| class Object2: public cppu::OWeakObject |
| {}; |
| |
| } } |
| |
| #endif // INCLUDED_UCB_CACHEMAPOBJECT2_HXX</PRE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <HR SIZE=1 NOSHADE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <P STYLE="margin-bottom: 0.1cm">The header file |
| <A HREF="http://www.openoffice.org/source/browse/ucb/ucb/workben/cachemap/cachemapobjectcontainer2.hxx"><FONT SIZE=2><FONT FACE="Courier New, monospace">cachemapobjectcontainer2.hxx</FONT></FONT></A> |
| is hardly more interesting:</P> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <HR SIZE=1 NOSHADE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <PRE>#ifndef INCLUDED_UCB_CACHEMAPOBJECTCONTAINER2_HXX |
| #define INCLUDED_UCB_CACHEMAPOBJECTCONTAINER2_HXX |
| |
| #ifndef _CPPUHELPER_WEAKREF_HXX_ |
| #include "cppuhelper/weakref.hxx" |
| #endif |
| #ifndef _OSL_MUTEX_HXX_ |
| #include "osl/mutex.hxx" |
| #endif |
| #ifndef _RTL_REF_HXX_ |
| #include "rtl/ref.hxx" |
| #endif |
| #ifndef _SAL_TYPES_H_ |
| #include "sal/types.h" |
| #endif |
| |
| #ifndef INCLUDED_HASH_MAP |
| #include <hash_map> |
| #define INCLUDED_HASH_MAP |
| #endif |
| |
| namespace rtl { |
| class OUString; |
| struct OUStringHash; |
| } |
| namespace ucb { namespace cachemap { class Object2; } } |
| |
| namespace ucb { namespace cachemap { |
| |
| class ObjectContainer2 |
| { |
| public: |
| ObjectContainer2(); |
| |
| ~ObjectContainer2() SAL_THROW(()); |
| |
| rtl::Reference< Object2 > get(rtl::OUString const & rKey); |
| |
| private: |
| typedef <A HREF="#hashmap">std::hash_map</A>< rtl::OUString, |
| com::sun::star::uno::WeakReference< Object2 >, |
| rtl::OUStringHash > |
| Map; |
| |
| ObjectContainer2(ObjectContainer2 &); // not implemented |
| void operator =(ObjectContainer2); // not implemented |
| |
| Map m_aMap; |
| osl::Mutex m_aMutex; |
| }; |
| |
| } } |
| |
| #endif // INCLUDED_UCB_CACHEMAPOBJECTCONTAINER2_HXX</PRE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <HR SIZE=1 NOSHADE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <UL> |
| <LI><P STYLE="margin-bottom: 0.1cm"><A NAME="hashmap"></A>Here, a (de facto standard?) |
| <FONT SIZE=2><FONT FACE="Courier New, monospace">std::hash_map</FONT></FONT> |
| is used instead of a <FONT SIZE=2><FONT FACE="Courier New, monospace">std::map</FONT></FONT> |
| as used for <FONT SIZE=2><FONT FACE="Courier New, monospace">ObjectContainer1</FONT></FONT>: |
| We do not need any guarantees about iterators, so there is no reason |
| not to use the (potentially faster) <FONT SIZE=2><FONT FACE="Courier New, monospace">std::hash_map</FONT></FONT>.</P> |
| </UL> |
| <P STYLE="margin-bottom: 0.1cm">The solution to the implementation problems is hidden in the |
| implementation of <FONT SIZE=2><FONT FACE="Courier New, monospace">cppu::OWeakObject</FONT></FONT> |
| and <FONT SIZE=2><FONT FACE="Courier New, monospace">com::sun::star::uno::WeakReference</FONT></FONT>, |
| so the implementation file |
| <A HREF="http://www.openoffice.org/source/browse/ucb/ucb/workben/cachemap/cachemapobjectcontainer2.cxx"><FONT SIZE=2><FONT FACE="Courier New, monospace">cachemapobjectcontainer2.cxx</FONT></FONT></A> |
| is quite simple, too:</P> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <HR SIZE=1 NOSHADE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <PRE>#ifndef INCLUDED_UCB_CACHEMAPOBJECTCONTAINER2_HXX |
| #include "cachemapobjectcontainer2.hxx" |
| #endif |
| |
| #ifndef INCLUDED_UCB_CACHEMAPOBJECT2_HXX |
| #include "cachemapobject2.hxx" |
| #endif |
| |
| #ifndef _COM_SUN_STAR_UNO_REFERENCE_HXX_ |
| #include "com/sun/star/uno/Reference.hxx" |
| #endif |
| #ifndef _COM_SUN_STAR_UNO_XWEAK_HPP_ |
| #include "com/sun/star/uno/XWeak.hpp" |
| #endif |
| #ifndef _CPPUHELPER_WEAKREF_HXX_ |
| #include "cppuhelper/weakref.hxx" |
| #endif |
| #ifndef _OSL_MUTEX_HXX_ |
| #include "osl/mutex.hxx" |
| #endif |
| #ifndef _RTL_REF_HXX_ |
| #include "rtl/ref.hxx" |
| #endif |
| #ifndef _RTL_USTRING_HXX_ |
| #include "rtl/ustring.hxx" |
| #endif |
| |
| using namespace com::sun; |
| using ucb::cachemap::Object2; |
| using ucb::cachemap::ObjectContainer2; |
| |
| ObjectContainer2::ObjectContainer2() |
| {} |
| |
| ObjectContainer2::~ObjectContainer2() SAL_THROW(()) |
| {} |
| |
| rtl::Reference< Object2 > ObjectContainer2::get(rtl::OUString const & rKey) |
| { |
| rtl::Reference< Object2 > xElement; |
| { |
| osl::MutexGuard aGuard(m_aMutex); |
| Map::iterator aIt(m_aMap.find(rKey)); |
| if (aIt != m_aMap.end()) |
| xElement = static_cast< Object2 * >( |
| star::uno::Reference< star::uno::XWeak >( |
| aIt->second.get(), star::uno::UNO_QUERY). |
| get()); |
| if (!xElement.is()) |
| { |
| xElement = new Object2; |
| m_aMap[rKey] |
| = star::uno::WeakReference< Object2 >(xElement.get()); |
| } |
| } |
| return xElement; |
| }</PRE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <HR SIZE=1 NOSHADE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <P STYLE="margin-bottom: 0.1cm">The main trick (hack?) here is to translate from a |
| <FONT SIZE=2><FONT FACE="Courier New, monospace">com::sun::star::uno::WeakReference< |
| Object2 ></FONT></FONT> via a <FONT SIZE=2><FONT FACE="Courier New, monospace">com::sun::star::uno::Reference< |
| com::sun::star::uno::XWeak ></FONT></FONT> to an <FONT SIZE=2><FONT FACE="Courier New, monospace">rtl::Reference< |
| Object2 ></FONT></FONT> (in general, you must go from a |
| <FONT SIZE=2><FONT FACE="Courier New, monospace">com::sun::star::uno::WeakReference< |
| <I>T</I> ></FONT></FONT> to a <FONT SIZE=2><FONT FACE="Courier New, monospace">com::sun::star::uno::Reference< |
| <I>T</I> ></FONT></FONT>, but it is not possible to have a |
| <FONT SIZE=2><FONT FACE="Courier New, monospace">com::sun::star::uno::Reference< |
| Object2 ></FONT></FONT>, so we must go via the base class <FONT SIZE=2><FONT FACE="Courier New, monospace">XWeak</FONT></FONT> |
| here).</P> |
| <P STYLE="margin-bottom: 0.1cm">The drawback of this solution is that it is not faster than the |
| initial one (even slower on some platforms). The overhead of using |
| the (generic) mechanism of weak references when requesting objects |
| outweighs the benefits of the faster refcounting.</P> |
| <P STYLE="margin-bottom: 0.1cm">Another point that can well be a drawback of this solution is that |
| a weak reference to a destroyed object is not removed from the map; |
| it is replaced with a weak references to a living object the next |
| time <FONT SIZE=2><FONT FACE="Courier New, monospace">ObjectContainer2::get()</FONT></FONT> |
| requests that object, but it is never erased from the map. Therefore, |
| the map itself keeps growing over the lifetime of the |
| <FONT SIZE=2><FONT FACE="Courier New, monospace">ObjectContainer2</FONT></FONT> |
| instance.</P> |
| <P ALIGN=LEFT><BR></P> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100% BGCOLOR="#666699"> |
| <H3 ALIGN=LEFT STYLE="margin-top: 0cm; text-decoration: none"><A NAME="Solution3"></A> |
| <FONT COLOR="#ffffff"><FONT SIZE=4>Solution 3 (Best of Both Worlds)</FONT></FONT></H3> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <P STYLE="margin-bottom: 0.1cm">If exploiting the generic facilities in the second solution did |
| not give any performance advantage, let us try to become faster with |
| a hand-crafted version (i.e, combining the hand-crafted framework of |
| the first solution with the implementation details of the second |
| one).</P> |
| <P STYLE="margin-bottom: 0.1cm">The header file |
| <A HREF="http://www.openoffice.org/source/browse/ucb/ucb/workben/cachemap/cachemapobject3.hxx"><FONT SIZE=2><FONT FACE="Courier New, monospace">cachemapobject3.hxx</FONT></FONT></A> |
| is almost identical to the header file from the first solution |
| (differences are in <FONT COLOR="#008000">green</FONT>):</P> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <HR SIZE=1 NOSHADE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <PRE>#ifndef INCLUDED_UCB_CACHEMAPOBJECT3_HXX |
| #define INCLUDED_UCB_CACHEMAPOBJECT3_HXX |
| |
| #ifndef _OSL_INTERLOCK_H_ |
| #include "osl/interlck.h" |
| #endif |
| #ifndef _OSL_MUTEX_HXX_ |
| #include "osl/mutex.hxx" |
| #endif |
| #ifndef _RTL_REF_HXX_ |
| #include "rtl/ref.hxx" |
| #endif |
| #ifndef _SAL_TYPES_H_ |
| #include "sal/types.h" |
| #endif |
| #ifndef _SALHELPER_SIMPLEREFERENCEOBJECT_HXX_ |
| #include "salhelper/simplereferenceobject.hxx" |
| #endif |
| |
| #ifndef INCLUDED_MAP |
| #include <map> |
| #define INCLUDED_MAP |
| #endif |
| #ifndef INCLUDED_MEMORY |
| #include <memory> |
| #define INCLUDED_MEMORY |
| #endif |
| |
| namespace rtl { class OUString; } |
| namespace ucb { namespace cachemap { class Object3; } } |
| |
| namespace ucb { namespace cachemap { |
| |
| class ObjectContainer3: public salhelper::SimpleReferenceObject |
| { |
| public: |
| ObjectContainer3(); |
| |
| virtual ~ObjectContainer3() SAL_THROW(()); |
| |
| rtl::Reference< Object3 > get(rtl::OUString const & rKey); |
| |
| private: |
| typedef std::map< rtl::OUString, Object3 * > Map; |
| |
| Map m_aMap; |
| osl::Mutex m_aMutex; |
| |
| void releaseElement(Object3 * pElement) SAL_THROW(()); |
| |
| friend class Object3; // to access Map, releaseElement() |
| }; |
| |
| class Object3 |
| { |
| public: |
| inline void acquire() SAL_THROW(()) |
| { osl_incrementInterlockedCount(&m_nRefCount); } |
| |
| <FONT COLOR="#008000">void release() SAL_THROW(());</FONT> |
| |
| private: |
| rtl::Reference< ObjectContainer3 > m_xContainer; |
| ObjectContainer3::Map::iterator m_aContainerIt; |
| oslInterlockedCount m_nRefCount; |
| |
| inline Object3(rtl::Reference< ObjectContainer3 > const & rContainer); |
| |
| inline ~Object3() SAL_THROW(()); |
| |
| Object3(Object3 &); // not implemented |
| void operator =(Object3); // not implemented |
| |
| friend class ObjectContainer3; |
| // to access m_aContainerIt, m_nRefCount, Object3(), ~Object3() |
| #if defined WNT |
| friend struct std::auto_ptr< Object3 >; // to access ~Object3() |
| #else // WNT |
| friend class std::auto_ptr< Object3 >; // to access ~Object3() |
| #endif // WNT |
| }; |
| |
| } } |
| |
| #endif // INCLUDED_UCB_CACHEMAPOBJECT3_HXX</PRE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <HR SIZE=1 NOSHADE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <P STYLE="margin-bottom: 0.1cm">Also, the implementation file |
| <A HREF="http://www.openoffice.org/source/browse/ucb/ucb/workben/cachemap/cachemapobject3.cxx"><FONT SIZE=2><FONT FACE="Courier New, monospace">cachemapobject3.cxx</FONT></FONT></A> |
| has a strong resemblance to the implementation file from the first |
| solution (again, differences in <FONT COLOR="#008000">green</FONT>):</P> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <HR SIZE=1 NOSHADE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <PRE>#ifndef INCLUDED_UCB_CACHEMAPOBJECT3_HXX |
| #include "cachemapobject3.hxx" |
| #endif |
| |
| #ifndef _OSL_DIAGNOSE_H_ |
| #include "osl/diagnose.h" |
| #endif |
| #ifndef _OSL_INTERLOCK_H_ |
| #include "osl/interlck.h" |
| #endif |
| #ifndef _OSL_MUTEX_HXX_ |
| #include "osl/mutex.hxx" |
| #endif |
| #ifndef _RTL_REF_HXX_ |
| #include "rtl/ref.hxx" |
| #endif |
| #ifndef _RTL_USTRING_HXX_ |
| #include "rtl/ustring.hxx" |
| #endif |
| |
| #ifndef INCLUDED_MEMORY |
| #include <memory> |
| #define INCLUDED_MEMORY |
| #endif |
| |
| using ucb::cachemap::Object3; |
| using ucb::cachemap::ObjectContainer3; |
| |
| inline |
| Object3::Object3(rtl::Reference< ObjectContainer3 > const & rContainer): |
| m_xContainer(rContainer), |
| m_nRefCount(0) |
| { |
| OSL_ASSERT(m_xContainer.is()); |
| } |
| |
| inline Object3::~Object3() SAL_THROW(()) |
| {} |
| |
| <FONT COLOR="#008000">void Object3::release() SAL_THROW(())</FONT> |
| <FONT COLOR="#008000">{</FONT> |
| <FONT COLOR="#008000"> if (osl_decrementInterlockedCount(&m_nRefCount) == 0)</FONT> |
| <FONT COLOR="#008000"> {</FONT> |
| <FONT COLOR="#008000"> m_xContainer->releaseElement(this);</FONT> |
| <FONT COLOR="#008000"> delete this;</FONT> |
| <FONT COLOR="#008000"> }</FONT> |
| <FONT COLOR="#008000">}</FONT> |
| |
| void ObjectContainer3::releaseElement(Object3 * pElement) SAL_THROW(()) |
| { |
| OSL_ASSERT(pElement); |
| <FONT COLOR="#008000">osl::MutexGuard aGuard(m_aMutex);</FONT> |
| <FONT COLOR="#008000"> if (pElement->m_aContainerIt != m_aMap.end())</FONT> |
| <FONT COLOR="#008000"> m_aMap.erase(pElement->m_aContainerIt);</FONT> |
| } |
| |
| ObjectContainer3::ObjectContainer3() |
| {} |
| |
| ObjectContainer3::~ObjectContainer3() SAL_THROW(()) |
| {} |
| |
| rtl::Reference< Object3 > ObjectContainer3::get(rtl::OUString const & rKey) |
| { |
| osl::MutexGuard aGuard(m_aMutex); |
| Map::iterator aIt(m_aMap.find(rKey)); |
| <FONT COLOR="#008000"> if (aIt == m_aMap.end())</FONT> |
| <FONT COLOR="#008000"> {</FONT> |
| <FONT COLOR="#008000"> std::auto_ptr< Object3 > xElement(new Object3(this));</FONT> |
| <FONT COLOR="#008000"> aIt = m_aMap.insert(Map::value_type(rKey, xElement.get())).first;</FONT> |
| <FONT COLOR="#008000"> aIt->second->m_aContainerIt = aIt;</FONT> |
| <FONT COLOR="#008000"> xElement.release();</FONT> |
| <FONT COLOR="#008000"> return aIt->second;</FONT> |
| <FONT COLOR="#008000"> }</FONT> |
| <FONT COLOR="#008000"> else if (osl_incrementInterlockedCount(&aIt->second->m_nRefCount) > 1)</FONT> |
| <FONT COLOR="#008000"> {</FONT> |
| <FONT COLOR="#008000"> rtl::Reference< Object3 > xElement(aIt->second);</FONT> |
| <FONT COLOR="#008000"> osl_decrementInterlockedCount(&aIt->second->m_nRefCount);</FONT> |
| <FONT COLOR="#008000"> return xElement;</FONT> |
| <FONT COLOR="#008000"> }</FONT> |
| <FONT COLOR="#008000"> else</FONT> |
| <FONT COLOR="#008000"> {</FONT> |
| <FONT COLOR="#008000"> osl_decrementInterlockedCount(&aIt->second->m_nRefCount);</FONT> |
| <FONT COLOR="#008000"> aIt->second->m_aContainerIt = m_aMap.end();</FONT> |
| <FONT COLOR="#008000"> aIt->second = new Object3(this);</FONT> |
| <FONT COLOR="#008000"> aIt->second->m_aContainerIt = aIt;</FONT> |
| <FONT COLOR="#008000"> return aIt->second;</FONT> |
| <FONT COLOR="#008000"> }</FONT> |
| }</PRE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <HR SIZE=1 NOSHADE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <P STYLE="margin-bottom: 0.1cm">This implementation does not need to lock a mutex in every call to |
| <FONT SIZE=2><FONT FACE="Courier New, monospace">Object3::release()</FONT></FONT>. |
| Only if the refcount drops to zero is the mutex of the |
| <FONT SIZE=2><FONT FACE="Courier New, monospace">ObjectContainer3</FONT></FONT> |
| locked and the object removed from the map. It is possible that an |
| <FONT SIZE=2><FONT FACE="Courier New, monospace">ObjectContainer3::get()</FONT></FONT> |
| gets in the way between dropping the refcount to zero and locking the |
| mutex. But the <FONT SIZE=2><FONT FACE="Courier New, monospace">get()</FONT></FONT> |
| method notices that (it goes through a cycle of incrementing and |
| decrementing the refcount itself, and if the refcount is not larger |
| than 1, the object is about to be deleted) and leaves alone the |
| to-be-deleted object and inserts into the map a fresh instance |
| instead.</P> |
| <P STYLE="margin-bottom: 0.1cm">Note that this implementation also does not have the drawback of |
| an ever-growing map, because each object is removed from the map as |
| soon as its refcount drops to zero.</P> |
| <P ALIGN=LEFT><BR></P> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100% BGCOLOR="#666699"> |
| <H3 ALIGN=LEFT STYLE="margin-top: 0cm; text-decoration: none"><A NAME="Measurements"></A> |
| <FONT COLOR="#ffffff"><FONT SIZE=4>Performance Measurements</FONT></FONT></H3> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <P STYLE="margin-bottom: 0.1cm">A simple test file |
| (<A HREF="http://www.openoffice.org/source/browse/ucb/ucb/workben/cachemap/cachemaptest.cxx"><FONT SIZE=2><FONT FACE="Courier New, monospace">cachemaptest.cxx</FONT></FONT></A>) |
| makes 200000 <FONT SIZE=2><FONT FACE="Courier New, monospace">get()</FONT></FONT> |
| requests, half of which find objects already present in the map. The |
| refcount of each requested object is incremented and decremented 50 |
| times in a row. Only 100 different identifiers are used for the |
| objects, so that the second solution does not suffer from the problem |
| of an ever-growing map. The measurements for two of our production |
| platforms (Solaris Sparc with Forte compiler and Windows x86 with |
| Microsoft compiler) are as follows (all times in milliseconds; because |
| of different machines used, only comparisons within each column are |
| meaningful):</P> |
| <TABLE WIDTH=100% BORDER=1 CELLPADDING=4 CELLSPACING=3 STYLE="page-break-inside: avoid"> |
| <COL WIDTH=85*> |
| <COL WIDTH=85*> |
| <COL WIDTH=85*> |
| <TR VALIGN=TOP> |
| <TD WIDTH=33%> |
| <P><BR> |
| </P> |
| </TD> |
| <TD WIDTH=33%> |
| <P>unxsols3.pro</P> |
| </TD> |
| <TD WIDTH=33%> |
| <P>wntmsci7.pro</P> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=33% VALIGN=TOP> |
| <P>Solution 1</P> |
| </TD> |
| <TD WIDTH=33% VALIGN=BOTTOM SDVAL="9137" SDNUM="1031;"> |
| <P ALIGN=RIGHT>9137</P> |
| </TD> |
| <TD WIDTH=33% VALIGN=BOTTOM SDVAL="3846" SDNUM="1031;"> |
| <P ALIGN=RIGHT>3846</P> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=33% VALIGN=TOP> |
| <P>Solution 2</P> |
| </TD> |
| <TD WIDTH=33% VALIGN=BOTTOM SDVAL="8634" SDNUM="1031;"> |
| <P ALIGN=RIGHT>8634</P> |
| </TD> |
| <TD WIDTH=33% VALIGN=BOTTOM SDVAL="5598" SDNUM="1031;"> |
| <P ALIGN=RIGHT>5598</P> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=33% VALIGN=TOP> |
| <P>Solution 3</P> |
| </TD> |
| <TD WIDTH=33% VALIGN=BOTTOM SDVAL="3166" SDNUM="1031;"> |
| <P ALIGN=RIGHT>3166</P> |
| </TD> |
| <TD WIDTH=33% VALIGN=BOTTOM SDVAL="2704" SDNUM="1031;"> |
| <P ALIGN=RIGHT>2704</P> |
| </TD> |
| </TR> |
| </TABLE> |
| <P ALIGN=LEFT><BR></P> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <HR SIZE=1 NOSHADE> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100% BGCOLOR="#666699"> |
| <P ALIGN=LEFT><FONT COLOR="#ffffff">Author: <A HREF="mailto:bergmann@sun.com">Stephan |
| Bergmann</A> ($Date: 2001/06/14 14:49:37 $)<BR><I>Copyright 2001 |
| OpenOffice.org Foundation. All Rights Reserved.</I></FONT> |
| </P> |
| </TD> |
| </TR> |
| <TR> |
| <TD WIDTH=100%> |
| <HR SIZE=1 NOSHADE> |
| </TD> |
| </TR> |
| </TABLE> |
| <HR SIZE=3 NOSHADE> |
| </body> |
| </HTML> |