LabVIEW Idea Exchange

cancel
Showing results for 
Search instead for 
Did you mean: 
Petru_Tarabuta

Compiler improvement: Treat subroutine VIs as inline VIs to increase performance

Status: New

Summary: The performance of VIs whose reentrancy setting is Preallocated clone reentrant and whose priority setting is subroutine should be increased to match the performance of inlined VIs (VIs with the Inline subVI into calling VIs setting ticked).

VI Properties - Execution - Math Operations (PCR) (SBRTN).png

Details: I have been benchmarking a few simple mathematical subVIs. My goal was to understand the performance associated with various VI execution settings configurations: Non-reentrant (NR) vs. Shared clone reentrant (SCR) vs. Preallocated clone reentrant (PCR), Allow debugging on vs. off, and subroutine priority vs. inline.

 

The benchmarks showed that, taking mathematical subVIs whose reentrancy is Preallocated clone reentrant:

  1. Setting the priority to subroutine does make the VIs more performant (execute quicker, using fewer CPU instructions per VI call). This is as expected.
  2. Reverting the priority of the same VIs back to normal and instead inlining the VIs also makes the VIs more performant. This is as expected.

What I didn't expect was that the performance increase when the VI is inlined is larger than when the VI uses subroutine priority. In other words, the benchmarks showed that in-lining a VI makes it around 5.0% to 6.3% more performant than when the VI is subroutine. This also means that the best possible performance is achieved by inlining VIs rather than setting them to subroutine.

 

Subroutine vs Inline.png

The chart above shows the relative performance of the same set of three mathematical subVIs when all three VIs are set to subroutine vs. when they are inlined. One Loop means a benchmark caller VI that contained a single for loop that contained a chain of the three subVIs. Two Loops means the for loop was duplicated, such that two identical for loops executed in parallel. Four Loops means that the two for loops were duplicated again, such that there were four for loops executing in parallel. The Y axis represents the number of iterations the loops were able to perform in a given amount of time (2 seconds). When the benchmark caller VI used two or four loops, the Y axis value is the arithmetic average of the iterations of each of the two/four loops.

 

As can be seen in the chart, there is a small but consistent performance improvement when VIs are inlined. This small performance gap remained relatively constant in three circumstances:

  • In Development Environment, with the Compiler setting (Tools > Options > Environment) set to 5 out of 10 (the default value)
  • In Development Environment, with the Compiler setting set to 10 out of 10 (maximum compiler optimisations)
  • In EXE built with Compiler setting set to 10 out of 10

This showed that this performance gap is currently immune to the maximum compiler optimisations or to being built into an EXE. 

 

This idea requests that when a subVI is set to subroutine, the compiler should treat the VI as if it was inlined, and therefore apply any/all possible performance optimisations to calling the subVI.

 

This is in accordance with a comment that Altenbach made to this excellent idea:

Altenbach comment.png

For avoidance of doubt, inlining VIs has a drawback: it makes the LabVIEW editor sluggish. That's why simply "inline all VIs and forget about subroutine" is not a great advice - it would make the editor very sluggish. This is something that can be observed when working with lots of malleable VIs (VIMs), which by their nature must be inlined. Subroutine VIs do not slow down the editor. If this idea was implemented, it would enable achieving the best possible execution performance without slowing down the editor.

 

All testing was done using LabVIEW 2026 Q3 Patch 1 (26.3.1f1) (64-bit) on Windows 11.

9 Comments
Petru_Tarabuta
Active Participant

A resource relevant to this idea: The Subroutine Priority Level section of the Prioritizing Parallel Tasks (LabVIEW User Manual).

 

Subroutine Priority Level (edited).png

Note the parts I underlined using thin, blue lines. They suggest that subroutines should behave very much like inlined VIs. The words "as efficiently as possible" are not quite true at the moment - a more efficient alternative exists, namely inlining a VI.

fefepeto_kb
Member

Maybe I'm misinterpreting something, but I do think that running some VIs in subrutine priority vs calling them inline should be separate options for the following reasons:

  • Subrutine calls still are function calls, where the program counter gets written into the stack, the execution jumps to the beginning of a function, executes the function and then jumps back to the calling code. Subrutine program code stored once in the memory no matter the number of calls made to it.
  • Inlined VIs are practically compiled code copied directly into the caller. No function calls nor stack involvement, but every time the code is called, the program code is stored in the calling VIs.

Lets make an obscenely magnified and unrealistic use-case: We have a.vi which calls b.vi 1000 times. The compiled code of b.vi is ~1kb. While we get slightly worse performance out of b.vi being set as subrutine, we also have the size of the executable ~999kb smaller. This might be a meaningful difference on an older system, or on one that has a slow storage and load time is deemed significant.

If we consider the above differences, then I think both options have their place in the development toolbox of LabVIEW engineers, maybe it should be better emphasized what the difference between the two is, and why that is.

Petru_Tarabuta
Active Participant

Hi fefepeto,

To clarify, I fully agree that there is a place for both settings. I did not ask for the two settings to be merged into one.

 

I was asking for a performance improvement whereby calling subroutines would be as performant as using inline VIs.

 

Your explanation that "subroutine calls are still function calls" makes sense, and might mean that what I asked for was non-sensical and impossible to implement.

 

The benchmarked performance gap - 5% to 6% - seems quite large to be generated just by the fact that execution jumps to the beginning of the function, then jumps back to the calling code. The remaining 95% of the execution time was occupied by three math subVIs that, together, performed quite a lot of math operations. But I admit I don't know enough about low-level program execution to have a good intuition. So perhaps the 5% to 6% performance gap is fully accounted for by the execution jumping back and forth.

fefepeto_kb
Member

The rest might have came from the optimizations that could be done when the code was inlined and folding (both constant folding and conditional structure folding) stale code removal was possible (including the removals because of the input wiring is known, or some constants are wired to the VI inputs that allow elimination of conditional paths).

Also, the function call might be a bit more overhead, since it might also involve copying the program code to to the lowest level of cache if not available, but that shall be managed by another part in the CPU, and in modern PCs it should not have an impact on the execution.

What might be interesting to know is if the 3 VIs have a common dependency that is set to non-reentrant for whatever reason. That might introduce further waiting times, which be better managed when the code is copied together in a single VI (in this case it is likely making a better use of the available threads).

 

What I tried to imply, was, that bringing these two options so close together that there is basically no performance difference might mean that one option makes the use of the other an oddity. Like I would be happy to pay the extra storage price for performance sensitive applications, since the load times with flash storages since the NAND flash revolution are quick enough to make the load time negligible.

Petru_Tarabuta
Active Participant

Sorry for my belated reply.

 

I've thought a bit more and identified the reason that led me to believe the idea made sense before posting it (i.e. was not impossible or non-sensical). I can't speak for anyone else but this might be the same reason why altenbach suggested basically the same idea in his comment (I also want to clarify that I ideated this idea independently and came across altenbach's comment while researching if other similar ideas related to subroutine and inline settings had already been posted).

 

The reason: My understanding was that each instance of a preallocated clone VI was already given its own individual memory space. I took this to mean that each instance's data and code were stored in memory independently of the data and code of any other instance. This meant that, as I understood it, the code of each preallocated clone instance was already replicated multiple times inside the code of the application.

 

Let's say that an application uses three instances of preallocated clone A.vi. As I saw it, this meant that the app's memory would contain three copies of the code of A.vi, located at three distinct memory addresses.

 

I believe that both I and altenbach thought: If three copies of the code are already stored at three different memory locations, and the instances were set to subroutine (which means always use same execution thread as the caller), this is essentially the same as having inlined that VI into each of its three calling locations. Therefore, it should be possible for the compiler to call the three independent locations with the same efficiency as it calls three inlined instances. In other words, the compiler should be able to eliminate the overhead currently associated with calling non-inlined but subroutine-enabled VIs.

 

"What might be interesting to know is if the 3 VIs have a common dependency that is set to non-reentrant for whatever reason." - No, the three VIs have no common dependency. The three VIs are very simple and do not contain any subVIs. They call native mathematical primitives such as Add, Subtract, Multiply, Divide, Exponential, Sine, Reciprocal. Please find below block diagram screenshots of the three subVIs. I refer to these subVIs as "Low Complexity", "Medium Complexity", and "High Complexity".

 

Low Complexity VI:

Low Complexity SubVI.png

 

Medium Complexity VI:

Medium Complexity SubVI.png

 

High Complexity VI:

High Complexity SubVI.png


The high complexity VI is formed of 10 repetitions of the contents of the medium complexity VI.

 

These are dummy VIs I created strictly for the purposes of benchmarking. Nevertheless, I believe they are representative of small, medium, and larger mathematical operations. My goal was to create realistic VIs that will help towards realistic benchmarks.

 

In each benchmark a random number between 0 and 1 is fed to the input of the low complexity VI. The output of the latter is then fed to the input of the medium complexity VI, and the output of this is fed to the input of the high complexity VI. In other words the three subVIs are chained together (each subVI has exactly one input and one output).

 

I'm preparing the benchmarks in support of a presentation I will give during the upcoming GDevCon#7 conference (Brussels, Belgium, 22 to 24 September 2026). I will share the VIs in a public GitHub repository soon after the presentation takes place.

Petru_Tarabuta
Active Participant

Summary: Some more data which indicates that the file size of an EXE is larger when the EXE contains calls to inline VIs vs. when the same EXE contains calls to the same VIs set to subroutine instead of inline.

 

Details: I created a benchmark VI (top-level caller VI) that contains 10 parallel for loops. Each for loops calls a sequence of the three aforementioned subVIs repeated five times. In other words, each for loop calls subVIs like this: A.vi, B.vi, C.vi, A.vi, B.vi, C.vi, ..., A.vi, B.vi, C.vi (for a total of 15 subVI calls - 3 x 5 - inside each for loop).

 

I then set this benchmark VI as the startup VI of an EXE, and built the EXE three times in a row when the subVIs were set to subroutine. Then replaced the subVIs with their non-subroutine, inline equivalents (by swapping to a difference case of a diagram disable structure inside each of the for loops), and rebuilt the EXE three times in a row.

 

The EXE sizes when building the first set of EXEs (subroutine set) were consistently 247,808 bytes (all three EXEs had the same size). This is equivalent to 242 KB.

 

The EXE sizes when building the second set of EXEs (non-subroutine, inline set) were consistently 274,432 bytes (all three EXEs had the same size). This is equivalent to 268 KB.

 

This means that the "inlined" EXE (EXE that contains lots of inlined VIs) was around 26 KB larger, which is around 10.7% larger than the "subroutine" EXE (EXE that contains lots of subroutine VIs). This is a significant difference. It seems to indicate that more code is being stored in the "inlined" EXE.

Intaris
Proven Zealot

Subroutine is still not the same as inlining. Its execution, once started, cannot be interrupted in that thread. It plays havoc with the task scheduler. Something Inlined VIs do not (automatically) do.

 

A subroutine VI is essentially an atomically timed task within a thread. No interruptions allowed. The boundaries of this atomicity are the sub-VI boundary.

Bear in mind that calls to a subroutine VI can also beset to be "skipped if busy".

 

That said, if this can still be coupled to performance wins, I'm all for it.

Petru_Tarabuta
Active Participant

"Subroutine is still not the same as inlining. Its execution, once started, cannot be interrupted in that thread." - Fully agree. This is a genuine, significant difference between subroutine and inline VIs. Maybe this fact makes this idea and altenbach's suggestion impossible.

 

"It plays havoc with the task scheduler." - I disagree. All evidence indicates that using subroutines simplifies the job of the task scheduler. "A subroutine VI is essentially an atomically timed task within a thread. No interruptions allowed. The boundaries of this atomicity are the sub-VI boundary." - Fully agree. This is what makes the task scheduler's job easier. There are fewer points at which it needs to decide which code to run next. The task scheduler can work with bigger chunks of code. This is good, as long as those chunks don't become too large, of course. Maybe a chunk can be considered acceptable (not too large) if it takes ≤ 100 microseconds to execute. For some applications chunks as large as 1 ms or even 10 ms might be fine.

 

"Bear in mind that calls to a subroutine VI can also beset to be "skipped if busy"." - Yes, I know this. But isn't it orthogonal to the discussion? I don't see it being an argument or counter-argument.

 

"That said, if this can still be coupled to performance wins, I'm all for it." - I resonate with this.

Petru_Tarabuta
Active Participant

The aforementioned GDevCon presentation has taken place. The presentation slides, experiment source code, and experiment results are shared at this GitHub repository: https://github.com/RobustoSystems/LabVIEWMultithreading