Passing values around

I am writing some piece of code that utilizes OpenSSL as follows

    symm::Crypter::new(cipher, symm::Mode::Encrypt, key, Some(iv))
        .and_then(|mut ctx| ctx.aad_update(aad).map(|_| ctx))
        .and_then(|mut ctx| {
            ctx.update(plaintext, &mut ciphertext)
                .map(|size| (size, ctx))
        });

Here, I am not sure if I do the right thing passing the ctx from one and_then to another.
Is it copied on every stage, again and again, resulting in that many allocations or does the compiler understand that I am using the same thing and uses the same object which was allocated during the call of new?

Just pass around stuff by value unless you have a good reason not to, and don't worry too much about it. Unless you specifically ask for heap allocation (e.g. Box, Vec, etc.), there won't be any, and function arguments and other locals go on the stack by default. They might even be put in registers or optimized away completely.

And always remember, all of this is an implementation detail. You shouldn't rely on values residing in a specific place for correctness. And before trying to micro-optimize your code like this, first and foremost make sure that you wrote what expresses the sematics of your intent the best.

1 Like

I just didnt want to create lots of new structs.
So stack variables are always passed by their reference down to other functions or returned from others is that correct ?

Rust never allocates behind your back.

Structs are not passed by reference automatically, but the optimizer can avoid unnecessary copies in many cases.

I am just trying to understand how the memory is used exactly. So optimizer can handle things you say but as I understand what will be copied what will be referenced will be up to optimizer in that case which means to be sure and deterministic one must explicitly state passing things by ref or value is that right @kornel ?

Rust, the language, doesn't allocate anything on the heap. With #[no_std] the heap doesn't even need to exist. So it never allocates anything for you, except places where you explicitly ask for it by calling a heap-allocating method like Box::new() or Vec.push().

You never ever have to worry about things getting unintentionally allocated. The lack of garbage collection in Rust also means Rust doesn't know how to make the garbage.

Everything else happens on stack. Passing of ownership of structs to a function technically involves copying them from one place on the stack to another place on the same stack. But the optimizer may see that's a pointless operation, and eliminate it.

1 Like

What I meant by allocating was copying data from one location to another. Sorry the the misunderstanding.

So what I am saying is that copying a huge chunk of data from one place to another is still a lot of work to do. Which you say rust can optimize away.

So are the examples below equivalent ?

let mut thing = SomeHugeThing::new().unwrap();
thing.foo()
.and_then(|_|thing.bar())
.and_then(|_|thing.baz())
SomeHugeThing
.and_then(|mut thing|thing.foo().map(|_|thing))
.and_then(|mut thing|thing.bar().map(|_|thing))
.and_then(|mut thing|thing.baz().map(|_|thing))

mut next to variable name has no influence over compiled code. It's only controls what you can do with that binding (variable name), but not even what you can do with the data held in it. It's irrelevant.

All of these constructs are just a complex way of writing:

fn id(x: SomeHugeThing) -> SomeHugeThing {x}

id(id(id(id(id(SomeHugeThing)))));

which will mean that in an unoptimized implementation it'll get memcpyd over and over again into and out of these function calls.

But then/and_then/map are small functions, and they're likely to get inlined, and then the code will look more like this:

let a = SomeHugeThing;
let b = a;
let c = b;
let d = c;
let c = d;
let b = c;
let a = b;

and the optimizer should be able to see that all of it is unnecessary.

If SomeHugeThing is really huge, it should be Box<SomeHugeThing>, and then the worst case will be the same as the best case, at a cost of indirection when accessing it.

@kornel why doesnt the optimizer optimizes the case with id(id(id(id(id(SomeHugeThing))))) ?

It does. You can check these things on rust.godbolt.com (but don't forget to add optimization flag!)

Wait I am kinda confused above you told they are memcpyd so they are not memcpyd in this case right

They start as a memcpy-equivalent before optimization, and then optimizer works hard to remove them all.

In statically dispatched inlineable functions copies will be very likely eliminated. In dyn-dispatched functions they won't. In extern "C" functions probably won't, unless you do clever stuff with cross-language LTO. I'm not sure about non-inlineable Rust functions.

1 Like

I understand. Thank you for your time and patience :slight_smile:

1 Like

No. As a first approximation, passing by value is equivalent to memcpy(), but again, it may be optimized out.

Please, read the answers of those who are willing to help you carefully. Kornel wrote that they are memcpy'd in an unoptimized build.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.