memory-managementdynamic-memory-allocationzig

Why is copying more efficient in Zig remap?


The remap function in Zig's memory allocator interface is supposed to "attempt to expand or shrink memory, allowing relocation". In particular, it mentions that...

A null return value indicates that the resize would be equivalent to allocating new memory, copying the bytes from the old memory, and then freeing the old memory. In such case, it is more efficient for the caller to perform the copy.

I don't understand the claim that it is more efficient to perform a copy. If I want to copy some block of memory, I will have to allocate some new memory, copy the bytes over and (eventually) free the old block anyways. So what efficiency am I gaining?

The only thing I can think of is if I already have an already allocated block of memory that I can reuse for copying in which case I avoid the allocation step.


Solution

  • remap is not in any actual Zig release yet. It was checked into the master branch 2 days ago. It may not actually make it into any actual release.

    That said, looking at the uses of remap that were introduced in the same commit, we can see the kinds of cases that motivated this interface. The idea is that the caller can't perform the alloc/copy/free sequence more efficiently than remap, but it might not want to do that specific alloc/copy/free sequence anyway.

    For example, in array_list.zig, under addManyAt:

                const new_capacity = growCapacity(self.capacity, new_len);
                const old_memory = self.allocatedSlice();
                if (self.allocator.remap(old_memory, new_capacity)) |new_memory| {
                    self.items.ptr = new_memory.ptr;
                    self.capacity = new_memory.len;
                    return addManyAtAssumeCapacity(self, index, count);
                }
    
                // Make a new allocation, avoiding `ensureTotalCapacity` in order
                // to avoid extra memory copies.
                const new_memory = try self.allocator.alignedAlloc(T, alignment, new_capacity);
                const to_move = self.items[index..];
                @memcpy(new_memory[0..index], self.items[0..index]);
                @memcpy(new_memory[index + count ..][0..to_move.len], to_move);
                self.allocator.free(old_memory);
    

    If the remap can proceed without a copy, it does. Otherwise, the code allocates a new buffer, but it separately copies the parts of the original buffer before and after index into the parts of the new buffer they need to go in. It also avoids copying any undefined space at the end of the old buffer, between the old length and capacity.