Some benchmarks of SOM vs C++ based objects

From EDM2
Jump to: navigation, search

Written by Gordon Zeglinski

Introduction

So are we sick of SOM yet? I hope not! This issue last will be the last column on SOM for a while. We will look at some benchmarks of SOM vs C++ based objects. Hopefully you are a curious as I am about the overhead built into SOM.

On with the Benchmark

What we will try to do here is write a series of tests to determine the overhead involved with calling a member function in SOM. Because we are interested only in the function call overhead, the function will simply increment a counter variable. This counter variable will be stored in the object's instance data. For comparison, a C++ version of the SOM object will be used to establish a base mark. The interface definition of the SOM object follows.

interface SOMFoo : SOMObject
{

   attribute long Count;          // Stores the Count Data

   void IncrementCount();        //procedure
   void OffIncrementCount();     //offset
   void LookIncrementCount();    //lookup

#ifdef __SOMIDL__
implementation
{

    releaseorder: _get_Count,_set_Count,IncrementCount,\
                  OffIncrementCount,LookIncrementCount;

    IncrementCount:     procedure;
    LookIncrementCount: namelookup;

    callstyle=oidl;
    filestem = sombench;
    somInit: override;  // initialize instance variables to 0
    somUninit: override; // just for fun

};
#endif /* __SOMIDL__ */
};

Three functions are defined to test the various resolution methods. IncrementCount is used to test the "procedure" resolution method. A procedure doesn't use a resolution method per se; a procedure is similar to a non-virtual C++ function because it is called by its address. OffIncrementCount is used to test the offset resolution method. This is the default method used by SOM to resolve a function call. This method is similar to virtual functions in C++. LookIncrementCount is used to test the name lookup resolution method.

A function can be labeled a procedure as follows:

IncrementCount:     procedure;

Because the default "type" for a function is method, one only has to list functions that are to be procedures.

Note: only methods can be overridden.

Similarily, one can specify that a method is to lookup offset resolution by using the following:

LookIncrementCount: namelookup;

Again, one only lists functions which deviate from the normal resolution method. The normal method is offset.

Note: there seems to be a bug in the C++ emitters. This bug causes incorrect code to be generated for the lookup resolution method.

For the C++ object, we use the following class definition:

class CPPfoo{
   int Count;          // Stores the Count Data

public:
   CPPfoo();
   ~CPPfoo();

   void IncrementCount();                //similar to SOM procedure
   void InlineIncrementCount(){Count++;} //no equivalence in SOM procedure
   virtual void OffIncrementCount();     //similar to SOM offset

   void SetCount(int C){Count=C;}
   int GetCount(){return Count;}

};

The C++ object duplicates the SOM object wherever possible. The two major differences between tests on the C++ and SOM objects are that the C++ object can't do "name lookup resolution" and the SOM object can't do inline code.

Now that we have seen the objects, we can look at how they will be used. Both the C++ and SOM objects are structured similarly, thus, it follows that both of the tests will be implemented similarly.

#define INCL_DOS
#include <os2.h>

#include "sombench.xh"
#include <stdio.h>

void main(){
   ULONG StartTime, EndTime;
   int i;

   SOMFoo *sfoo=new SOMFoo;

   DosQuerySysInfo(QSV_MS_COUNT,QSV_MS_COUNT,&StartTime,4); //Get Start
time using the system timer

   for(i=0;i<1000000;i++)

      SOMFoo::IncrementCount(sfoo);             //lets call the fnc a
million times

   DosQuerySysInfo(QSV_MS_COUNT,QSV_MS_COUNT,&EndTime,4);   //Get End time
we

   printf("Time for Procedure calls %i ms count=
%i\r\n",EndTime-StartTime,sfoo->_get_Count());

   sfoo->_set_Count(0);
   DosQuerySysInfo(QSV_MS_COUNT,QSV_MS_COUNT,&StartTime,4);
   for(i=0;i<1000000;i++)
      sfoo->OffIncrementCount();

   DosQuerySysInfo(QSV_MS_COUNT,QSV_MS_COUNT,&EndTime,4);

   printf("Time for Offset lookup calls %i ms count=
%i\r\n",EndTime-StartTime,sfoo->_get_Count());

   sfoo->_set_Count(0);
   DosQuerySysInfo(QSV_MS_COUNT,QSV_MS_COUNT,&StartTime,4);
   for(i=0;i<1000000;i++)
      lookup_LookIncrementCount(sfoo);
//      sfoo->LookIncrementCount();

   DosQuerySysInfo(QSV_MS_COUNT,QSV_MS_COUNT,&EndTime,4);

   printf("Time for Name lookup calls #1 %i ms count=
%i\r\n",EndTime-StartTime,sfoo->_get_Count());

   sfoo->_set_Count(0);
   DosQuerySysInfo(QSV_MS_COUNT,QSV_MS_COUNT,&StartTime,4);
   for(i=0;i<1000000;i++)
      (somResolveByName(sfoo,"LookIncrementCount") )(sfoo);

   DosQuerySysInfo(QSV_MS_COUNT,QSV_MS_COUNT,&EndTime,4);

   printf("Time for Name lookup calls #2 %i ms count=
%i\r\n",EndTime-StartTime,sfoo->_get_Count());

}

To determine the overhead in making the function call, we call the function 1 million times. The time it takes to call the function 1 million times along with the value of the incremented variable is displayed after each iteration. The results of the SOM test follow.

Time for Procedure calls 656 ms count= 1000000
Time for Offset lookup calls 719 ms count= 1000000
Time for Name lookup calls #1 22031 ms count= 1000000
Time for Name lookup calls #2 175031 ms count= 1000000

On a side note, The procedure IncrementCount is emitted as a static member function in C++. This is strange because you still have to pass it a pointer to the instance of the C++ object that wraps instance SOM object. This of course is exactly what a C++ member function does (in an implicit manner).

Running the tests on the C++ object yields the following.

Time for Procedure calls 219 ms count= 1000000
Time for Offset lookup calls 281 ms count= 1000000
Time for inline calls 0 ms count= 1000000

Examining the Results

As usual to give a benchmark meaning, the system it is run on has to be listed. The following list shows the syslevels of the various components involved in the tests.

                      IBM OS/2 Base Operating System
Version 2.11     Component ID 562107701
Type 0
Current CSD level: XR06200
Prior   CSD level: XR02110

                      IBM C/C++ Tools (compiler)
Version 2.00     Component ID 562201703
Current CSD level: CT00010
Prior   CSD level: CT00009

                   SOMobjects Developer Toolkit
Version 2.00     Component ID 96F8647TK
Current CSD level: SM20004
Prior   CSD level: SM00000

All of this was running on a 486 DX2-66 PC clone machine with 20 megs of RAM.

Let's start by looking at the C++ test. For 1 million calls to a member function, only 219 milliseconds was used. Calling a virtual function took just slightly more time. The inline case took no measurable amount of time. What happened most probably in the inline case is that the optimizer completely removed the loop and did a direct assignment instead. (I haven't checked the compiled code to verify if this is the case or not. If you are curious feel free to check it yourself.)

One of the arguments you see tossed around when arguing the merits of C++ versus C is that the additional amount of time it takes to call a virtual function compared to a normal "C function" is negligible. This test confirms those arguements. The overall amount of time used to call one a virtual function for "real cases" is even less important.

The offset resolution case in the SOM test took about 2.5 times longer to execute than did the virtual C++ function test. Recall that the offset resolution case in SOM is equivalent to the virtual function case in C++. There are several ways to call a function by it's name. In the first name lookup test, we use the macro created by the SOM compiler called lookup_LookIncrementCount. For the second test, we use somResolveByName. Note that the difference in execution time between the two lookup tests.

Even though the second name lookup case took much longer than the other cases, one should remember that this test does not necessarily represent a real world use of the somResolveByName function. This function was called a million times. If one were to write this in an efficient manner, only 1 call would be made to determine the address of the function. Once this address has been determined, it can be reused inside the loop. The time used by the revamped lookup test should then approach that of the procedure test.

One of the reasons the SOM version is slower than the C++ version is because, in SOM, the object's interface doesn't have to be fully disclosed. The base pointer to the data is not the same as the pointer to the objects instance. Thus, in SOM, one has to query the pointer for the instance data in order to access it. Consider the following SOM function:

SOM_Scope void  SOMLINK OffIncrementCount(SOMFoo *somSelf)
{
    SOMFooData *somThis = SOMFooGetData(somSelf);
//    SOMFooMethodDebug("SOMFoo","OffIncrementCount");

   (somThis->Count)++;
}

Note: I commented out the debug call to increase performance.

Now compare the SOM function to the following C++ member function:

void CPPfoo::IncrementCount(){
   Count++;
}

Notice that in the SOM function, we have to determine the value of somThis.

Finally a word of caution. OS/2's time slicing is far too coarse for high degrees of accuracy with numbers this small. A variation of 32ms (or so) may occur when running these tests, since that is the resolution of the clock used to preempt tasks running within the system.

Included Files

CPPTEST.CPP C++ test
SOMBENCH.IDL SOM idl file
SOMBENCH.CPP C++ implementation for SOM object
SOMBENCH.XH SOM header
SOMBENCH.XIH SOM implementation header
SOMBTEST.CPP main routine for SOM tests

Wrapping Things Up

That's it for another fun-filled look at SOM. It should be noted that there probably will not be an installment of this column next month while I wrap up the development of INTERcomm, a commercial communications package that I have developed.