Open CASCADE Handles. Let's handle'em. Part 2

by - 23:40

(continued)

*Standard_Transient and MMgt_TShared*
If you are attentive enough, you will likely notice that most handle classes inherit MMgt_TShared, while it does not add any value to Standard_Transient. The comments in the cdl file claims it supports reference counting. Well, reference counter is inside Standard_Transient ! I dove into the source code of Open CASCADE 3.0 (released in 1999, the first open source release) and it is the same. So, it seems this change was done more than ten years ago, but the orphan MMgt_TShared is still alive. So, I am now using Standard_Transient only and advised OCC folks to remove MMgt_TShared eventually. So, you might want to migrate to Standard_Transient as your base class as well.

*Cyclic dependencies*
The underlying objects won’t get destroyed if you have cyclic dependence (i.e. one object referring to the other), and you will end up with a memory leak. For instance the following class is prone to such a cyclic dependency:

DEFINE_STANDARD_HANDLE(OCC_UT_Employee,Standard_Transient)
class OCC_UT_Employee : public Standard_Transient
{
public:
OCC_UT_Employee (const Handle(OCC_UT_Employee)& theBoss) : myBoss (theBoss) {}
const Handle(OCC_UT_Employee)& Boss() const { return myBoss; }
void AddDirectReport (const Handle(OCC_UT_Employee)& theReport);

private:
Handle(OCC_UT_Employee) myBoss;
TColStd_ListOfTransient myDirectReports;
public:
DEFINE_STANDARD_RTTI(OCC_UT_Employee)
};

As Foundation Classes User’s Guide suggests you either have to use pointers in some case (e.g. OCC_UT_Employee* myBoss) or nullify some references first (myBoss.Nullify()).


*Handle overhead*
Like for anything in this world, you have to pay for convenience of using a handle. Any assignment or deallocation leads to execution of several instructions (comparisons, function calls, branching, etc). Keep that mind. If you are using handle in performance-critical places, think how you can optimize. Some hints on top of my head:

1. Make DownCast() to some expected type first and then use inside a cycle.
Compare two implementations:

static void CheckEmployee (const Handle(Standard_Transient)& theEmployee)
{
for (int i = 0 ; i < aNbCycles; i++) {
Handle(OCC_UT_Employee) anEmp = Handle(OCC_UT_Employee)::DownCast (theEmployee);
}
}

static void CheckEmployee (const Handle(Standard_Transient)& theEmployee)
{
Handle(OCC_UT_Employee) anEmployee = Handle(OCC_UT_Employee)::DownCast (theEmployee);
for (int i = 0 ; i < aNbCycles; i++) {
Handle(OCC_UT_Employee) anEmp = anEmployee;
}
}

With aNbCycles equal 10 millions, on my laptop, the former takes 1.6 secs, and the latter – 0.42 secs, or 3.8X speed up.

2. Avoid copies
Make your class methods return const Handle()& whenever possible and assign to the same type in the caller, rather than using plain Handle(). Consider the following two cases:

a. Imagine you initially defined the Boss() method in OCC_UT_Employee class as follows:
Handle(OCC_UT_Employee) Boss() const { return myBoss; }

and use it as follows:
static void CheckBoss (const Handle(Standard_Transient)& theEmployee)
{
Handle(OCC_UT_Employee) anEmployee = Handle(OCC_UT_Employee)::DownCast (theEmployee);
for (int i = 0 ; i < aNbCycles; i++) {
Handle(OCC_UT_Employee) aBoss = anEmployee->Boss();
}
}

b. Now you switch to const Handle()&:

const Handle(OCC_UT_Employee)& Boss() const { return myBoss; }
...
static void CheckBoss (const Handle(Standard_Transient)& theEmployee)
{
Handle(OCC_UT_Employee) anEmployee = Handle(OCC_UT_Employee)::DownCast (theEmployee);
for (int i = 0 ; i < aNbCycles; i++) {
const Handle(OCC_UT_Employee)& aBoss = anEmployee->Boss();
}
}

Speed up is from 0.26s to 0.03s, or 8.5x ! However, admittedly, Open CASCADE code itself is contaminated with these issues. OCC folks: if you are reading this, please take a note ;-).

(to be continued)

You May Also Like

2 comments