bytes icon indicating copy to clipboard operation
bytes copied to clipboard

Enable shrinking of allocations

Open tgnottingham opened this issue 5 months ago • 0 comments

I would like to be able to shrink allocations in certain cases, but I don't immediately see how to do that.

I should mention that my understanding of this crate is limited, so there's a strong possibility I'm using it wrong. And I also realize that whether or not an allocator would actually shrink an allocation without copying is up to the implementation.

Here's a semi-abstract example:

use bytes::BytesMut;

fn main() {
    const N: usize = 2 * 1024 * 1024;

    // b1 contains a lot of data, relatively. Let's say that it contains some large HTTP requests.
    let mut b1 = BytesMut::zeroed(N);
    print("b1", &b1);
    println!();

    // Prints:
    // b1 -- addr: 0x75af143ff010, len: 2097152, cap: 2097152, spare: 0

    // We parse b1, and find that we only need to split off a small part of it. E.g. we want to
    // split off an HTTP header value (ignoring that the code doesn't exactly illustrate this).
    let b2 = b1.split_to(32);
    print("b1", &b1);
    print("b2", &b2);
    println!();

    // Prints:
    // b1 -- addr: 0x75af143ff030, len: 2097120, cap: 2097120, spare: 0
    // b2 -- addr: 0x75af143ff010, len: 32, cap: 32, spare: 0

    // Later, some new data comes in, and we add more data to b1, causing it to reallocate. b1's
    // old capacity doesn't get deallocated, so b2 would be able to make use of it were we to add
    // to b2 (though b2 doesn't report it as part of its capacity). But if we don't have a need to
    // add to b2, that space is wasted. It seems the only way to deallocate it would be to copy b2
    // to another location, which would undermine our reason for using the bytes crate. :/
    b1.extend_from_slice(b"1");
    print("b1", &b1);
    print("b2", &b2);
    println!();

    // Prints:
    // b1 -- addr: 0x75af141ff010, len: 2097121, cap: 2097121, spare: 0
    // b2 -- addr: 0x75af143ff010, len: 32, cap: 32, spare: 0
}

fn print(name: &str, bm: &BytesMut) {
    let addr = bm.as_ptr();
    let cap = bm.capacity();
    let len = bm.len();
    let spare = cap - len;
    println!("{} -- addr: {:p}, len: {}, cap: {}, spare: {}", name, addr, len, cap, spare);
}

The issue is the same whether or not we freeze b2 into Bytes.

It would be nice if there were some way to shrink the b2 allocation, but I don't know what the best way of doing that would be.

If there were an explicit function along the lines of Vec::shrink_to_fit, the code using b2 wouldn't necessarily know when b1 had reallocated and therefore when a good time to call that shrink function would be. Still, this option might be acceptable, especially if the function was extremely inexpensive when shrinking wasn't necessary, so that the code using b2 could call it periodically without concern of too much of a performance hit.

And I imagine this would only work if the Bytes or BytesMut you're trying to shrink is the only one still referencing the allocation, which makes it less useful.

So, is there a good way of making shrinking allocations possible? And am I this crate wrong? :)

Edit 1: noticed that this issue is similar to #634, though it's not quite the same, as that's about adding documentation concerning deallocations, and this is more of a feature request for shrinking allocations.

Edit 2: Also saw a similar issue discussed in #401, which implies that shrinking would incur a copy in practice (at least given the default allocator?).

I'm guessing the advice in https://github.com/tokio-rs/bytes/issues/634#issuecomment-1738946758 is going to be the answer here, but I'd be happy to hear otherwise.

tgnottingham avatar Mar 18 '24 19:03 tgnottingham