Code Red > .NET

ObjectId.GetObject()

(1/2) > >>

TT:
Just recently, I happened to be peeking at the source code for ObjectId, and do not remember having ever seen this before, but as soon as I did, I realized that this method is terribly inefficient and should not be called in a loop that runs a huge number of times.

Here is the source code for ObjectId.GetObject() as rendered by ILSpy:


--- Code: ---
public DBObject GetObject(OpenMode mode, bool openErased, bool forceOpenOnLockedLayer)
{
  Database database;
  if (ref this != null)
  {
    IntPtr unmanagedPointer = new IntPtr(<Module>.AcDbObjectId.database(ref this));
    database = Database.Create(unmanagedPointer, false);
  }
  else
  {
    database = null;
  }
  return database.TransactionManager.TopTransaction.GetObject(
            this, mode, openErased, forceOpenOnLockedLayer);
}


--- End code ---

If you look carefully, you'll notice that every single call to this method results in the creation of a managed wrapper for the Database that contains the ObjectId, and then a call to it's TransactionManager.TopTransaction properties, to get the top transaction, whose GetObject() method is then called.

So, I will be revising my code to remove calls to ObjectId.GetObject() in cases where it is being used in a loop that can execute a potentially huge number of times, and replacing it with a call to the GetObject() method of the TransactionManager for the database I'm operating on.

If performance is critical, I would suggest you consider doing the same.

Jeff H:
Thanks for the heads up!
 
No error checking or checking if database is null, but
 
Would you do something along the lines of
 

--- Code: ---
         public static DBObject OpenObject(this ObjectId id, OpenMode mode = OpenMode.ForRead, bool openErased = false, bool openObjectOnLockedLayer = false)
        {
            return id.Database.TransactionManager.TopTransaction.GetObject(id, mode, openErased, openObjectOnLockedLayer);
        }

--- End code ---

TT:

--- Quote from: Jeff H on July 06, 2012, 12:30:34 pm ---Thanks for the heads up!
 
No error checking or checking if database is null, but
 
Would you do something along the lines of
 

--- Code: ---
         public static DBObject OpenObject(this ObjectId id, OpenMode mode = OpenMode.ForRead, bool openErased = false, bool openObjectOnLockedLayer = false)
        {
            return id.Database.TransactionManager.TopTransaction.GetObject(id, mode, openErased, openObjectOnLockedLayer);
        }

--- End code ---

--- End quote ---



No, I wouldn't do that.  There's no problem with ObjectId.GetObject() for casual use. It's when it's being called a huge number of times in a loop that it can have a significant effect on performance.

The only cases where I'm replacing ObjectId.GetObject() are in loops where it's called in the loop body, and the loop can execute a huge number of times. I'll revise the code to get the TransactionManager from the database before entering the loop and assign it to a variable that I'll use inside the loop to open objects.

Something like this (abridged):

Before:


--- Code: ---public static IEnumerable<T> GetObjects<T>( this BlockTableRecord btr ) where T: Entity
{
  RXClass rxclass = RXClass.GetClass( typeof( T ) );
  foreach( ObjectId id in btr )
  {
    if( id.ObjectClass.IsDerivedFrom( rxclass ) )
      yield return (T) id.GetObject( OpenMode.ForRead, false, false );
  }
}

--- End code ---

After:


--- Code: ---public static IEnumerable<T> GetObjects<T>( this BlockTableRecord btr ) where T: Entity
{
  TransactionManager tm = btr.Database.TransactionManager;
  RXClass rxclass = RXClass.GetClass( typeof( T ) );
  foreach( ObjectId id in btr )
  {
    if( id.ObjectClass.IsDerivedFrom( rxclass ) )
      yield return (T) tm.GetObject( id, OpenMode.ForRead, false, false );
  }
}

--- End code ---

That simple revision effectively eliminates all of the code in the body of ObjectId.GetObject().

And for those who seem to prefer to marginalize optimizations like this one,
this might help them understand why high-frequency use of ObjectId.GetObject().
is a waste of clock cycles.

This is the functional-equivalent of what is happening when ObjectId.GetObject()
is used in the above GetObjects<T>() "before" example, with the code from the
body of ObjectId.GetObject() inlined:


--- Code: ---public static IEnumerable<T> GetObjects<T>( this BlockTableRecord btr ) where T: Entity
{
  RXClass rxclass = RXClass.GetClass( typeof( T ) );
  foreach( ObjectId id in btr )
  {
    if( id.ObjectClass.IsDerivedFrom( rxclass ) )
      yield return (T) id.Database.TransactionManager.TopTransaction.GetObject(
          OpenMode.ForRead, false, false );
  }
}

--- End code ---

Fixing that requires very little effort (wasted or not), but recognizing it requires
a little skill and experience.

Jeff H:
Oops!
 
If I am understanding correctly the example I posted does absolutely nothing to help, or if anything would make it worse.
Using ILspy for ObjectId.Database property

--- Code - C#: ---// Autodesk.AutoCAD.DatabaseServices.ObjectIdpublic unsafe Database Database{ get {  if (ref this != null)  {   IntPtr unmanagedPointer = new IntPtr(<Module>.AcDbObjectId.database(&this));   return Database.Create(unmanagedPointer, false);  }  return null; }} 
Then on top of that for the Database.TransactionManager
 

--- Code - C#: ---  // Autodesk.AutoCAD.DatabaseServices.Databasepublic TransactionManager TransactionManager{ get {  IntPtr unmanagedPointer = new IntPtr(<Module>.AcDbDatabase.transactionManager(this.GetImpObj()));  return (TransactionManager)RXObject.Create(unmanagedPointer, false); }} 
Then TopTransaction

--- Code - C#: ---// Autodesk.AutoCAD.DatabaseServices.TransactionManagerpublic unsafe virtual Transaction TopTransaction{ get {  AcDbTransactionManager* expr_06 = this.GetImpObj();  AcTransaction* ptr = calli(AcTransaction* modopt(System.Runtime.CompilerServices.CallConvCdecl)(System.IntPtr), expr_06, *(*(long*)expr_06   88L));  if (ptr != null)  {   IntPtr unmanagedPointer = new IntPtr((void*)ptr);   return new Transaction(unmanagedPointer, false);  }  return null; }} 
Then calling GetObject from the Transaction object is the same the thing as TransactionManager.GetObject with the exception of calling CheckTopTransaction first

--- Code - C#: ---// Autodesk.AutoCAD.DatabaseServices.Transactionpublic virtual DBObject GetObject(ObjectId id, OpenMode mode, [MarshalAs(UnmanagedType.U1)] bool openErased, [MarshalAs(UnmanagedType.U1)] bool forceOpenOnLockedLayer){ this.CheckTopTransaction(); return TransactionManager.GetObjectInternal(<Module>.AcDbImpTransaction.transactionManager(this.GetImpObj()), id, mode, openErased, forceOpenOnLockedLayer);}  

--- Code - C#: ---// Autodesk.AutoCAD.DatabaseServices.TransactionManagerpublic virtual DBObject GetObject(ObjectId id, OpenMode mode, [MarshalAs(UnmanagedType.U1)] bool openErased, [MarshalAs(UnmanagedType.U1)] bool forceOpenOnLockedLayer){ return TransactionManager.GetObjectInternal(this.GetImpObj(), id, mode, openErased, forceOpenOnLockedLayer);}  
So if I am understanding this correctly,
Iterating over 5,000 objects the example I posted vs yours, my example would create 14,997 extra unneeded objects(Database, TransactionManager, & TopTransaction---I will give myself one of each) and 5,000 calls CheckTopTransaction.
 
 
Thanks again
 
 
 

TT:

--- Quote from: Jeff H on July 07, 2012, 12:24:03 pm ---Oops!
 
If I am understanding correctly the example I posted does absolutely nothing to help, or if anything would make it worse.
Using ILspy for ObjectId.Database property

--- Code - C#: ---// Autodesk.AutoCAD.DatabaseServices.ObjectIdpublic unsafe Database Database{ get {  if (ref this != null)  {   IntPtr unmanagedPointer = new IntPtr(<Module>.AcDbObjectId.database(&this));   return Database.Create(unmanagedPointer, false);  }  return null; }} 
Then on top of that for the Database.TransactionManager
 

--- Code - C#: ---  // Autodesk.AutoCAD.DatabaseServices.Databasepublic TransactionManager TransactionManager{ get {  IntPtr unmanagedPointer = new IntPtr(<Module>.AcDbDatabase.transactionManager(this.GetImpObj()));  return (TransactionManager)RXObject.Create(unmanagedPointer, false); }} 
Then TopTransaction

--- Code - C#: ---// Autodesk.AutoCAD.DatabaseServices.TransactionManagerpublic unsafe virtual Transaction TopTransaction{ get {  AcDbTransactionManager* expr_06 = this.GetImpObj();  AcTransaction* ptr = calli(AcTransaction* modopt(System.Runtime.CompilerServices.CallConvCdecl)(System.IntPtr), expr_06, *(*(long*)expr_06   88L));  if (ptr != null)  {   IntPtr unmanagedPointer = new IntPtr((void*)ptr);   return new Transaction(unmanagedPointer, false);  }  return null; }} 
Then calling GetObject from the Transaction object is the same the thing as TransactionManager.GetObject with the exception of calling CheckTopTransaction first

--- Code - C#: ---// Autodesk.AutoCAD.DatabaseServices.Transactionpublic virtual DBObject GetObject(ObjectId id, OpenMode mode, [MarshalAs(UnmanagedType.U1)] bool openErased, [MarshalAs(UnmanagedType.U1)] bool forceOpenOnLockedLayer){ this.CheckTopTransaction(); return TransactionManager.GetObjectInternal(<Module>.AcDbImpTransaction.transactionManager(this.GetImpObj()), id, mode, openErased, forceOpenOnLockedLayer);}  

--- Code - C#: ---// Autodesk.AutoCAD.DatabaseServices.TransactionManagerpublic virtual DBObject GetObject(ObjectId id, OpenMode mode, [MarshalAs(UnmanagedType.U1)] bool openErased, [MarshalAs(UnmanagedType.U1)] bool forceOpenOnLockedLayer){ return TransactionManager.GetObjectInternal(this.GetImpObj(), id, mode, openErased, forceOpenOnLockedLayer);}  
So if I am understanding this correctly,
Iterating over 5,000 objects the example I posted vs yours, my example would create 14,997 extra unneeded objects(Database, TransactionManager, & TopTransaction---I will give myself one of each) and 5,000 calls CheckTopTransaction.
 
 
Thanks again

--- End quote ---

The last snippet from my post shows what is actually happening when ObjectId.GetObject() is called in the foreach() block (by simply replacing the call to that method, with the code from its body) 

The overhead of creating all the additional managed wrappers can be significant, but because AutoCAD is single-threaded and the garbage collector runs in a high-priority background thread, on a multi-core system, the GC will run on another under-utilized CPU core, so the overhead of garbage-collecting the managed wrappers and managing and calling their finalizers is actually not much of an issue.

If you want to see how much additional overhead is involved, edit acad.exe.config to disable asynchronous garbage collection, and you should see a significant and perceptible degradation of performance.

Navigation

[0] Message Index

[#] Next page

Go to full version