Footprint Reduction of CORBA Client and Servers

Middleware News Brief (MNB) features news and technical information about Open Source middleware technologies.

INTRODUCTION

CORBA clients and servers can be used on a variety of computer systems ranging from small devices, such as embedded systems and handheld computers, to large mainframes.

Typically, on smaller computers CORBA is used to support clients. Because of the memory constraints of these smaller systems, it is often necessary to identify ways to reduce the memory footprint of the client code. Likewise, larger systems are typically used for running CORBA-based servers. Even though there may be a large amount of memory, if a large system runs a number of servers supporting a large number of interface definition language (IDL) operations, the memory may be stressed in this situation too.

This Middleware News Brief looks at some techniques that can be used to minimize the memory footprint of clients and servers written in C++ that use CORBA.

Application-specific servant code will not be considered here. Instead, the focus is on the size of the object code that is created by running the C++ compiler against the code generated by the IDL compiler. The size is defined here as the sum of the data and text (or code) sections of the object code. Note that the size(1) command is used to obtain this information, not ls(1).

settings blue

Sidebar

Version 1.2a of the ORB TAO, available from www.theaceorb.com, is used as the example ORB in the sections that follow. The C++ code is compiled using GNU C++ 3.2 on Pentium III computer running Linux. Some of the conclusions, of course, may be different if another ORB is used instead of TAO.

If you are using TAO's shared libraries, the footprint reduction steps given here can be used along with the Shared Library Reduction Tool under development by Object Computing. This tool analyzes what code in TAO's shared libraries is needed by a set of applications you provide. It then constructs a makefile that builds a version of the ACE and TAO shared libraries that contain only the code needed for these applications.

More information about this tool can be found in directory apps/sorereduce included in the TAO distribution.

IDL COMPILER OPTIONS

The IDL compiler supplied with your ORB may provide options that have an impact on memory footprint. For example, TAO's IDL compiler accepts a number of options to fine-tune the generated C++ code. There may be a run-time performance impact with using options that reduce footprint, therefore you should gauge the performance of your client or server code when using these options if run-time responsiveness is of critical concern.

Just as one programming language can be used for a CORBA-based server and other languages used for the clients, different IDL compiler options can be used for generating client-side proxy code and for generating server-side skeleton code. For example, IDL compiler options that favor run-time performance can be used on the server-side, while options that reduce footprint can be used for client-side code.

Below, some of the options that can impact memory footprint are listed. A complete description of all the options can be found in the TAO's Developer's Guide [1].

OptionDescription
-Sa Suppress CORBA Any support. If your interfaces do not need to be converted to CORBA Anys, this is an easy way to cut down on code size. For small interfaces, this can reduce the footprint significantly. For example, applying this option to the Empty interface discussed in the next section reduced the footprint by about 10%. Operations for insertion and extraction from Anys is done for every user-defined type.
-St Suppress Typecode support. This can be suppressed if CORBA TypeCodes do not need to be associated with your interfaces. Because CORBA Any requires TypeCode support, -Sa must also be passed. This only provides about 0.5% savings over applying -Sa to the Empty interface.
-Sc Suppress generation of tie classes. A tie class is generated for every interface definition encountered. However, in practice, tie classes are used very infrequently. (See section 9.9 of [2] for discussion about why tie classes provide limited utility.) In fact, tie class support is used so rarely that it was disabled by default in generating footprint values in this news brief. Note that if you are using Visual C++, you will need to remove any references to the tie files (files ending in S_T.cppS_T.h, and S_T.i) in the IDL custom build rule's Build Output list. Failure to do so will result in IDL compiler always being called when doing builds.
-H Specify the look up strategy to use for each interface definition encountered. This has minimal footprint impact. For example, using linear search instead of perfect hash resulted in only a 0.4% reduction in server code when compiling the OMG Trading Service IDL file [4] that includes more than 500 lines of IDL code and comes with TAO.

IDL CONSTRUCTS

This section looks at the footprint impact of using some of the types and constructs available in the IDL. Everyone wants a reduced footprint, but in many cases, not at the cost of reduced functionality. The goals of footprint reduction need to be carefully weighed against possible reduced functionality when making IDL changes such as those mentioned in this section.

THE Empty INTERFACE

It is illustrative to look at the code generated by simply defining an empty CORBA interface:

    interface Empty
    {
    };

For client-side support, the IDL compiler must generate class Empty_var, which acts as a smart pointer to an Empty (see section 6.19 of [2] for a discussion of _var classes).

Also generated is Empty_out, used to support passing _var objects as out parameters (section 7.14.3 of [2] discusses the purpose of _out parameters).

Finally, the proxy class Empty is generated. Each proxy class must provide member functions for duplicating and narrowing object references and forward each request to the server. Each ORB implementation is allowed to create additional classes to support the proxy class.

Using the versions of TAO and the GNU C++ compiler mentioned in the Sidebar, the footprint of the IDL generated client-side code is 15KB.

On the server-side, the IDL compiler must generate the skeleton class from which your servant will derive. The skeleton code is involved in the dispatching of each request to the appropriate servant member function. As is the case for client-side generated code, the IDL compiler is free to define additional member functions and classes as needed.

In the case of TAO, the client-side proxy code generated by the IDL compiler is also needed by the server-side skeleton code. Therefore, the footprint for the skeleton code includes the footprint for the proxy code. For the Empty interface, the skeleton code adds 16KB of memory to the footprint, bringing the total to 31KB.

USING INHERITANCE VERSES NOT USING INHERITANCE

Next we consider the effect of inheritance on footprint reduction. Specifically, we examine the IDL-generated code when an interface is changed from implicitly inheriting from another interface (all interfaces implicitly inherit from CORBA::Object) to explicitly inheriting from another interface.

    interface Interface1
    {
        void f();
    };
 
    interface Interface2
    {
        void f();
        void g();
    };

The resulting size for the client-side code is 48KB and 96KB for the server-side code.

Alternatively, now allow Interface2 to inherit from Interface1:

    interface Interface2 : Interface1
    {
        void g();
    };

By using inheritance, the size for the client-side and server-side code is reduced by 8% and 9% respectively. This is because Interface2 TAO proxy implementation classes can inherit code from Interface1 implementation classes.

For example, the member function f() for the proxy class Interface1 can be used by the proxy class Interface2 instead of defining a separate implementation.

If such inheritance is done primarily for footprint reduction reasons, this can be considered implementation inheritance.

Because IDL is an abstract specification, ideally you should not be using inheritance solely for footprint reduction reasons. However, when faced with such a choice during design, the footprint ramifications could be a weighing factor.

USING struct VERSES NOT USING struct

Consider the following simple interface:

    interface Contact
    {
        attribute string firstName;
        attribute string lastName;
    };

This gives a the client-size footprint and server-size footprint of 49KB and 97KB respectively.

Now suppose a struct is used to hold the name contents:

    struct NameStruct
    {
      string first;
      string last;
    };
 
    interface Contact
    {
      attribute NameStruct name;
    };

By using the struct, the client-size footprint is reduced by 23%. This is because the reduction by two of attribute set/get operations more than offset to additional code need to support passing a NameStruct.

On the server-side, footprint is reduced by 26%. The additional reduction is due to fewer dispatching functions being needed because a pair of set/get operations has been eliminated.

In this case, introducing a struct for the person's name makes sense because in most cases both the first and last name will be queried together instead of separately. Introducing structs that contain loosely coupled elements solely for footprint reduction reasons should be avoided if possible.

Note that the use of a struct in this case has versioning implications.

For example, in the original Contact interface that doesn't use a struct, if a middleName attribute is added, existing client code can be used without modification (of course middleName will not be available to these clients). However, if the NameStruct is modified to include middleName, client code must be recompiled to pick up this change, even if middleName is not used by these clients. This should be kept in mind when deciding whether to use struct to replace one or more operations.

USING A BOUNDED sequence VERSUS USING array

A one-dimensional CORBA array is semantically similar to a CORBA bounded sequence. An array maps to a C++ array, whereas a sequence maps to a non-trivial class, so some memory saving may be obtained by using an array instead of a sequence.

To assess the footprint saving of an array instead of a sequence, consider the following IDL file:

    typedef sequence<long, 100> SequenceLong;
 
    interface SeqTester
    {
        void f(in SequenceLong seq);
    };

The footprint for this is 32KB for client-side code and 58KB for server-side code.

Now compare with using an array:

    typedef long ArrayLong[100];
 
    interface SeqTester
    {
        void f(in ArrayLong array);
    };

The footprint for client code and server code is reduced by 10% and 5% respectively.

There are run-time performance implications in using an array over a bounded sequence (see section 4.7.7 of [2] for a good comparison between sequence and array). For example, the whole array is sent over the wire, whereas only the populated entries in a sequence are sent over the wire.

USING A union VERSUS NOT USING A union

Finally, we compare the memory usage of using a CORBA union and not using a union:

    union U switch (char)
    {
     case 'L':
         long l;
     case 'S':
         string s;
    };
 
    interface UnionTester
    {
        void f(in U val);
    };

The footprint for this is 31KB for client-side code and 58KB for server-side code.

Next we will avoid "overloading" the function and instead provide two separate functions:

    interface UnionTester
    {
        void f_long(in long val);
        void f_unsigned_long(in string val);
    };

Now the memory usage has increased by 7% for the client code and 15% for the server code. This is due to the extra proxy function and extra skeleton dispatching code included by introducing a new operation. This percent increase in footprint size will only get larger as more members are added to the union.

Some drawbacks to using unions are that the construction of the C++ object corresponding to the union must be done properly and there must be code equivalent to a switch statement to process the C++ union object.

FOOTPRINT REDUCTION ON THE CLIENT-SIDE ONLY

If you are primarily concerned with reducing the footprint for client code, another option is available. This involves applying the Interface Segregation Principle (ISP) [3]. This states that "fat" interfaces (large interfaces whose operations are not cohesive) should be avoided. Instead, separate, smaller interfaces should be available for the client to choose from in order to obtain the services of interest.

As an example of applying the ISP, suppose a large interface exists:

   interface FullService {
        void configure1(string s);
        void configure2();
        // Many more configure operations...
 
        void doService1();
        string doService2(in long val);
        // Many more service operations...
    };

A collection of smaller interfaces could be defined with each interface providing a front end to a group of operations from FullService:

    interface ServiceAdmin {
        void configure1(string s);
        long configure2();
        // More configure operations...
    };
 
    interface Service {
        void doService1();
        string doService2(in long val);
        // More service operations...
    };

If most clients do not need to administer FullService objects, they can work with just a reference to a Service object instead. Doing so can reduce the client footprint.

There are a number of ways for servers to implement this separation of operations into different interfaces. A few of these techniques are discussed here. These techniques involve having separate servants for implementing FullServiceServiceAdmin, and Service.

These servants are shown in the following UML diagram. The convention used here is that for CORBA a interface named Interface the corresponding C++ servant is named Interface_i.

Figure 1. Servant Inheritance

Figure 1. Servant Inheritance

USE AN OBJECT REFERENCE FOR OPERATION DELEGATION

The servants implementing interface ServiceAdmin and Service could contain an object reference to FullService that is running on the same server, or perhaps a different server. Each member function implementing a Service operation would then delegate to the FullService CORBA object the actual operation implementation. This approach requires no change to the FullService implementation.

The relationship between these servants looks like:

Figure 2. Delegate to Full Object Reference

Figure 2. Delegate to Full Object Reference

As an example, a candidate for applying the ISP is the OMG's Trading Service IDL file [4]. The Trading Service IDL file, which is included in TAO, contains over 500 IDL statements. Much of this related to administrative and configuration tasks or advanced features that may not be needed by most clients in a particular deployment scenario.

For the version of TAO and GNU C++ compiler mentioned in the Sidebar, the client footprint is close to 1MB. We'll examine ways to reduce the footprint for clients that do not need all the functionality provided by such large IDL files.

USE A FullService_i REFERENCE FOR OPERATION DELEGATION

This design is similar to the previous design, except the servants ServiceAdmin_i and Service_i have a reference to a FullService_i object. This requires that all three servants be running in the same process.

This approach offers better performance than the previous design because the delegation is done through a virtual C++ call instead of through a remote CORBA invocation. However, it does require modification to the server running FullService_i.

In this design the relationship between these servants looks like:

Figure 3. Delegate to Full

Figure 3. Delegate to Full

FullService_i DELEGATES TO OTHER SERVANTS

Finally, a third design is given that is similar to the second design except the delegation is "reversed." The FullService_i object delegates to ServiceAdmin_i and Service_i.

The main advantage of this approach is that is aids maintenance of the servant code.

It may be tempting to try to use multiple inheritance instead of delegation, but this is not possible because FullService_i must inherit from POA_FullService generated the IDL compiler.

Although the FullService_i interface may be fat, the implementation can be fairly "skinny" because it is using delegation for all of its operations. The drawback to the approach is that it would require major modification to any existing FullService_i code.

The relationship among the servants is as follows:

Figure 4. Delegate From Full

Figure 4. Delegate From Full

SUMMARY

This news brief has shown ways to reduce the memory footprint of code generated by a CORBA IDL compiler. This includes:

  • Selecting IDL compiler options carefully
  • Favoring certain IDL constructs over others where appropriate
  • Hiding large interfaces behind smaller, more cohesive interfaces

By learning the characteristics of your IDL compiler and applying the above steps you can increase the likelihood of being able to use the power and portability of CORBA under constrained memory conditions.

REFERENCES

  1. The TAO Developer's Guide is available at the OCI TAO web site: 
    http://www.theaceorb.com/product/
  2. Henning, Michi and Vinoski, Steve. Advanced CORBA Programming with C++, Addison-Wesley, 1999.
  3. Martin, Robert. The Interface Segregation Principle
    www.objectmentor.com/publications/isp.pdf
  4. The CORBA Trading Object Service, published by the OMG.
    www.omg.org/technology/documents/formal/trading_object_service.htm
secret