You are here: Start » AVL.NET » AVL.NET Performance Tips

AVL.NET Performance Tips

Contents

Introduction

Developing applications, that target runtimes with automatic memory management (e.g. .NET Framework), in many scenarios takes off the responsibility of freeing memory thanks to the Garbage Collector concept. Even then, however, it is important to be aware what happens with the allocated data and when it actually is going to be freed. In case of memory allocated on the managed heap (or stack), being aware of how the memory is managed is mainly a matter of application performance. When it comes to dealing with unmanaged resources, memory management awareness is a matter of preventing memory leaks, which apart from performance impact may lead to the application abnormal behavior. Since AvlNet types are in great part an unmanaged resources containers, it is crucial to know their lifetime and how to properly dispose them. This article presents several guides on how to deal with AvlNet types efficiently.

Dispose

Dispose short-living and disposable objects with known lifetime as soon as possible.

All types that implements the IDisposable interface potentially contains an unmanaged data which should be released in the Dispose method. This is especially true in case of great part of AvlNet types that implements the IDisposable interface, e.g. Image, Region, Path, etc. In those cases, the unmanaged resources are simply native C++ AVL objects.

The IDisposable.Dispose() method may be called either directly or indirectly with using statement:

using (var image = new Image())
{
    // Do something with the image.
}

Above is equivalent to the try-finally construction with explicit IDisposable.Dispose() method call:

var image = new Image();
try
{
    // do something with the image
}
finally
{
    image.Dispose();
}

Example 1

In this example a thresholded image is obtained to be used as a background in the Segment2DDesigner editor. It is used only in the editor and is a perfect subject to disposal once the editor is closed.

// ...
using (var processedFrame = new Image())
{
    // Create a new processed image.
    AVL.ThresholdImage(frame, null, 100.0f, 200.0f, 0.0f, processedFrame);

    using (var dialog = new Segment2DDesigner())
    {
        // Use two images as a backgrounds
        dialog.Backgrounds = new[] { frame, processedFrame };

        if (dialog.ShowDialog() == DialogResult.OK)
        {
            // Do something with dialog edited object.
        }
    }

} // Implicitly dispose processedFrame image.

Example 2

Here, among images and regions that are disposed with using statement, there is also a Region list, whose elements also need to be disposed. With SafeList<T>, their disposal may be realized in the using statement too. Following example splits provided region into the blobs and draws them with color that depends from the blob size.

AvlNet.Settings.IsDiagnosticModeEnabled = true;

using (var blobs = new SafeList<Region>())
using (var image = new Image(rgbImage))
{
    AVL.SplitRegionIntoBlobs(region, RegionConnectivity.EightDirections,
        1, null, true, blobs);
						
						if (blobs.Any())
						{
							var areas = new int[blobs.Count];
							for (int i = 0; i < blobs.Count; ++i)
							{
								AVL.RegionArea(blobs[i], out areas[i]);
							}

							float maxArea = areas.Max();

        for (int i = 0; i < blobs.Count; ++i)
        {
            // The bigger the blob is, the more reddish it'll be drawn.
            float red = areas[i] / maxArea * 255f;

            var color = new Pixel(red, 255.0f - red, 0.0f, 1.0f);

            AVL.DrawRegion(image, blobs[i], color, 0.5f);
        }
    }

    // Show results in preview widow.
    AVL.DebugPreviewShowNewImage(image);
    AVL.DebugPreviewWaitForAnyWindowClose();
}

Watch for Objects Created by the AVL.NET methods

Most often objects are created explicitly in the client code with constructors. There are however situations in which memory is allocated within AVL type methods.

Collections

Since there is no out modifier in the collection-type outputs (e.g. SplitRegionIntoBlobs.outBlobs parameter), it is possible to pass a non-empty collection to be (re)populated. If passed collection size is greater than the resulting one, exceeding elements are just removed from the collection without their finalization. To guarantee element finalization during removal from the collection, one can use the SafeList<T> collection, which apart of finalizing elements being removed, finalizes all elements when collection is being finalized itself due to implementation of the System.IDisposable:

using (var blobs = new SafeList<Region>())
{
    AVL.SplitRegionIntoBlobs(region, RegionConnectivity.EightDirections, 1, true, blobs);

    // ... Do something with blobs
}

Nullable objects

Analogous to the disposable collections is case of nullable objects (atl::Conditional<T> and atl::Optional<T> in native AVL), which can be passed non-empty and thus leak the underlying object when reset to an empty object (e.g. CreateEdgeModel1.outEdgeMode). This is what the SafeNullableRef<T> type is designed for, e.g.:

using (var edgeModel = new SafeNullableRef<EdgeModel>())
{
    AVL.CreateEdgeModel1(image, 0, 0.0f, 35.0f, 15.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, edgeModel);

    if (edgeModel.HasValue)
    {
        // ... Do something with the model
    }

}

In the above example, if no edge model could be created, edgeModel's parameterless Reset method is called to make the object empty. SafeNullableRef<T> guarantees that before reseting, previous object (if there is any) is finalized.

Do Not Rely On Finalizers

If disposable and finalizable (all AVL.NET disposable types are also finalizable) objects aren't manually disposed with Dispose method, either implicitly or explicitly, once reclaimed by the GC, such an objects may be finalized by the GC (see The garbage collector and unmanaged resources).

However, GC does not guarantee that the finalizer will ever be called. Even if it did, since GC runs indeterministically, such an objects could residue in the memory for a very long time. This may lead to the situation when application have allocated a large amount of useless data that is just waiting to be released.

Keep Alive

Prevent from reclaiming disposable objects by the GC when native data still is being processed. The simplest misuse is when one obtain pointer to the raw data and then dispose the container, e.g.:

unsafe
{
    PointRun* ptr;
    using (var region = new Region())
    {
        region.Add(0, 0, 10);
        ptr = (PointRun*)region.Data.ToPointer();

        // Print length of the first point run. Expected value is 10
        Console.WriteLine(ptr->Length);
    } // dispose the region and unmanaged resources it contains

    // Once region is disposed, memory that ptr used to point to, no longer contains region data
    // so the following line most probably will print some garbage value
    Console.WriteLine(ptr->Length);
}

While this is relatively easy to fix, there is other scenario which can be quite hard to track down and engages Garbage Collector run, e.g.:

unsafe
{
    PointRun* ptr;

    {
        var region = new Region(100, 100);
        region.Add(0, 0, 10);

        ptr = (PointRun*)region.Data.ToPointer();

        // Print length of the first point run. Expected value is 10
        Console.WriteLine(ptr->Length);
    } // region goes out of scope and can be reclaimed by the GC

    // ...

    // Because GC could already finalize reclaimed objects, following line may display
    // garbage value even if Dispose method was not called
    Console.WriteLine(ptr->Length);
}

In this case it cannot be determined if ptr still points to the region data in the second call, since GC could have already finalized the region.

There is even more tricky, yet not so probable threat, when disposable object have been optimized away and finalized before accessing raw data it used to contain.:

unsafe
{
    var region = new Region(100, 100);
    region.Add(0, 0, 10);

    // last time region is referenced
    PointRun * ptr = (PointRun*)region.Data.ToPointer();

    // Print length of the first point run. Expected value is 10
    Console.WriteLine(ptr->Length);

    // ... some long operations that don't use the region variable.
    //     Since region is not referenced any longer, it can be marked
    //     as unreferenced by the optimizer and can be reclaimed by the GC.

    // GC could have finalized region before accessing ptr in the following line
    Console.WriteLine(ptr->Length);
}

The issue in the last two snippets is actually a result of relying on a finalizer, which should be avoided.

Although examples with native pointers may looks rather artificial (except if someone in deed wanted to process raw data in their own highly optimized algorithms) it is not far from what happens in the AVL.NET functions.

Managed AVL functions are wrappers for native AVL functions with native objects being passed. As it was mentioned before, C++ native objects may be contained by the managed AVL.NET types (that implements IDisposable) as unmanaged resources. Previous raw pointer usage example in some way represents situation that happens under the hood in the AVL.NET functions. This is why it is crucial to ensure that the unmanaged resources has not been released before or during their processing, either by the native AVL function or directly in the user code.

Such scenarios are more probable in multithreaded applications, where one thread is responsible for running long-lasting AVL functions, and the other with an access to the processed objects may occasionally dispose them. Issues common to the multithreaded applications are not however within a scope of this article. Good practices in such an applications equally applies to AVL.NET client applications as well.

Reuse

It is always a good practice to reduce memory allocation. While it is natural to cache objects that are used frequently as an inputs, it is also suggested practice in outputs case. In the following snippet memory for rotatedImage is only allocated twice: at constructor (which just creates an empty image) and at first for-loop iteration inside the RotateImage where the image is reset with the new size (actual block of memory allocation). In all other iterations the same buffer is reused since image dimensions does not change over the loop due to appropriate RotationSizeMode value passed to the function.

using (var windowState = new DebugPreviewWindowState())
using (Image image = new Image())
using (Image rotatedImage = new Image())
{
    // ... get an image to be rotated

    for (int i = 0; i <= 360; ++i)
    {
        AVL.RotateImage
        (
            inImage: image,  
            inAngle: i, 
            inSizeMode: RotationSizeMode.Preserve, 
            inInterpolationMethod: InterpolationMethod.NearestNeighbour,
            inInverse: false,
            outImage: rotatedImage // fill rotatedImage variable with a function result
        );

        //display rotated image in existing window
        AVL.DebugPreviewShowImage(windowState, rotatedImage);
    }

    AVL.DebugPreviewWaitForAnyWindowClose();
}

The same strategy can be applied in more camera-acquisition-like usage. Most often image memory consumption does not change between consecutive acquisitions so working buffer is again a perfect subject to reuse:

using (var image = new Image())
{
    while (isAcquisitionEnabled)
    {
        if (GenICam.GigEVision_TryReceiveImage(deviceHandle, 2000, image))
        {
            // ... process the image
        }
    }
}

Summary

Working with an unmanaged data requires a special attention even in C# language with automatic memory management. IDisposable interface significantly helps to avoid leaking such a data due to it's Dispose method. However, while it should be called eventually by the user, either explicitly or with using statement, it cannot be invoked to early, e.g. when the underlying data is still going to be or is being processed.

Allocating memory, especially big blocks of memory can be time consuming so it is good practice to reuse existing blocks as much as possible. Also deallocating memory in the Garbage Collector run is not free so reducing GC work increases overall application performance.

Further Reading

Previous: HMI Controls for AVL.NET Next: Code Migration Guide to version 4.11