06-12-2026 05:29 PM - edited 06-12-2026 05:38 PM
The Indicator/output tunnels, how are they operating behind the scenes when set to Indexing or even Concatenating? Is the For Loop doing an ongoing Build Array? Or are the tunnels pre-initialized using the array size of whatever input (whether driven by an array tunnel or Loop Count)? I would hope the latter as that's gotta be more efficient I would think. And if user uses condition, it just deletes whatever unused elements from the array at the end?
I made a quick VI to hopefully better explain what I'm asking
06-13-2026 12:55 AM - edited 06-13-2026 01:14 AM
@LuminaryKnight wrote:
I would hope the latter as that's gotta be more efficient I would think.
When you say “more efficient,” that can mean either memory efficiency or performance efficiency. Assuming you mean performance, and the only reliable way to know is to run micro‑benchmarks and see which approach is faster.
The most inefficient approach is to use a continuously growing array using Build Array primitive instead of a preallocated one. If you know the size of your array in advance, always preallocate (even more than needed if necessary).
This is basically the least efficient way to work with a native LabVIEW array:
Why it is inefficient and how it works under the hood is described in this comment:
It is much more efficient to use Replace Array Subset instead of Build Array. In this example, the speedup is about 6×:
Now, if you are talking about conditional indexing, then:
In this particular case, there is almost no difference, so it looks like array is preallocated in advance.
The situation changes slightly if you use a while loop instead of a for loop, but not dramatically:
(it’s fairly obvious that using a while loop will not help the compiler optimize your code).
This behavior and design pattern is similar in almost any programming language. For example, the following Rust code using a preallocated vector versus a dynamically growing vector is more or less equivalent to the LabVIEW examples above:
use std::time::Instant;
fn main() {
let n = 1_000_000;
// --- Preallocated vector ---
// We know exactly how many even numbers will be produced: n/2
let mut v1 = vec![0usize; n / 2];
let start = Instant::now();
let mut idx = 0;
for i in 0..n {
if i % 2 == 0 {
v1[idx] = i; // like "Replace Array Subset"
idx += 1;
}
}
let duration_prealloc = start.elapsed();
// --- Growing vector ---
let mut v2 = Vec::new();
let start = Instant::now();
for i in 0..n {
if i % 2 == 0 {
v2.push(i); // like "Build Array"
}
}
let duration_grow = start.elapsed();
println!("Preallocated time: {:?}", duration_prealloc);
println!("Growing vector time: {:?}", duration_grow);
}
And the results:
>r-arr.exe
Preallocated time: 156.7 µs
Growing vector time: 2.8696 ms
If you run a LabVIEW‑based app or a Rust program under a binary debugger, you’ll see why they behave so differently. Internally, both LabVIEW and Rust use LLVM to generate machine code from a high‑level representation.
This is described in the article NI LabVIEW Compiler: Under the Hood.
In short, LabVIEW analyzes your block diagram, eliminates dead code, folds constants, splits execution into clumps that can run in parallel, builds DFIR (DataFlow Intermediate Representation), and finally lowers that DFIR into LLVM IR. Rust follows a similar multi‑stage compilation pipeline: it first produces HIR (High‑Level IR), then THIR (Typed HIR), then MIR (Mid‑Level IR), and finally LLVM IR — a language‑independent low‑level intermediate representation used by the LLVM backend to generate optimized machine code.
But the difference is that in Rust you can easily emit the LLVM IR listing:
cargo +nightly rustc --release -- --emit=llvm-ir
If you inspect the LLVM IR, you will see the reallocations:
bb3: ; preds = %bb15
%alloc_size.i = shl nuw i64 %self.0.val, 3
%1 = icmp ne ptr %self.8.val, null
tail call void @llvm.assume(i1 %1)
%cond.i.i = icmp uge i64 %_34.0, %alloc_size.i
tail call void @llvm.assume(i1 %cond.i.i)
; call __rustc::__rust_realloc // <- REALLOCATION!
%raw_ptr.i.i = tail call noundef align 8 ptr @___rustc14___rust_realloc(ptr noundef nonnull %self.8.val, i64 noundef %alloc_size.i, i64 noundef 8, i64 noundef range(i64 0) %_34.0) #17
br label %bb3
Unfortunately, LabVIEW does not expose this. LabVIEW behaves like a “black box”, so you must infer behavior through timing, or inspect the generated machine code with a debugger. This was demonstrated here in the comment, by the way:
Re: Got some interesting benchmarks on To Upper vs. "Branchless Programming"
Also keep in mind that LabVIEW allows parallelized for‑loops and DVR (Data Value References)‑based techniques, which in some cases can yield more efficient execution on arrays — but these must be benchmarked individually.
Andrey.
06-13-2026 03:05 AM
Attached is a PDF illustrating my previous comment.
In short, Build Array internally uses the lvrt.FlatArrayResize() function, while auto‑indexing uses lvrt.ReshapeArray(). It looks like these functions apply different internal array‑resizing strategies, which leads to different performance.
06-13-2026 10:30 AM
The nice thing about LabVIEW is that you don/t need to really worry about these things.
If autoindexing non-conditionally of a FOR loop, the output size is fully determined before the loop starts and will be fully allocated right then. (Note that this is not possible when autoindexing at a while loop output tunnel).
Historically much later, the conditional FOR loop terminal and the conditional output tunnel were introduced, making it possible that the final output is shorter. However the compiler still has a clear upper size boundary and I am sure this is fully allocated for the worst case scenario, to be trimmed after the loop ends.
If you do your own conditional array building, things get a bit murkier. Sometimes the compiler can recognize the pattern, simplify, and optimize. For example if you append a single element, the upper size boundary can again be predicted. (If you would append random length arrays this is no longer possible.).
Your examples are a bit unrealistic, because it would be impossible to reasonably operate the boolean control while the loops are spinning and if you would place the terminals before the loops, its value will be constant folded for the duration of the loops. As a rule of thumb, controls and indicators (and their locals!) don't belong inside tight loops.
(Now lets look what happens when autoindexing on a while loop. The way I understand it, there will be a reasonably sized first allocation and every time it runs out of space, an even larger chunk gets pre-allocated. This gives a reasonable balance between memory and speed.)
In any case, the compiler is very smart and can do a lot of things behind the scenes. If you think that you can get a quick insight by just doing some benchmarking, you are opening another can of worms. 😄 (se e.g. here).
06-14-2026 01:30 AM
@altenbach wrote:
In any case, the compiler is very smart and can do a lot of things behind the scenes. If you think that you can get a quick insight by just doing some benchmarking, you are opening another can of worms. 😄 (se e.g. here).
To be completely honest, I find the LabVIEW compiler to be more "straightforward" than "very smart" — and I say that with all my deep respect for this ingenious graphical programming language.
One thing I forgot to mention in the previous comment is the Desktop Execution Trace Toolkit. We can enable memory‑allocation tracing there and observe the resizing:
And we can see how it looks in the case of Build Array:
or Auto Index:
Resizing occurs in both cases, but with auto‑indexing the memory is resized in much larger chunks — 2048 bytes instead of 128 — so it happens far less frequently (i did not counted all calls), which leads to better performance.
The second thing I forgot in the Rust code is the “dummy” calculation on the array. Because of that, the optimizer simply removed it and kept an “empty” loop. I’ve updated the PDF in the attachment with assembly output.
In general, in my humble opinion, it’s better not to worry too much about micro‑optimizing loops when it isn’t truly necessary. A clear and readable block diagram is usually more valuable than an over‑optimized but harder‑to‑understand one. So if we need Build Array, then simply use it — just keep in mind the possibility of reallocations. LabVIEW is not primarily designed for raw performance; it is a convenient graphical environment. When a bottleneck appears, some optimizations can certainly be done directly at LabVIEW level, and optimized as well, but for ultimate performance we often need to wrap critical loops in a programming language with a highly optimized, a “really smart” compiler.
A common problem in LabVIEW when working with native arrays in native loops is the massive number of bounds checks on every access, along with excessive memory accesses and limited use of CPU registers and in additional, LabVIEW makes very poor use of SIMD instructions in such loops.
06-15-2026 03:27 AM
There was some improvement many versions ago, that i think preallocates the array if you have indexing output, and if using Conditional out it can simply cut it afterwards. In a while loop that's harder, so i think it does like Java and allocates e.g. 100 elements and if filled up it doubles it and so on. Asking the OS for more memory is expensive, thus it's better to do it in chunks.
06-15-2026 05:54 AM - edited 06-15-2026 06:20 AM
Just for lolz, the better implementation in LabVIEW for that loop would be:
Admittingly it requires a little thinking to end up with this from the original intend and the solution you showed may seem more direct in that respect but getting rid of a Modulo operator in a loop and reducing that loop iteration by factor 2 is getting significant when you work with millions of elements/iterations. My first slightly more naive optimization was to use a OR 1 instead of the modulo but then I figured why iterate half of the time to do literally nothing but that OR operator.
Yes, Rust does that all automatically so it is really better in more complex optimizations. But I still like LabVIEW more. ❤️
I have also a suspicion that there might be supersecretprivatespecialstuff or something in LabVIEW that can expose the actual LLVM IR in some ways, but really haven't tried to find out as it is not really my pet toy. It quite obviously would be pretty hairy looking as there is indeed quite a bit of overhead in the generated code.
06-15-2026 08:35 AM - edited 06-15-2026 08:39 AM
@rolfk wrote:
Just for lolz, the better implementation in LabVIEW for that loop would be:
.
I think you have the element and index inputs swapped. Of course getting an array of incremental even numbers is a relatively special case and the real world problems of conditional indexing are a bit more complicated.
To generate an array of even numbers, I might do one of the following:
It is possible that the second version could take better advantage of SIMD, but it is also possible that they both are the same in the end. I have not tested.
06-15-2026 02:53 PM
@altenbach wrote:
To generate an array of even numbers, I might do one of the following:
It is possible that the second version could take better advantage of SIMD, but it is also possible that they both are the same in the end. I have not tested.
Yes — usually the second variant is faster because there are "library" functions for these primitives, and they use SIMD with much less overhead. It doesn’t make sense to measure or debug this with synthetic tests; it should be tested on real applications and meaningful algorithms. By the way, the Rust function was also not optimally compiled:
The compiler was smart enough to replace division by modulo with an trivial increment by two, but it left two comparisons when one is unnecessary — the code can be simplified from 7 to 5 instructions. Nobody is perfect (yet).
06-16-2026 03:33 AM - edited 06-16-2026 03:37 AM
@altenbach wrote:
@rolfk wrote:
Just for lolz, the better implementation in LabVIEW for that loop would be:
.
I think you have the element and index inputs swapped. Of course getting an array
You are of course right. For some reason my mind figured that the creation of just even numbers would be to easy to even worry about creating such a convoluted routine. It definitely would not have been my first intuitive choice but I would rather have done your second solution right away, although for a factor 2 I usually end up using a two input Add. I'm not sure which is faster, bit-shift used to have some overhead on older CPUs but that is definitely in the realm of premature optimization.