In this post, we explore another important topic: by-value and by-reference classes.
By-value classes
A by-value class follows the dataflow paradigm. In LabVIEW, classes are natively by-value and thus behave like any other basic LabVIEW data type (excluding refnums): numbers, arrays, strings, clusters, etc.
In LabVIEW, data is “carried” by wires . When a wire forks, the data in this wire is duplicated. As a result, the two transported data follow completely independent paths. In other words, operations performed on the data of the first wire have no effect on the data of the second wire and vice versa. This behavior is called “by-value” and follows the dataflow rules of LabVIEW itself.
Since classes in LabVIEW are natively by-value, when we instantiate an object, it follows the rule written above. If the wire carrying this object forks, a memory copy of the object is made and it is as if there were two independent objects. Methods and setters applied on the first object have no effect on the second object and vice versa.
By-Reference classes
By-reference classes behave like the refnum data type. A refnum is a reference to a location in memory, accessible from multiple “points” through some protection mechanisms. Refnum types in LabVIEW include queues, file I/O, control references, the VI Server type, etc.
As a refnum is a pointer to a memory area, when a wire carrying a refnum forks, a copy of the refnum itself is made, but not of the data it points to. The pointer follows the rules of by-value programming, but not the memory area (and hence the data) it points to.
A by-reference class follows this approach. This type of class leverages refnum-like data structures and consequently inherits their behavior (in the next post, we will see how by-reference classes are implemented in detail).
In a by-reference object, its data is stored in a memory area which, potentially, can be shared among parallel code sections. Consider, for example, two while loops that access the same by-reference object. Although there is no data flow dependency between the two loops, changes made to the shared object within one loop impact the other loop.
In object-oriented programming languages like C#, classes are only by-reference. Operations performed by a method or function on an object modify the object itself and always refer to the original object. To duplicate an object, it is necessary to clone a new one using the original one as starting point.
Advantages and disadvantages
Let’s now examine the advantages and disadvantages of these two types of classes to make more informed decisions.
By-value classes have the following advantages:
- Using the concept of data flow is a completely natural process in LabVIEW programming. Programming with a by-value class is no different than using numbers, strings, and any other basic data type in LabVIEW (excluding refnums).
- As the by-value object is not “shared” between different sections of the code, it cannot be changed anywhere else in the code except in the wire that hosts it. This makes debugging quick and easy.
- The use of by-value classes allows multiple threads to run without worrying about one section of code affecting values in another section.
- By-value objects, following the natural approach in the LabVIEW environment, simplify sharing of code among developers, especially if they have different levels of expertise.
By-value classes have the following disadvantages:
- The same object cannot be shared across multiple forks.
Suppose we have a ReportSettings object that contains report creation settings and is used by multiple while loops running in parallel. One possible approach is to pass the ReportSettings object (by-value) to the loops that use it. You then have many independent copies of the object for each loop to which it is passed. However, this may not be a good practice because if you have to make changes on the object (for example, by calling its setters or methods that change its state), by having one independent object for each “wire,” each change impacts only the object’s copy of the related “wire.” In this scenario, if you want to have a single object, the correct approach is to have a single actor (in the simplest case a while loop) in which the ReportSettings object exists and to provide a mechanism for exchanging data or commands between that actor and all other actors that need to perform operations on that object.
- Possible issues may arise in memory management when dealing with large objects. If an object contains a large amount of data, occupying a significant portion of the application memory, it is necessary to ensure that it is not frequently copied. Memory management and the copies that LabVIEW creates are aspects that should always be kept under control. This applies even to large by-value objects.
Let’s assume we have a TestResult object containing the results of a test sequence, potentially occupying tens or hundreds of megabytes of data. In this case, every time the wire hosting TestResult object is forked, it creates a copy which quickly raises the application RAM usage.
By-reference classes have the following advantages:
- Easily share the same object among multiple parallel code sections (e.g., two parallel while loops). With a shared by-reference object, changes made at one point are “visible” in every other point. For example, considering two parallel loops sharing the same by-reference object, if the first loop modifies the object by calling a related setter or method, the updated object is also visible to the second loop.
- Possible code simplification. Suppose again that we have a ReportSettings object that contains the report creation settings and is used in read-only mode by multiple parallel while loops in the application. The by-reference approach allows the various loops involved that read its status to have an up-to-date version of the object at all times without having to implement any special mechanism beyond those for making the object from by-value to by-reference.
- Better memory management when dealing with large objects. By-reference approach avoids unnecessary memory copies of the same object. The copying of data occurs not only when a wire forks but also when the data is passed to a Sub-VI. It’s possible to observe where LabVIEW performs data copies directly from LabVIEW Tools -> Profiles -> Show buffer allocations. When handling large objects, using a by-reference approach may be more appropriate to avoid unnecessary memory copies that increase the application RAM usage. A practical example is the IMAQ library where images (which can occupy a considerable amount of memory) are managed by-reference.
By-reference classes, on the other hand, have the following disadvantages:
- They require the implementation of protection mechanisms. With the by-reference approach, in fact, there is a shared resource, and these mechanisms avoid race conditions or other undesirable conditions.
- Braking of the dataflow paradigm, which is the natural programming method used in the LabVIEW environment.
- More complicated debugging. Debugging with code using by-reference classes can be time-consuming and complicated.
- The implementation of by-reference classes is not natively supported by LabVIEW. Thus, there is an overhead due to implementing the mechanisms to make by-reference a class which is natively by-value.
Conclusions
In this post we’ve explored by-value and by-reference classes, their main differences, advantages and disadvantages. The natural question that arises is: should classes in our applications be by-value or by-reference? As always, the answer is… it depends. It depends on the architecture of the application, how the classes interact with each other, the experience and habits of the programmer, and any memory management optimizations the programmer wants to perform. In my experience, the first choice always leans toward by-value classes. However, the use of by-reference classes is appropriate when this allows for meaningful simplification of the code and when there is a need to handle large objects.
The next post will naturally continue this discussion. We will see different ways of implementing by-reference class. In the meantime, for those interested in further insights from NI regarding by-value and by-reference classes, I suggest to take a look at the following link:
LabVIEW Object-Oriented Programming: The Decisions Behind the Design
Thank you for the time dedicated to reading this article. As always, in case of doubts, requests, or simple curiosity, I am available here.