OOPS AvenueWritten by Gordon Zeglinski |
IntroductionSo 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 BenchmarkWhat 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= 1000000On 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 ResultsAs 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: SM00000All 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 FilesCPPTEST.CPP C++ test Wrapping Things UpThat'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. |