LabVIEW

cancel
Showing results for 
Search instead for 
Did you mean: 

Maps are too slow (?)

Solved!
Go to solution

@Thomas_robertson wrote:
  • The safe way to pass a map around the software is to use a DVR.

Out of curiosity, do Sets behave the same way? I am not displaying any sets on the Front Panel, but they are in a shift register for subVIs to access. I changed my Map Access to a DVR of a Map instead of passing the Map around the shift register, not just wondering if I need to do the same for Sets?

0 Kudos
Message 21 of 28
(621 Views)

Sets and Maps are complex datatypes, but are treated like any other LabVIEW data: By Value. They are very efficient and there is no problem with them, EXCEPT when you create copies. And fundamentally, everytime you branch a wire, there is potentially created a copy. The LabVIEW compiler tries to minimize copies when it can, but if you wire a data type out of a subVI and keep a copy in the subVI (in a shift register or feedback node, the magic ends exactly here. There is simply no way to try to optimize that further in a meaningful way without using full level application optimization that would require more resources than your normal PC ever will have.

 

Basically, any wire branch of complex data is a potential performance hazard. LabVIEW may be able to optimize it if it is inside a single diagram level, but simply has to stop at some point to try to be smarter, to avoid crippling compile time performance for a bit more runtime performance.,

Rolf Kalbermatter
My Blog
0 Kudos
Message 22 of 28
(586 Views)

@rolfk wrote:

Sets and Maps are complex datatypes, but are treated like any other LabVIEW data: By Value. They are very efficient...


Yes, I agree. By the way, I'm positively impressed with the performance of the maps in LV. Just for fun, I tested maps from Rust (both HashMap and BTree), and yes, they are faster than LabVIEW, but not dramatically:

image-20250302211445178.png

Code used for benchmark:

Spoiler
snippet.png

use std::collections::{HashMap, BTreeMap};
use std::slice;

//LabVIEW String Handle:
#[repr(C)]
pub struct LStr {
    cnt: i32,
    str: [u8; 1],
}

type LStrHandle = *mut *mut LStr;

// ============================================================================
// Hash Map
//
#[unsafe(no_mangle)]
pub unsafe extern "C" fn hash_map_init(hash_map_out: *mut *mut HashMap<Vec<u8>, i32>) -> i32 {
    if hash_map_out.is_null() { return -1; } // Error: null output pointer
    let hash_map = HashMap::new(); 
    unsafe { *hash_map_out = Box::into_raw(Box::new(hash_map)); }
   0
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn hash_map_insert(hash_map_in: *mut HashMap<Vec<u8>, i32>, key: LStrHandle, value: i32) -> i32 {
    if hash_map_in.is_null() || key.is_null() { return -1; } // Error: null input pointer

    unsafe {
        let key_ptr = *key; // Dereference the double pointer to get to LStr
        if key_ptr.is_null() { return -1; } // Error: null pointer
        let key_ref = &*key_ptr;
        if key_ref.cnt <= 0 { return -2; } // Error: invalid string length

        // Get the string bytes using the count from the struct
        let str_ptr = key_ref.str.as_ptr();
        let bytes = slice::from_raw_parts(str_ptr, key_ref.cnt as usize);
        
        (*hash_map_in).insert(bytes.to_vec(), value); // Create Vec<u8> from bytes
        0
    }
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn hash_map_get(hash_map_in: *mut HashMap<Vec<u8>, i32>, key: LStrHandle, value_out: *mut i32) -> i32 {
    if hash_map_in.is_null() || key.is_null() || value_out.is_null() { return -1; } // Error: null pointers

    unsafe {
        // Dereference the double pointer to get to LStr
        let key_ptr = *key;
        if key_ptr.is_null() { return -1; } // Error: null pointer
        let key_ref = &*key_ptr;
        
        if key_ref.cnt <= 0 { return -2; } // Error: invalid string length

        // Get the string bytes using the count from the struct
        let str_ptr = key_ref.str.as_ptr();
        let bytes = slice::from_raw_parts(str_ptr, key_ref.cnt as usize);
        
        if let Some(v) = (*hash_map_in).get(bytes) {
            *value_out = *v;
            0
        } else {
            -4 // Error: key not found
        }
    }
}

// ============================================================================
// BTree Map
//
#[unsafe(no_mangle)]
pub unsafe extern "C" fn btree_map_init(btree_map_out: *mut *mut BTreeMap<Vec<u8>, i32>) -> i32 {
    if btree_map_out.is_null() { return -1; } // Error: null output pointer
    let btree_map = BTreeMap::new();
    unsafe { *btree_map_out = Box::into_raw(Box::new(btree_map)); }
    0
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn btree_map_insert(btree_map_in: *mut BTreeMap<Vec<u8>, i32>, key: LStrHandle, value: i32) -> i32 {
    if btree_map_in.is_null() || key.is_null() { return -1; } // Error: null input pointer

    unsafe {
        let key_ptr = *key;
        if key_ptr.is_null() { return -1; }
        let key_ref = &*key_ptr;
        
        if key_ref.cnt <= 0 { return -2; }

        let str_ptr = key_ref.str.as_ptr();
        let bytes = slice::from_raw_parts(str_ptr, key_ref.cnt as usize);
        
        (*btree_map_in).insert(bytes.to_vec(), value);
        0
    }
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn btree_map_get(btree_map_in: *mut BTreeMap<Vec<u8>, i32>, key: LStrHandle, value_out: *mut i32) -> i32 {
    if btree_map_in.is_null() || key.is_null() || value_out.is_null() { return -1; }

    unsafe {

        let key_ptr = *key;
        if key_ptr.is_null() { return -1; }
        let key_ref = &*key_ptr;
        
        if key_ref.cnt <= 0 { return -2; }

        let str_ptr = key_ref.str.as_ptr();
        let bytes = slice::from_raw_parts(str_ptr, key_ref.cnt as usize);
        
        if let Some(v) = (*btree_map_in).get(bytes) {
            *value_out = *v;
            0
        } else {
            -4 // Error: key not found
        }
    }
}​

The code in the attachment is not intended for production; it's just a funny Sunday evening experiment, nothing more. It is 64-bit only, compressed twice, otherwise, the NI Virus Scan runs forever.

Message 23 of 28
(565 Views)

Sorry, I forgot about the overhead caused by functional globals. Additionally, Maps were introduced in LabVIEW 2019. Below is the correct comparison:

snippet.png

And the results — the performance of LabVIEW Maps (for the given data) is somewhere between HashMap and BTree:

Screenshot 2025-03-03 05.56.10.png

Code for LV2019+Rust 1.85.0.

To compile Rust code, you need to install the latest version of Rust (or update your existing version), then build it as shown below:

 

rustup update
cargo build -r

 

 

rust.gif

 

Message 24 of 28
(543 Views)

Recently our team began using Maps in a portion of our code to facilitate faster lookup of items.   After implementing it in a small portion of our code we noticed an alarming 50% increase in the time to do some of our larger operations.   After tracing it down to the Map code we read a bit on the forums and did some experimenting and learned some lessons which we thought we’d share:

  • Passing maps around is dangerous because any copies appear to be very expensive.  I believe this is because LabVIEW has to do a complex copy of a linked list type data structure rather than a simple contiguous memory block copy.
  • The safe way to pass a map around the software appears to be to use a DVR.   All access to the maps should only happen within the in place structure, passing it out of the structure risks making copies.  For encapsulation and ease of maintenance we believe the best choice is to create a class to wrap your map and hold the DVR privately with member methods for inserting and searching with the map.  Of course doing it this way you need to be mindful of the fact that you are referring to one shared copy and have bypassed dataflow paradigm.  
  • For reasons that we don’t quite understand, it appears that having the map drawn to a control/indicator is expensive.  This performance hit occurs even in EXE’s when passing the map via control/indicator through subvis despite the controls presumably being stripped out at compile time.  I’m having a hard time believing our data on this topic because it defies my understanding of how LabVIEW compiles code to EXE, but we believe that we are measuring it.    Again using a DVR passed around directly or within a wrapper class can eliminate this concern but there may be a performance hit if you wire the map to an indicator for debug purposes. 

 

We are grateful to this thread which helped guide us to a better solution, but I felt it was worth summarizing our findings in a fresh thread.  https://forums.ni.com/t5/LabVIEW/Maps-are-too-slow/td-p/4061047.

 

I'm curious if others have made similar findings and if they have anything to add regarding best practices to mitigate these performance concerns?

0 Kudos
Message 25 of 28
(478 Views)

Did you ever test to see if putting a Map as private data of a class had the same performance hits?

 

That's how I use a Map in the one application I have used it in so far.  I feel confident that the "drawing the control/indicator" bit isn't a problem when using it that way, as the class icon doesn't show it and all the accessors are in-lined.

 

The Maps in the class I have aren't large enough to notice a hit on copying them, if that's still the case though.  In theory that would only happen if the Map was taken out of the accessor  and the wire forked causing a copy, but as you mention it seems that theory may not apply to maps as it ought to.

0 Kudos
Message 26 of 28
(452 Views)

@Kyle97330 wrote:

 

The Maps in the class I have aren't large enough to notice a hit on copying them, if that's still the case though.  In theory that would only happen if the Map was taken out of the accessor  and the wire forked causing a copy, but as you mention it seems that theory may not apply to maps as it ought to.


Actually, anytime you branch the class wire you do usually involve a copy of all the private data in the class, so there is more potential to run into performance issues than just passing out the map out of a property accessor. The only exception to this rule is in a class method that branches the wire to access some of the private data through a Unbundle (by Name) node. LabVIEW can and will optimize such access if it is limited to one diagram level.

Rolf Kalbermatter
My Blog
0 Kudos
Message 27 of 28
(429 Views)

@Kyle97330 wrote:

Did you ever test to see if putting a Map as private data of a class had the same performance hits?


I had a dramatic slowdown that I solved by switching from a Map in private data to a DVR to a map in private data. (https://forums.ni.com/t5/LabVIEW/Maps-are-too-slow/m-p/4315987/highlight/true#M1262873)

 

There was probably a way to get the compiler to stop doing whatever copies it was doing, but switching to a DVR was very quick and easy and accomplished what I needed.

0 Kudos
Message 28 of 28
(407 Views)