Performance Comparison: Block instantiation vs DuplicateShallow

Hi there,

I have a class that contains several geometric members. Say one Brep and three meshes.

I need to display a few hundred to thousands of copies of this class and its geometric members to the user. I don’t need the user to be able to edit any of thsi geometry, just visualisation and further internal processing at a later stage.

What is more efficient: Turning the geometric properties of my class into a block definition and instantiating it at different locations or duplicating my class instance by performing DuplicateShallow() and applying a transform on all its geometric members?

Is there even a noticeable difference?

Also am I assuming correctly that once I add my newly duplicated class instance (in case I go with DuplicateShallow) to the RhinoDoc it will actually increase file size? (as opposed to block definitions)

Any insight would be very helpful.

Thanks
Benjamin

I assume that @dale would know this?

Block instantion

instantiation ?

I fixed the typo in your topic title

I would guess the block instantiation would not only be faster but also easier to implement. I also have no clue how much experience you have in developing code, but often the goal is not to write the most efficient code, but rather code that is quick to implement and easy to modify. In truth, whichever method is easier for you to implement while meeting your needs is probably good enough.

Hi Cole thanks for the response,

implementation complexity is no concern. We can do both. Actually we have the DuplicateShallow solution already implemented and it works fine. But we’re considering to make a switch if Block instances give us a performance benefit.

I would assume that the acutal C++ code under the hood doesn’t differ too much between Block instantiation and DuplicateShallow … ? My guess is that DuplicateShallow just exposes some internal function to the RhinoCommon API that Block instantiation also uses internally… but would really like to get some insight from someone who knows…

Cheers
Ben

Ah, gotcha. After some testing, I can get the impetus behind this!

protected override Result RunCommand(RhinoDoc doc, RunMode mode)
{
    var rand = new Random();

    List<Brep> blocks = new List<Brep>();
    for (int i = 0; i < 1000; i++) {
        var xInterval = new Interval(rand.Next(-100, 100), rand.Next(-100, 100));
        xInterval.MakeIncreasing();
        var yInterval = new Interval(rand.Next(-100, 100), rand.Next(-100, 100));
        yInterval.MakeIncreasing();
        var zInterval = new Interval(rand.Next(-100, 100), rand.Next(-100, 100));
        zInterval.MakeIncreasing();

        var box = new Box(Plane.WorldXY, xInterval, yInterval, zInterval).ToBrep();
        if(box == null)
        {
            i--;
            continue;
        }

        blocks.Add(new Box(Plane.WorldXY, xInterval, yInterval, zInterval).ToBrep());
    }

    List<Plane> planes = new List<Plane>();
    for (int i = 0; i < 1000; i++)
    {
        var point = new Point3d(
            rand.Next(-1000, 1000),
            rand.Next(-1000, 1000),
            rand.Next(-1000, 1000));
        var vector = new Vector3d(
            rand.NextDouble() - .5,
            rand.NextDouble() - .5,
            rand.NextDouble() - .5);
        Plane p = new Plane(point, vector);
        planes.Add(p);
    }

    int idef_inx = doc.InstanceDefinitions.Add("test_definition", "A test block definition", Plane.WorldXY.Origin, blocks); // 77 ms
    var instance = doc.InstanceDefinitions[idef_inx]; // 2 ms

    List<GeometryBase> geometries = new List<GeometryBase>();
    
    geometries = MoveCopyElements(blocks, planes); // 24,824 ms
    AddGeometryToDoc(geometries, doc); //~200,000 ms

    MoveCopyDefinition(instance, planes, doc); // 66 ms

    doc.Views.Redraw();

    return Result.Success;
}

private List<GeometryBase> MoveCopyElements(List<Brep> geoms, List<Plane> planes)
{
    List<GeometryBase> m_geom_list = new List<GeometryBase>();
    foreach (var plane in planes)
    {
        foreach (var geom in geoms)
        {
            var moved_brep = geom.DuplicateShallow();
            Transform xform = Transform.PlaneToPlane(Plane.WorldYZ, plane);
            moved_brep.Transform(xform);
            m_geom_list.Add(moved_brep);
        }
    }
    return m_geom_list;
}

private void AddGeometryToDoc(List<GeometryBase> list, RhinoDoc doc)
{
    foreach(var geom in list) doc.Objects.Add(geom);
}

private void MoveCopyDefinition(InstanceDefinition def, List<Plane> planes, RhinoDoc doc)
{
    List<InstanceDefinition> m_def_list = new List<InstanceDefinition>();
    foreach (var plane in planes) {
        var xform = Transform.PlaneToPlane(Plane.WorldXY, plane);
        doc.Objects.AddInstanceObject(def.Index, xform);
    }
}

It seems moving and placing block instances is significantly faster than moving and adding Breps. That said, I’m not sure how you could have multiple instances of a BlockDefinition without adding them to the document - if that’s what you want, then go for instances. If it is just for visualization, I would recommend having a display mesh as a property of your class. From what I understand, they are lightweight to move and efficient to display with the DisplayPipeline, with the idea that you create and modify the mesh only when the base geometry is modified and updated.

From the documentation:

https://developer.rhino3d.com/api/rhinocommon/rhino.geometry.geometrybase/duplicateshallow?version=8.x

Constructs a light copy of this object. By “light”, it is meant that the same underlying data is used until something is done to attempt to change it. For example, you could have a shallow copy of a very heavy mesh object and the same underlying data will be used when doing things like inspecting the number of faces on the mesh. If you modify the location of one of the mesh vertices, the shallow copy will create a full duplicate of the underlying mesh data and the shallow copy will become a deep copy.

This means your file size should be the same or similarly low with duplicateshallow as if you were using blocks.

Thanks a lot Martin,

yes, so from this documentation I assumed that Block instantiation under the hood works and performs very similarly to DuplicateShallow, as this sounds pretty much like what a block instance is meant to do. But that’s of course just an assumption.

As for “baking” (aka adding to the Rhino Doc objects) we did observe very large file sizes with DuplicateShallow. I assume that once you add the object to the Rhino doc it performs an actual deep copy.

I guess we’ll just have to give it a try with block instances…

1 Like

Thanks a lot for this!

Although we have a slightly different use case (instead of 1000 Boxes in one block instance, we have 1000 block instances of a block definition containing just one box)…

But the outcome should be very comparable. Now I am wondering if applying a transform to a shallow duplicate (even if it’s a rigid transform) counts as a “change” to the object (with regards to the documentation Martin quoted) and causes a deep copy to be performed.

In any case, we need this transform so should definitely give blocks a shot.

Thanks everyone!

Any time - glad to help!

This is not true, but the mistake is easy to make. Both sound similar because both are technically shallow copies, but ShallowDuplicate is a shallow copy in code whereas a block instance is a shallow copy in the document. Once the ShallowDuplicate is added to the document, the resultant RhinoObject no longer references the original geometry in code. The wording for ShallowDuplicate is the way it is is because shallow copies are lighter on memory then deep copies, as you just need a pointer instead of a whole copy of an object’s properties.

This is correct. Unfortunately, moving the copy seems to dereference it from the original geometry:

protected override Result RunCommand(RhinoDoc doc, RunMode mode)
{
    var base_box = new Box(Plane.WorldXY, 
        new Interval(0,10),
        new Interval(0,10),
        new Interval(0,10));

    var base_box_brep = base_box.ToBrep();
    var copy_box_brep = base_box_brep.DuplicateShallow();

    // Before moving the original:
    // Both boxes have the same vertices
    WriteVertices(base_box_brep, "Brep Base");
    WriteVertices(copy_box_brep, "Brep Copy");

    base_box_brep.Transform(Transform.Translation(10, 10, 10));

    // After moving the original:
    // Despite moving only the original, the copy is moved as well
    WriteVertices(base_box_brep, "Brep Base 1");
    WriteVertices(copy_box_brep, "Brep Copy 1");

    copy_box_brep.Transform(Transform.Translation(10, 10, 10));

    // After moving the copy:
    // The copy moves without the original moving
    WriteVertices(base_box_brep, "Brep Base 2");
    WriteVertices(copy_box_brep, "Brep Copy 2");

    base_box_brep.Transform(Transform.Translation(10, 10, 10));

    // After moving the original again after modifying the copy:
    // Copy is no longer referencing the original geometry
    WriteVertices(base_box_brep, "Brep Base 3");
    WriteVertices(copy_box_brep, "Brep Copy 3");

    return Result.Success;
}

private void WriteVertices(GeometryBase geom, string msg)
{
    foreach(var pt in geom.GetBoundingBox(true).ToBrep().Vertices)
    {
        RhinoApp.WriteLine($"{msg}: {pt.Location.X}, {pt.Location.Y}, {pt.Location.Z}");
    }
    RhinoApp.WriteLine();
}

My guess is block instances are faster because the memory for the instance contains less information in code, referencing data in the document. The ShallowDuplicate’s data, while mostly references to the base geometry in code, is stored entirely in code, and is therefore more costly in terms of space complexity.