[libc-commits] [libc] 243ddf6 - [libc] add shrink in-place support for reallocations (#200272)
via libc-commits
libc-commits at lists.llvm.org
Tue Jun 2 17:12:18 PDT 2026
Author: Schrodinger ZHU Yifan
Date: 2026-06-02T17:12:12-07:00
New Revision: 243ddf607bbe4841cfe734507120c8d8ddc41eca
URL: https://github.com/llvm/llvm-project/commit/243ddf607bbe4841cfe734507120c8d8ddc41eca
DIFF: https://github.com/llvm/llvm-project/commit/243ddf607bbe4841cfe734507120c8d8ddc41eca.diff
LOG: [libc] add shrink in-place support for reallocations (#200272)
This PR adds shrinking in-place for the freelist heap. This allows the
heap to reuse the place if the reallocation shrinks the size larger than
a minimal block unit.
Synthesized random action tests show that that increase heap utilization
rate from 87% to 97% percent, basically aligns with the expectation of
dlmalloc.
Assisted-by: AI tools, manually checked.
Added:
Modified:
libc/src/__support/block.h
libc/src/__support/freelist_heap.h
libc/src/__support/freestore.h
libc/test/src/__support/CMakeLists.txt
libc/test/src/__support/block_test.cpp
libc/test/src/__support/freelist_heap_test.cpp
Removed:
################################################################################
diff --git a/libc/src/__support/block.h b/libc/src/__support/block.h
index d45af1079a5bc..25a2240a06435 100644
--- a/libc/src/__support/block.h
+++ b/libc/src/__support/block.h
@@ -432,8 +432,6 @@ optional<Block *> Block::split(size_t new_inner_size,
size_t usable_space_alignment) {
LIBC_ASSERT(usable_space_alignment % MIN_ALIGN == 0 &&
"alignment must be a multiple of MIN_ALIGN");
- if (used())
- return {};
// Compute the minimum outer size that produces a block of at least
// `new_inner_size`.
@@ -452,13 +450,16 @@ optional<Block *> Block::split(size_t new_inner_size,
outer_size() - new_outer_size < sizeof(Block))
return {};
+ bool was_free = !used();
+
ByteSpan new_region = region().subspan(new_outer_size);
next_ &= ~SIZE_MASK;
next_ |= new_outer_size;
Block *new_block = as_block(new_region);
- mark_free(); // Free status for this block is now stored in new_block.
- new_block->next()->prev_ = new_region.size();
+ new_block->mark_free();
+ if (was_free)
+ mark_free();
LIBC_ASSERT(new_block->is_usable_space_aligned(usable_space_alignment) &&
"usable space must have requested alignment");
diff --git a/libc/src/__support/freelist_heap.h b/libc/src/__support/freelist_heap.h
index 66f3739efe214..2cce64381faea 100644
--- a/libc/src/__support/freelist_heap.h
+++ b/libc/src/__support/freelist_heap.h
@@ -53,6 +53,8 @@ class FreeListHeap {
void *allocate_impl(size_t alignment, size_t size);
+ bool shrink_in_place(Block *block, size_t size);
+
span<cpp::byte> block_to_span(Block *block) {
return span<cpp::byte>(block->usable_space(), block->inner_size());
}
@@ -158,6 +160,38 @@ LIBC_INLINE void FreeListHeap::free(void *ptr) {
free_store.insert(block);
}
+LIBC_INLINE bool FreeListHeap::shrink_in_place(Block *block, size_t size) {
+ size_t min_outer_size = Block::outer_size(cpp::max(size, sizeof(size_t)));
+ uintptr_t next_block_start = Block::next_possible_block_start(
+ reinterpret_cast<uintptr_t>(block) + min_outer_size, Block::MIN_ALIGN);
+ size_t new_outer_size = next_block_start - reinterpret_cast<uintptr_t>(block);
+ if (block->outer_size() >= new_outer_size) {
+ // TODO: this is a temporary workaround due to Block's setting prev_
+ // in its constructor. This code should be deleted once we finishing
+ // refactoring.
+ cpp::byte *overlap_ptr =
+ reinterpret_cast<cpp::byte *>(block) + new_outer_size;
+ size_t backup;
+ LIBC_NAMESPACE::inline_memcpy(&backup, overlap_ptr, sizeof(size_t));
+ optional<Block *> next = block->split(size);
+
+ LIBC_NAMESPACE::inline_memcpy(overlap_ptr, &backup, sizeof(size_t));
+
+ // register the new block on successful split
+ if (next.has_value()) {
+ Block *next_block = *next;
+ Block *right = next_block->next();
+ if (right != nullptr && !right->used()) {
+ free_store.remove(right);
+ next_block->merge_next();
+ }
+ free_store.insert(next_block);
+ }
+ return true;
+ }
+ return false;
+}
+
// Follows constract of the C standard realloc() function
// If ptr is free'd, will return nullptr.
LIBC_INLINE void *FreeListHeap::realloc(void *ptr, size_t size) {
@@ -180,10 +214,10 @@ LIBC_INLINE void *FreeListHeap::realloc(void *ptr, size_t size) {
return nullptr;
size_t old_size = block->inner_size();
- // Do nothing and return ptr if the required memory size is smaller than
- // the current size.
- if (old_size >= size)
+ if (old_size >= size) {
+ shrink_in_place(block, size);
return ptr;
+ }
void *new_ptr = allocate(size);
// Don't invalidate ptr if allocate(size) fails to initilize the memory.
diff --git a/libc/src/__support/freestore.h b/libc/src/__support/freestore.h
index 2dcb4b10b93d5..d611c17af4644 100644
--- a/libc/src/__support/freestore.h
+++ b/libc/src/__support/freestore.h
@@ -16,6 +16,8 @@ namespace LIBC_NAMESPACE_DECL {
/// A best-fit store of variously-sized free blocks. Blocks can be inserted and
/// removed in logarithmic time.
class FreeStore {
+ friend class FreeListHeap;
+
public:
FreeStore() = default;
FreeStore(const FreeStore &other) = delete;
diff --git a/libc/test/src/__support/CMakeLists.txt b/libc/test/src/__support/CMakeLists.txt
index 6b9c1b4ac8cc7..ea271513beffb 100644
--- a/libc/test/src/__support/CMakeLists.txt
+++ b/libc/test/src/__support/CMakeLists.txt
@@ -69,6 +69,7 @@ if(LLVM_LIBC_FULL_BUILD AND NOT LIBC_TARGET_OS_IS_GPU)
libc.src.__support.freelist_heap
libc.src.string.memcmp
libc.src.string.memcpy
+ libc.src.string.memory_utils.inline_memset
)
endif()
diff --git a/libc/test/src/__support/block_test.cpp b/libc/test/src/__support/block_test.cpp
index 3029cde834a5d..189e3cab1d784 100644
--- a/libc/test/src/__support/block_test.cpp
+++ b/libc/test/src/__support/block_test.cpp
@@ -248,7 +248,7 @@ TEST(LlvmLibcBlockTest, CanMarkBlockUsed) {
EXPECT_FALSE(block->used());
}
-TEST(LlvmLibcBlockTest, CannotSplitUsedBlock) {
+TEST(LlvmLibcBlockTest, CanSplitUsedBlock) {
constexpr size_t kN = 1024;
constexpr size_t kSplitN = 512;
@@ -259,7 +259,13 @@ TEST(LlvmLibcBlockTest, CannotSplitUsedBlock) {
block->mark_used();
result = block->split(kSplitN);
- ASSERT_FALSE(result.has_value());
+ ASSERT_TRUE(result.has_value());
+ Block *new_block = *result;
+
+ EXPECT_TRUE(block->used());
+ EXPECT_FALSE(new_block->used());
+ EXPECT_EQ(block->next(), new_block);
+ EXPECT_EQ(new_block->prev_free(), static_cast<Block *>(nullptr));
}
TEST(LlvmLibcBlockTest, CanMergeWithNextBlock) {
diff --git a/libc/test/src/__support/freelist_heap_test.cpp b/libc/test/src/__support/freelist_heap_test.cpp
index 9d3a6b612555f..3002834a51619 100644
--- a/libc/test/src/__support/freelist_heap_test.cpp
+++ b/libc/test/src/__support/freelist_heap_test.cpp
@@ -11,6 +11,7 @@
#include "src/__support/macros/config.h"
#include "src/string/memcmp.h"
#include "src/string/memcpy.h"
+#include "src/string/memory_utils/inline_memset.h"
#include "test/UnitTest/Test.h"
asm(R"(
@@ -180,10 +181,54 @@ TEST_FOR_EACH_ALLOCATOR(ReallocSmallerSize, 2048) {
void *ptr1 = allocator.allocate(ALLOC_SIZE);
void *ptr2 = allocator.realloc(ptr1, kNewAllocSize);
- // For smaller sizes, realloc will not shrink the block.
+ // For smaller sizes, realloc will shrink the block in-place, returning the
+ // same pointer.
EXPECT_EQ(ptr1, ptr2);
}
+constexpr size_t SMALL_HEAP_SIZE = 512;
+
+TEST_FOR_EACH_ALLOCATOR(ReallocShrinkAndAllocateTrailing, SMALL_HEAP_SIZE) {
+ constexpr size_t ALLOC_SIZE = (SMALL_HEAP_SIZE * 3) / 4;
+ constexpr size_t NEW_ALLOC_SIZE = SMALL_HEAP_SIZE / 4;
+ constexpr size_t TRAILING_ALLOC_SIZE = SMALL_HEAP_SIZE / 2;
+
+ void *ptr1 = allocator.allocate(ALLOC_SIZE);
+ ASSERT_NE(ptr1, static_cast<void *>(nullptr));
+
+ // Fill the block with a pattern to verify data preservation.
+ LIBC_NAMESPACE::inline_memset(ptr1, 0x5A, ALLOC_SIZE);
+
+ // Shrink the block in-place.
+ void *ptr2 = allocator.realloc(ptr1, NEW_ALLOC_SIZE);
+ EXPECT_EQ(ptr1, ptr2);
+
+ // Verify that the data in the shrunk block is preserved.
+ unsigned char *uptr2 = static_cast<unsigned char *>(ptr2);
+ for (size_t i = 0; i < NEW_ALLOC_SIZE; ++i)
+ EXPECT_EQ(uptr2[i], static_cast<unsigned char>(0x5A));
+
+ // Allocate another trailing block from the newly split/freed space.
+ void *ptr3 = allocator.allocate(TRAILING_ALLOC_SIZE);
+ ASSERT_NE(ptr3, static_cast<void *>(nullptr));
+
+ // Verify that the trailing block does not overlap with the shrunk block.
+ uintptr_t addr2 = reinterpret_cast<uintptr_t>(ptr2);
+ uintptr_t addr3 = reinterpret_cast<uintptr_t>(ptr3);
+ EXPECT_GE(addr3, addr2 + NEW_ALLOC_SIZE);
+
+ // Verify that writing to the trailing block works without corrupting the
+ // shrunk block.
+ LIBC_NAMESPACE::inline_memset(ptr3, 0xAA, TRAILING_ALLOC_SIZE);
+
+ for (size_t i = 0; i < NEW_ALLOC_SIZE; ++i)
+ EXPECT_EQ(uptr2[i], static_cast<unsigned char>(0x5A));
+
+ // Free the allocations.
+ allocator.free(ptr2);
+ allocator.free(ptr3);
+}
+
TEST_FOR_EACH_ALLOCATOR(ReallocTooLarge, 2048) {
constexpr size_t ALLOC_SIZE = 512;
size_t kNewAllocSize = N * 2; // Large enough to fail.
More information about the libc-commits
mailing list