Complex enum with non copyable objects

Hello,

I am creating a filehandler for an embedded system. The idea is to store all operations in an Operation enum with different types. The operations contain quite complex objects which are not copyable and can be only moved. operation will be a static variable so the memory is already allocated. When I do now a write operation I have to create a new vector, add the data and then moving it into the operation variable. I tried to disassemble it, but due to the use of heapless it looks quite unreadable.
I have concerns about the performance, vec is not copyable but still the data must pushed into the vector/string.

Is there a more efficient way?

use heapless::{String, Vec};

enum Operation {
    Open((usize, String<50>)),
    Close,
    Write(Vec<u8, 1024>),
    Read,
    FileExists(String<50>),
}

fn main() {
    let mut operation = Operation::Close; // size of Vec<u8,1024>(largest type) + overhead gets allocated

   // Filehandler will do something with the operation

    let mut vec: Vec<u8, 1024> = Vec::new();
    vec.push(5); // Now sizeof(operation) and sizeof(vec) is allocated
    operation = Operation::Write(vec);

    // Filehandler will do something with the operation

    let s = "Teststring";
    let mut string: String<50> = String::new();
    string.push_str(s); // Now sizeof(operation) and sizeof(string) is allocated.
    operation = Operation::FileExists(string);

    // Filehandler will do something with the operation
}

I didn't have time to try this myself, but can you push to the operation after assigning it? Because of using unreachable, the pattern match will be optimized out.

    operation = Operation::Write(Vec::<u8, 1024>::new());
    let Operation::Write(vec) = &mut operation else { unreachable()! };
    vec.push(5);

Edit: Fixed to use @kornel's suggestion instead of ref mut.

2 Likes

This can also be written without the esoteric ref mut:

let Operation::Write(vec) = &mut operation else { unreachable!() };`
2 Likes
enum Operation {
    //Open((usize, &'static str)),
    Close,
    Write([u8; 3]),
    //Read,
    //FileExists(&'static str),
}

pub fn main() {
    let mut operation = Operation::Close; // size of Vec<u8,1024>(largest type) + overhead gets allocated

    // Filehandler will do something with the operation

    let vec: [u8; 3] = [1, 2, 3];
    //vec.push(5); // Now sizeof(operation) and sizeof(vec) is allocated
    operation = Operation::Write(vec);

    // Filehandler will do something with the operation

    let s: &'static str = "Teststring";
    //string.push_str(s); // Now sizeof(operation) and sizeof(string) is allocated.
    //operation = Operation::FileExists(s);

    // Filehandler will do something with the operation
}

Assembly:

example::main::h02a42a1dcab42298:
        mov     byte ptr [rsp - 11], 0 // 4 Bytes (3Bytes Array + 1 Byte the enum value?)
        mov     byte ptr [rsp - 7], 1 // vec[0]
        mov     byte ptr [rsp - 6], 2 // vec[1]
        mov     byte ptr [rsp - 5], 3 // vec[2]
        mov     ax, word ptr [rsp - 7]
        mov     word ptr [rsp - 3], ax
        mov     al, byte ptr [rsp - 5]
        mov     byte ptr [rsp - 1], al
        mov     byte ptr [rsp - 4], 1
        mov     eax, dword ptr [rsp - 4]
        mov     dword ptr [rsp - 11], eax
        ret

When I enable the FileExists value in the enum and in the main I get:

example::main::h02a42a1dcab42298:
        mov     byte ptr [rsp - 80], 0 // 29 Bytes? Why
        mov     byte ptr [rsp - 51], 1

Why the size of the enum is now that large? I am expecting that the size increases to maybe 4 or 8 Bytes to store a reference + 1Byte the enum value.

Using your proposal:

enum Operation {
    //Open((usize, &'static str)),
    Close,
    Write([u8; 3]),
    //Read,
    //FileExists(&'static str),
}

pub fn main() {
    let mut operation = Operation::Close; // size of Vec<u8,1024>(largest type) + overhead gets allocated

    // Filehandler will do something with the operation

    //let vec: [u8; 3] = [1, 2, 3];
    //vec.push(5); // Now sizeof(operation) and sizeof(vec) is allocated
    operation = Operation::Write([0, 0, 0]);
    let Operation::Write(vec) = &mut operation else {unreachable!()};
    vec[0] = 1;
    vec[1] = 2;
    vec[2] = 3;

    // Filehandler will do something with the operation

    let s: &'static str = "Teststring";
    //string.push_str(s); // Now sizeof(operation) and sizeof(string) is allocated.
    //operation = Operation::FileExists(s);

    // Filehandler will do something with the operation
}

I get

example::main::h02a42a1dcab42298:
        sub     rsp, 24
        mov     byte ptr [rsp + 13], 0
        mov     byte ptr [rsp + 21], 0
        mov     byte ptr [rsp + 22], 0
        mov     byte ptr [rsp + 23], 0
        mov     ax, word ptr [rsp + 21]
        mov     word ptr [rsp + 18], ax
        mov     al, byte ptr [rsp + 23]
        mov     byte ptr [rsp + 20], al
        mov     byte ptr [rsp + 17], 1
        mov     eax, dword ptr [rsp + 17]
        mov     dword ptr [rsp + 13], eax
        mov     al, byte ptr [rsp + 13]
        and     al, 1
        movzx   eax, al
        cmp     rax, 1
        jne     .LBB0_2
        mov     byte ptr [rsp + 14], 1
        mov     byte ptr [rsp + 15], 2
        mov     byte ptr [rsp + 16], 3
        add     rsp, 24
        ret
.LBB0_2:
        lea     rdi, [rip + .L__unnamed_1]
        lea     rdx, [rip + .L__unnamed_2]
        mov     rax, qword ptr [rip + core::panicking::panic::h59297120e85ea178@GOTPCREL]
        mov     esi, 40
        call    rax

.L__unnamed_1:
        .ascii  "internal error: entered unreachable code"

.L__unnamed_3:
        .ascii  "/app/example.rs"

.L__unnamed_2:
        .quad   .L__unnamed_3
        .asciz  "\017\000\000\000\000\000\000\000\021\000\000\0006\000\000"

Looks like you changed from using a Vec and push, to declaring the values in an array without the need to push. My post was based on the assumption that you needed to use the push method.

But in fact it is hard to say whether it will be better or worse with my suggestion, since during construction the compiler may allocate the Vec first on the stack and then copy it into the enum variant. Is that what you see happening? You'll have to examine the asm to know what is best, that's not something I know how to do.

The pointer in the FileExists variant has alignment 8, which will probably force the enum discriminator to be 8 bytes as well.

I just changed it because then I was able to easily disassemble with https://rust.godbolt.org/

I don't think it will work to micro-optimize like this using code that isn't the actual code you'll be using.

2 Likes

I used your proposal about the reference

if let Operation::Write(vec) = &mut operation {
    vec[0] = data[0];
    vec[1] = data[1];
    vec[2] = data[2];
} else {
    operation = Operation::Write([0, 0, 0]);
    let Operation::Write(vec) = &mut operation else {unreachable!()};
   // Not done directly in the upper write because heapless::Vec does not support it
    vec[0] = 1;
    vec[1] = 2;
    vec[2] = 3;
}

ref mut is not "esoteric". It is the solution that is correct in terms of types; default binding modes are a hack.

1 Like

Patterns being strict duals of expressions were a mistake. It added new keywords and new meanings of other sigils to the language, which was a frequent point of confusion and friction for new users. It made the compiler nitpick about syntactic details that users did not understand or did not care about, valuing theoretical purity over usability. In retrospect, it was unnecessary, and the match ergonomics worked great. It made Rust easier and more convenient to use. Before it, the questions about ref vs & were frequent in this forum, but now we can bury ref and pretend it never existed without losing anything important.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.