2008-11-21

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

(continued)

3. Bypass DownCast() in critical places.
Compare:
a.
Handle(Standard_Transient) aTransient = new OCC_UT_Id(1);

for (i = 0 ; i < aNbCycles; i++) {
anId = Handle(OCC_UT_Id)::DownCast (aTransient)->Id();
}
b.
for (i = 0 ; i < aNbCycles; i++) {
Handle(OCC_UT_Id)& anIdHTmp = *((Handle(OCC_UT_Id)*) &aTransient);
anId = anIdHTmp->Id();
}
c. for (i = 0 ; i < aNbCycles; i++) {
OCC_UT_Id* anIdPTmp = (OCC_UT_Id*)aTransient.Access();
anId = anIdPTmp->Id();
}

a. takes 1.75s, b. and c. – ~0.04s, or are 40+ times faster (in some runs 100+) !

You will find use of b. in BRep_Tool methods, by the way.

However beware of cases with potential problems. Never use direct cast unless you can reliably check, if you can safely use it. For instance, the following code throws an exception instead of returning a null handle:
TopoDS_Face aFace;
TopLoc_Location aLoc;
Handle(Geom_Surface) aSurf = BRep_Tool::Surface (aFace, aLoc);

These were my insights. Anybody to share others ? I’d love to hear.

As Handles() are most widely used classes, their implementation must be flawless. In this regard, I wonder if use of UndefinedHandleAddress (equal to 0xfefd0000 or 0xfefdfefdfefd0000 on 64-bit platforms – see Hande_Standard_Transient.hxx) to denote a null handle makes any sense. Perhaps, plain zero would be enough, and this would save on comparison operators. Is someone willing to experiment ? OCC folks ?

*Multi-threading considerations*
In the conclusion, let me add that Handle() is not (yet?) thread-safe. You need to protect a handle instance with critical section (e.g. with Standard_Mutex) to use concurrently. Otherwise you may have a data race (concurrent access to unprotected data) and may end up with broken reference counter and consequently memory leaks, access violations, or other headaches.

Well, that was it, folks. So, how useful was it ? Please post your comments and tell me what you think. This is important for me. Thanks.
Roman

15 comments:

  1. Hello Roman,

    You have started great thing! IMHO OCC badly lacks this kind of activity - someone to share his knowledge in the form of articles.

    Now copule of comments on problems you highlighted:

    1. There is some utility of having UndefinedHandleAddress not equal to zero. I have seen a few cases when one static variable is constructed by reference to another static variable which might be not-yet-initialized at the moment. Now in such case you get immediately Access Violation signal at null address; if UndefinedHandleAddress were null this problem might remain unnoticed.

    One idea (thanks to AGV) is to set this value to 0x01 -- this address should be bad for all platforms. Though not completely sure...

    2. IMHO, your statement that Handles are not yet thread-safe might be confusing. Actually, they are safe for handling one object accessed from multiple threads, provided that each thread uses either its own Handle or common constant one; but for sure you need to ensure that you do not use and modify single Handle object concurrently. This is just the same as with any other simple type.

    ReplyDelete
  2. Nice, I am waiting for the next entry :)

    ReplyDelete
  3. Hi Andrey,

    Thanks for support and great comments.

    1. Interesting, I didn't know that. But not sure if I fully understand the use case. Can you post an example ?

    2. Yes, sorry for confusion. By thread-safety I meant that the same object can be safely used (read-write) in different threads. This is not the case for Handle. Though it is *re-entrant*, i.e. different objects can safely live in different threads. I adopted Qt terminology which says:

    * A reentrant function can be called simultaneously by multiple threads provided that each invocation of the function references unique data.
    * A thread-safe function can be called simultaneously by multiple threads when each invocation references shared data. All access to the shared data is serialized.

    By extension, a class is said to be reentrant if each and every one of its functions can be called simultaneously by multiple threads on different instances of the class. Similarly, the class is said to be thread-safe if the functions can be called by different threads on the same instance.


    Yes, and, of course, like you say, simultaneous reading from multiple threads is fine.

    ReplyDelete
  4. Very nice article. In fact, I was always tempted to do a direct casting in some cases, but I always had my doubts about it, even when I was sure the object was of that specific type. I'll keep that trick in mind.

    I hope you have the inspiration to continue writing more articles like these, each one slowly delving deeper into OCC.

    ReplyDelete
  5. Hello all,
    About thread safety when using an Handle, the Handle itself is not thread safe, because the reference count is not protected. If you do something like that inside two different thread that use the same original Handle, you might have problem:

    Handle(OCC_UT_Id) receivedHandle;
    Handle(OCC_UT_Id) copyOfTheReceivedHandle=receivedHandle; // here you might have a problem if two thread do this operation at the same time

    So, just a simple assignment of this kind will cause a problem, because the ref count might or might not be incremented correctly due to concurrent access to it. And this kind of assignment could append all the time, when you pass a Handle by value, return a Handle by value, do a DownCast, ...


    We have had problem with that and we solved the problem by building our own smart pointer based on Qt QSharedDataPointer. The smart pointer used thread safe ref count increment/decrement using atomic operation (which are much faster than using a mutex to protect the ref count).

    So this solve the assignment of smart pointer to other smart pointer. If you want to access/modify the data that the pointer point to, you still need a way to serialize the access (mutex,...)

    Francois.

    ReplyDelete
  6. Thanks, Francois.

    Yes, I think Qt experience would be helpful for OCC as well. I once read this article - http://doc.trolltech.com/qq/qq14-threading.html - and recently re-read it. Perhaps OCC could benefit both for handles and Standard_Mutex.

    ReplyDelete
  7. Andrey Betenev (abv)November 21, 2008 at 6:25 PM

    Hello Francois,

    In fact, manipulations with reference counter have been protected in OCC 6.3.0 using atomic operations. I take this chance to thank you for pointing out this issue on OCC forum.

    ReplyDelete
  8. Hello Andrey,
    that's good news, so it's thread safe now since release 6.3. (I saw you're the one how worked on the atomic operation.)

    Francois

    ReplyDelete
  9. Folks,

    Yes, a correct statement would be to state that Handle_Standard_Transient (as well as its descendants) itself *is* thread-safe as its entity field never changes (i.e. is only read). However, Standard_Transient (which is pointed to by Handle_Standard_Transient.entity) is not safe. Therefore any attempt to access its field with GetRefCount() to without a protection creates a data race. This occurs, for instance, if you copy a handle to another in one thread while accessing its ref count in another.

    And, of course, MMGT_REENTRANT=1 is a must for multi-threaded apps. Otherwise, even simple copies of handles creates a data race as ++ operator is not atomic.

    ReplyDelete
  10. Hi Roman,

    IMHO the main problem when programming with OCC is not the lack of documentation but the wheel being reinvented everywhere. Okay, there are historical reasons, but why would anyone want to become an expert with Handle today when smart pointers are being put into C++ standard?
    This is even more true for STL or templates, OCC would be much friendlier for developers if code base did use existing state of the art technology.
    The learning curve is way too steep, I never invested in learning CDL because I use OCC code occasionnally, and do not understand why I should learn CDL and WOK instead of using plain C++ within my favourite IDE. I hope that OCC devs will some day reconsider their achievement of rewriting everything from scratch ;-)
    In the meantime, your articles are definitely useful, thanks!

    ReplyDelete
  11. Hi Denis,
    Valid comments. Legacy is way too impacting OCC code. WOK and CDL are first victims I would sacrifice. On the other hand, there can be commercial customers (at least there were) under maintenance contracts, and the company may have some obligations. But I think this can be solved. Well, at least, they could be removed from the distribution. Fortunately, you don't have to study WOK or CDL, anyway.
    As far as smart pointers and collections are concerned, I also agree this should be on todo list.

    ReplyDelete
  12. Hi Roman,

    first of all thanks for the article!

    Maybe it's a stupid question but I don't get the difference between issues (1) and (3). Using the cast operator is quite clear. But I didn't really get what are 'critical situations' and why they influence the performance. Could you please comment on that?

    Thanks
    Pawel

    ReplyDelete
  13. Hi Pawel,

    Indeed, (1) and (3) may look similar but I intentionally separated them. (1) is still a preferred way (as it's safer and let you double-check the type) and is recommended if you are not sure if casting is always unambiguous, or you need to cast into several types, for instance:

    Handle(OCC_UT_Employee) anEmployee = Handle(OCC_UT_Employee)::DownCast (theEmployee);
    if (!anEmployee.IsNull() {
    ...
    } else {
    Handle(OCC_UT_Manager) aManager = Handle(OCC_UT_Manager)::DownCast (theEmployee);
    if (!aManager.IsNull()) {
    //manager-specific processing in a loop
    ...

    (3) bears some risk but is the fast way when performance is critical. Imagine if BRep_Tool::Surface() would be re-written as follows:

    const Handle(Geom_Surface)& BRep_Tool::Surface(const TopoDS_Face& F,
    TopLoc_Location& L)
    {
    Handle(BRep_TFace) TF = Handle(BRep_TFace)::DownCast (F.TShape());
    // Handle(BRep_TFace)& TF = *((Handle(BRep_TFace)*) &F.TShape());
    L = F.Location() * TF->Location();
    return TF->Surface();
    }
    Performance would be awful.

    Hope this clarifies.

    ReplyDelete
  14. Hello Roman,

    First, thanck you very much for the high quality articles you share in this blog.

    Second, I have a question related to handles that come from OCC "get started" page. Here are a few lines extracted from OCC web site:
    """
    To choose the best class for this application, consider the following:

    - gp_Pnt is manipulated by value. Like all objects of its kind, it will have a limited life time.
    - Geom_CartesianPoint is manipulated by handle and may have multiple references and a long life time.
    """

    What is the "life time" concept expressed in this chapter? How can it be measured?

    ReplyDelete
  15. The comment itself is a bit misleading. You need to choose between object manipulated by value and one by handle in the same way as if you chose between an object and a pointer to an object. If you need to share a data between several users you choose in favor of handle type. Another advantage of handle is that you do not need to worry about memory leaks, the memory will be freed up as soon as the last usage of underlying object will be terminated.
    Life time is a period of time between object creation and its destruction. The following three objects will have the same life time, limited by a scope, despite one is manipulated by value and two others are by handle:
    {
    gp_Pnt aP1 (0, 0, 0);
    Handle(Geom_CartesianPoint) aP2 = new Geom_CartesianPoint (aP1);
    Handle(Geom_CartesianPoint) aP3 = new Geom_CartesianPoint (1, 1, 1);
    }

    ReplyDelete