Allocations on a = b

I was just curious at the mention of this, that a = b can or cannot trigger allocations - what situation are you thinking of, where it can? Is it a pitfall or just a curiosity?

3 Likes

I was wondering too. I don't think an assignment can trigger an allocation in itself. Unless of course we allow the b in the example to be an arbitrary expression like Box::new(…) in which case it can, but that has nothing to do with the assignment.

Maybe what OP was getting at is that an assignment runs the destructor of the LHS if any, and that can trigger a deallocation.

I suppose, a custom type could allocate in it's Drop impl.

5 Likes

Yes, surely, although that would certainly be considered at least unusual or outright bad practice.

You just need to go from b: &T to a: &U where T: Deref<Target=U> and have an allocation in the dereference implementation, like a debug message, or some lookup that needs a Vec buffer.

struct Foo;

impl std::ops::Deref for Foo {
    type Target = str;
    fn deref(&self) -> &str {
        println!("let's allocate!");
        drop(Box::new(23));
        "foo"
    }
}

fn main() {
    let b = &Foo;
    let a: &str;
    a = b;
}
7 Likes

Here's a real example: https://github.com/rust-lang/regex/blob/fce37e49329157f411e45a3350c42e6927996a40/regex-syntax/src/hir/mod.rs#L1447

The main idea is that Hir is a recursive data type, but I only want to use constant stack space when dealing with it.

3 Likes

Why would that be bad practice? I'd say it's a very good idea to document if an objects cleanup needs to be costly. Like sending an instrumentation message. Or because the object manages a large amount of other allocations and wants the ability to send some of them to a pool for reuse, but is selective about what it sends out.

There is also a transitivity issue here. Does everything you use inside your drop implementations promise to never allocate? One of my projects had an unregister_root_id that seems innocent enough, but can actually cause a number of reallocations if used on a transaction object that has to lift data from the main storage.

1 Like

Since I skipped this in my earlier comments,:I think it's just something to be aware of. It's fine until something accidentally relies on it not being there.

Because it's contrary to people's (reasonable) expectations.

That's kind of my point though? I don't think this expectation holds up in general. I would for example say it is reasonable that a database client library reserves the right to potentially perform allocations/reallocations or other such things on drop to manage a pool. I'd happily take a bit costlier drop if it pays off performance wise in the long run. Similar principle as the allocation pooling I mentioned above.

And if a it's an FFI wrapper type, things get more complicated because then we're entering different ways of doing things in general. How would a type wrapping some UI element driven via FFI ever guarantee the other project in another language won't perform a potentially costly operation when a widget is freed? How do we know how costly a rollback of a database connection caused by an early return will be?

It's probably more useful to anchor expectations to specific (edit: kinds of) types.

1 Like

I have to admit I found the idea a=b could allocate surprising, although it seems obvious in hindsight.

Custom type code can run anywhere traits are involved and Deref and Drop are traits that can be triggered implicitly. Rust is normally an explicit language, which is a very good thing. I can understand why implicit behavior is sometimes desirable, however it can make for unpredictable behavior because it is very hard to reason about things you can not see. Another area I see people seem to have trouble reasoning about are the various closure traits: probably partly because they are determined implicitly.

An example of implictness causing headaches taken from a (sort of) non-programming context: Linux from Scratch is a book of instructions for building an entire linux operating system from source packages. In the preliminary info from chapter 5 is an important note that gives steps to be repeated both before and after each and every one of 100-or-so odd packages which follow. From my time spent on forums and irc, guess what I have seen is one of the most common problems? Answer: Failing to remember to repeatedly do these implicit steps throughout the remainder of the book. Sure it cuts down on the length of the book and removes a lot of repetition, but it's difficult to account for something you can't see.

Obviously the Rust team weighs implicit behavior carefully before adding it to the language and I think it is good to only add it where the benefits strongly outweigh the inevitable cost.

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.