Some learnings on developing a fast Python/C++ script for importing a mesh from an .OBJ file

In the C++ code below, a new_mesh is created using ON_Mesh and then populated with vertices and faces and it all works fine. But if new_mesh turns out not to be valid, then should new_mesh be deleted as shown in the last line or does the usage of ON_Mesh take care of this? That is, when this procedure exits, will ON_Mesh will remove the memory allocated for new_mesh?

ON_Mesh *new_mesh = new ON_Mesh();

// Add vertices and faces in this space.

new_mesh->Compact();
valid = new_mesh->IsValid();
// If mesh is not valid, try removing degenerate faces.
if (!valid) {
	new_mesh->CullDegenerateFaces();
	new_mesh->Compact();
	valid = new_mesh->IsValid();
}
// Add new mesh to Rhino document.
if (valid) { meshObject->SetMesh(new_mesh); }
// Is next line needed? Or does using ON_Mesh eliminate this need?
else delete new_mesh;

I have very few memory leak problems in my C++ code and want to make sure this is not a contributor.

Regards,
Terry.

Yes, the last line is necessary in order to not leak memory

Thanks for the quick response.

And this should be true for all

new ON_XX

elements I use (like SimpleArray, etc). Right?

All C++ classes that are created with the new statement are created on the heap and need a matching delete. The only case where this is not necessary is when you pass the instance off to another class that claims it controls the “lifetime” of the object.

Like when I give the mesh to a CRhinoMeshObject and add it to the document. I discovered I cannot delete the mesh after doing this or it is removed from the document. What you are saying confirms that the class that CRhinoMeshObject belongs to controls the lifetime of the mesh geometry I created in my C++ script. Nice to know.

	CRhinoMeshObject *meshObject = new CRhinoMeshObject(attribs);
	meshObject->SetMesh(mesh);
	// This adds mesh only to heap.
	if (!pDoc->AddObject(meshObject)) {
		// Remove object from Heap if it was not added to doc.
		delete meshObject;
		meshObject = NULL;
		// Remove new_mesh from Heap;
		delete mesh;
		mesh = NULL;
	}
	else { mesh_serial_number = meshObject ? meshObject->RuntimeSerialNumber() : 0; }

So there are no smart pointers in ON_XX classes like these,

unique_ptr<char[]> buf(new char[bufsize]);
unique_ptr<char[]> lmemblock(new char[block_size]);

I do not do a delete for these as the smart pointer takes care of it.

So no smart pointers in ON_XX land, right? Want to make sure I cover all the bases without exception.

Regards,
Terry.

You can use smart pointers if you want. They are part of the std C++ library.

For the samples that you’ve shown they seem like overkill. You would still need to properly release the object from a smart pointer on successfull addition to the document.

The Rhino C++ SDK doesn’t directly expose smart pointers for function arguments. We use them in different places in the core where class instances are shared between several different containers.

Sorry, I was not clear. I just mentioned I use smart pointers to eliminate the need for baby-sitting the delete with some standard C++ objects.

I was just checking if anything similar existed in ON_XX classes. CRhinoMeshObject works like a smart pointer since I can give it a new_mesh I created and it takes care of freeing up the memory for new_mesh (like when the mesh visible in the document is deleted, I would imagine).

For now I will treat CRhinoMeshObject (and other CRhinoXXObject) like smart pointers with respect to the geometry I pass to them that was created with new ON_xx.

Regards,
Terry.

Steve,

I have a few quick follow up questions.

(1) In the code below is the last line to delete new_mesh needed? Or does deleting meshObject already take care of this?
(2) Should I set both the meshObject and new_mesh pointers to NULL? Currently I am only doing this for the meshObject pointer.

CRhinoMeshObject *meshObject = new CRhinoMeshObject(attribs);
meshObject->SetMesh(new_mesh);
// This adds mesh only to heap.
if (!pDoc->AddObject(meshObject)) {
	// Remove meshObject from Heap and NULL its pointer if not added to Doc.
	delete meshObject;
	meshObject = NULL;
	// Remove new_mesh from Heap;
	delete new_mesh;
}

Very much appreciate your help!

Regards,
Terry.
70 and still kickin.

If the function returns false, you need to
delete meshObject;
You should not delete the new_mesh as it’s lifetime has been passed to the meshObject for control.

By the way, there is a much simpler approach to all of this. You can call
CRhinoDoc::AddMeshObject instead. This function takes a const reference to a mesh and does all of the work that you are currently doing. The mesh that you create can be made on the stack instead of on the heap and will be automatically cleaned up as it goes out of scope.

But that approach is 2X slower because the mesh is first copied to the stack and then to the heap. I went that route in the beginning and got advice on the Forum to do it this way. It is definitely faster and I love speed. But speed with reliability. This is why I am filling in all the gaps of my understanding of what is required for this approach.

What about my question about setting the pointers to NULL?

Regards,
Terry.

You need to create a release build and profile. This is not 2x slower.

Setting pointers to NULL is good in practice, but not necessary.

What is a release build and profile? I call the C++ from Python and get the mesh there from its serial number that is passed back from the C++.

Release build and profile, interesting…

Do you mean the Visual Studio Release setting for the Compiler? Yes I see in your current note this is true.

I understand that you do like to micro-optimize which I’m not going to argue against, but I want to make this known for other people reading this thread. The approach you are taking is going to be slightly faster than the AddMeshObject function, but it is overall a safer function to call and is not 2X slower.

The C++ code needs to be compiled as a release build instead of a debug build and more important is that you need to measure times while running Rhino to properly compare for optimizing.

All timing is done while running Rhino with the C++ code compile with the Release setting. I do not know how to execute stand along C++ code created in Visual Studio so I have never done timing in C++ space alone. I do use timings measured in the C++ code like this:

time1 = chrono::steady_clock::now();
C++ code
time2 = chrono::steady_clock::now();

and then get the time for that section of code using:

d1 = (int32_t)chrono::duration_cast<chrono::microseconds> (time2 - time1).count(); // ws ops

and then pass d1 back to IronPython in Rhino to see how long different options require.

I looked at my own code and see that you are right; my old note says it is only 20% slower to use CRhinoDoc::AddMeshObject.

This is not micro-optimizing to me. I grew up on the IBM PC back in 1981 where I used assembly code to micro-optimize the code. I even created self-modifying code that altered the instructions so that short jumps could be used rather than long jumps in order to write pixels to the screen 25% faster.

Plus I need something to chew on while I am isolating indoors in NJ, the state with the highest covid-19 death rate and where the positivity rate just shot up from <1% to over 6%. Playing with the code gives me new puzzles to work on now that I am retired from Intel where I helped run the CPU speed up from 90 MHz to 5 GHz and from 1 CPU/chip to 18/chip and more.

Regards,
Terry.

1 Like

Hi @Terry_Chappell,

I recently wrote a struct that can be used as a timer in C++, with no dependencies except standard the library:

#include <iostream>
#include <chrono>
#include <thread>

struct Timer {
    std::chrono::time_point<std::chrono::steady_clock> start, end;
    std::chrono::duration<float> duration;

    Timer() {
        start = std::chrono::high_resolution_clock::now();
    }

    ~Timer() {
        end = std::chrono::high_resolution_clock::now();
        duration = end - start;
        float ms = duration.count() * 1000.0f;
        std::cout << "Timer: " << ms << "ms" << std::endl;
    }
};

Upon its destruction it outputs the time.

All I’m pointing out is that I recommend that people use the AddMeshObject function instead of the AddObject function.

1 Like

And I guess I am recommending that someone going to the trouble to code in C++ in order to speed up their Python code use the faster but longer as I documented in my code from a year ago:

	// Get doc from RuntimeSerialNumber passed as uint32_t.
	CRhinoDoc *pDoc = CRhinoDoc::FromRuntimeSerialNumber(doc_serial_number);
	// Do something with Attributes. Recommended on the Forum but may not be needed.
	ON_3dmObjectAttributes attribs;
	pDoc->GetDefaultObjectAttributes(attribs);
	// Add mesh to document.
	// Next line is simpler but slower because AddMeshObject adds mesh to Stack and then copies to Heap.
	//CRhinoMeshObject *meshObject = pDoc->AddMeshObject(mesh); // 20% slower due to extra copy
	CRhinoMeshObject *meshObject = new CRhinoMeshObject(attribs);
	meshObject->SetMesh(mesh);
	// This adds mesh only to heap.
	if (!pDoc->AddObject(meshObject)) {
		// Remove object from Heap if it was not added to doc.
		delete meshObject;
		meshObject = NULL;
	}
	else { mesh_serial_number = meshObject ? meshObject->RuntimeSerialNumber() : 0; }

I know about the high_resolution_clock but have not yet tried it to see if it has more resolution than steady_clock. Something to consider.

I am still trying to figure out how to enable Python/C++ for you. I could give you a link to a Visual Studio Project 2017 that is already setup to do this. My current one is too big (10,000 lines of C++) but I know I have make copies of it in the past. So I will look at doing this to create a small version with only a few thousand lines of C++ code in the main program. I also need to make a Python program that calls it so you can see all the nuts and bolts of how it works. This drove me crazy when I first got started. It is absolutely plug & play once you have your Python setup to read the DLL and the C-types variables defined to use in the arguments of the C++ procedures you call from Python. The Python code will show all this for multiple examples. It will also show using the Rhino serial numbers to pass the Rhino Document to C++ and the serial number of objects added to the Document inside the C++ code passed back to Python so you can do more work on them there. Hopefully I can get this done soon.

Of course I should mention I only have this working on a Windows machine. Are you on Windows? If not there is a way to build the DLL equivalent for use in the Apple world. Just not something I have done.

Regards,
Terry.

1 Like

Steve,

I went back and checked the timing on using AddObject which copies to the heap only:

time1 = chrono::steady_clock::now();
ON_3dmObjectAttributes attribs;
pDoc->GetDefaultObjectAttributes(attribs);
CRhinoMeshObject *meshObject = new
CRhinoMeshObject(attribs);
meshObject->SetMesh(mesh);
if (!pDoc->AddObject(meshObject)) { delete meshObject; meshObject = NULL; }
else { mesh_serial_number = meshObject ? meshObject->RuntimeSerialNumber():0; }
time2 = chrono::steady_clock::now();
d1 = (int32_t)chrono::duration_cast<chrono::microseconds> (time2-time1).count();

vs using AddMeshObject which copies to the stack and then to the heap:

time1 = chrono::steady_clock::now();
CRhinoMeshObject *meshObject = pDoc->AddMeshObject(cmesh);
mesh_serial_number = meshObject ? meshObject->RuntimeSerialNumber():0;
time2 = chrono::steady_clock::now();
d1 = (int32_t)chrono::duration_cast<chrono::microseconds> (time2-time1).count();

Here is a graph of the slowdown caused by using AddMeshObject vs AddObject:

image

I created the constant mesh cmesh, used with the AddMeshObject method, before the timed section above using this (which has been edited based upon your post below):

const ON_Mesh &cmesh = *mesh;

Thus it appears that using the simpler, perhaps more reliable, AddMeshObject is well more than 2X slower from these timings. This seems to be different than what you expected and is more than the 20% difference I measured in the past.

The difference now is that I precompute the face normals and vertex normals before adding the mesh to the document. If a mesh without these is added, then Rhino automatically computes them. But this takes 4-5X longer than copying the data. Thus the timing difference between the methods shrinks considerably when normals are not precomputed.

Since my C++ code with parallelism computes the normals 4X to 10X faster than Rhino, it saves time to pre-compute them. In fact face-area weighted vertex normals can be computed with little overhead. With the normals all set to go, then all that is left is for Rhino to copy the data to the document, which it does quickly at 4.4 GB/sec on my 4 GHz machine. If the copy is done twice, then it is going to take twice as long at least.

Thus with pre-computed normals, AddObject will always be more than 2X faster than AddMeshObject.

From this exercise with pre-computed normals, I now know that Rhino does virtually nothing to the mesh data when it is added to the document as there is essentially no time left after the copy operation. This seems strange since Rhino will not add invalid meshes with degenerate faces. But there is not enough time to do the copy and check for degenerate faces. It is a mystery to me where Rhino gets this time. But that’s a topic for another day.

Regards,
Terry.

1 Like