Skip to content

Commit 977ad2a

Browse files
committed
resizable_memory: release pages with MADV_DONTNEED on linux
1 parent 68acd13 commit 977ad2a

File tree

2 files changed

+84
-18
lines changed

2 files changed

+84
-18
lines changed

include/decodeless/detail/mappedfile_linux.hpp

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ class MemoryMap {
8888
static constexpr bool ProtNone = MemoryProtection == PROT_NONE;
8989
static constexpr bool Writable = (MemoryProtection & PROT_WRITE) != 0;
9090
using address_type = std::conditional_t<Writable || ProtNone, void*, const void*>;
91+
using byte_type = std::conditional_t<Writable || ProtNone, std::byte, const std::byte>;
9192
MemoryMap(address_type addr, size_t length, int flags, int fd, off_t offset)
9293
: m_size(length)
9394
, m_address(mmap(const_cast<void*>(addr), length, MemoryProtection, flags, fd, offset))
@@ -123,6 +124,9 @@ class MemoryMap {
123124
unmap();
124125
}
125126
address_type address() const { return m_address; }
127+
address_type address(size_t offset) const {
128+
return static_cast<address_type>(static_cast<byte_type*>(m_address) + offset);
129+
}
126130
size_t size() const { return m_size; }
127131
void sync(int flags = MS_SYNC | MS_INVALIDATE)
128132
requires Writable
@@ -135,12 +139,7 @@ class MemoryMap {
135139
throw LastError();
136140
}
137141
void resize(size_t size) {
138-
#if 0
139-
void* addr = mremap(m_address, m_size, size,
140-
MREMAP_MAYMOVE | MREMAP_FIXED | MREMAP_DONTUNMAP, m_address);
141-
#else
142142
void* addr = mremap(m_address, m_size, size, 0);
143-
#endif
144143
if (addr == MAP_FAILED) {
145144
throw LastError();
146145
} else if (addr != m_address) {
@@ -268,23 +267,34 @@ class ResizableMappedMemory {
268267
size_t ps = pageSize();
269268
size_t newMappedSize = ((size + ps - 1) / ps) * ps;
270269

271-
// Add/remove just the new range
272-
if (newMappedSize > m_mappedSize)
273-
protect(m_mappedSize, newMappedSize - m_mappedSize, PROT_READ | PROT_WRITE);
274-
else if (newMappedSize < m_mappedSize)
275-
protect(newMappedSize, m_mappedSize - newMappedSize, PROT_NONE);
270+
if (newMappedSize > m_mappedSize) {
271+
// Add just the new range
272+
if (mprotect(m_reserved.address(m_mappedSize), newMappedSize - m_mappedSize,
273+
PROT_READ | PROT_WRITE) != 0)
274+
throw LastError();
275+
} else if (newMappedSize < m_mappedSize) {
276+
// Release unused range to the OS. mprotect() will not do this.
277+
// Using MADV_DONTNEED is many times faster than mmap().
278+
#if 1
279+
if (mprotect(m_reserved.address(newMappedSize), m_mappedSize - newMappedSize,
280+
PROT_NONE) != 0)
281+
throw LastError();
282+
if (madvise(m_reserved.address(newMappedSize), m_mappedSize - newMappedSize,
283+
MADV_DONTNEED) != 0)
284+
throw LastError();
285+
#else
286+
if (mmap(m_reserved.address(newMappedSize), m_mappedSize - newMappedSize, PROT_NONE,
287+
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED, -1, 0) == MAP_FAILED)
288+
throw LastError();
289+
#endif
290+
}
276291
m_mappedSize = newMappedSize;
277292
m_size = size;
278293
}
279294

280295
ResizableMappedMemory& operator=(ResizableMappedMemory&& other) noexcept = default;
281296

282297
private:
283-
void protect(size_t offset, size_t size, int prot) {
284-
if (mprotect(reinterpret_cast<void*>(uintptr_t(m_reserved.address()) + offset), size,
285-
prot) != 0)
286-
throw LastError();
287-
}
288298
detail::MemoryMap<PROT_NONE> m_reserved;
289299
size_t m_size = 0;
290300
size_t m_mappedSize = 0;

test/src/mappedfile.cpp

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
// Copyright (c) 2024 Pyarelal Knowles, MIT License
22

3+
#include <algorithm>
34
#include <cstdio>
5+
#include <decodeless/mappedfile.hpp>
46
#include <filesystem>
57
#include <fstream>
68
#include <gtest/gtest.h>
7-
#include <decodeless/mappedfile.hpp>
89
#include <ostream>
910
#include <span>
1011

@@ -226,7 +227,7 @@ TEST_F(MappedFileFixture, LinuxResize) {
226227

227228
#endif
228229

229-
TEST_F(MappedFileFixture, ResizeMemory) {
230+
TEST(MappedMemory, ResizeMemory) {
230231
const char str[] = "hello world!";
231232
{
232233
resizable_memory file(0, 10000);
@@ -270,7 +271,7 @@ TEST_F(MappedFileFixture, ResizeMemory) {
270271
}
271272
}
272273

273-
TEST_F(MappedFileFixture, ResizeMemoryExtended) {
274+
TEST(MappedMemory, ResizeMemoryExtended) {
274275
size_t nextBytes = 1;
275276
resizable_memory memory(nextBytes, 1llu << 32); // 4gb of virtual memory pls
276277
void* data = memory.data();
@@ -425,3 +426,58 @@ TEST_F(MappedFileFixture, Readme) {
425426
fs::remove(tmpFile2);
426427
EXPECT_FALSE(fs::exists(tmpFile2));
427428
}
429+
430+
#ifndef _WIN32
431+
std::vector<uint8_t> getResidency(void* base, size_t size) {
432+
std::vector<unsigned char> result(size / getpagesize(), 0u);
433+
int ret = mincore(base, size, result.data());
434+
if (ret != 0)
435+
throw detail::LastError();
436+
return result;
437+
}
438+
439+
TEST(MappedMemory, PageResidencyAfterDecommit) {
440+
const size_t page_size = getpagesize();
441+
const size_t reserve_size = page_size * 64; // 64 pages total
442+
const size_t commit_size = page_size * 4; // We'll use 4 pages
443+
444+
// Reserve virtual address space (uncommitted, inaccessible)
445+
void* base =
446+
mmap(nullptr, reserve_size, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
447+
ASSERT_NE(base, MAP_FAILED) << "Failed to mmap reserved space";
448+
EXPECT_TRUE(std::ranges::all_of(getResidency(base, commit_size),
449+
[](uint8_t c) { return (c & 1u) == 0; }));
450+
451+
// Commit a portion with PROT_READ | PROT_WRITE
452+
int prot_result = mprotect(base, commit_size, PROT_READ | PROT_WRITE);
453+
ASSERT_EQ(prot_result, 0) << "Failed to mprotect committed region";
454+
EXPECT_TRUE(std::ranges::all_of(getResidency(base, commit_size),
455+
[](uint8_t c) { return (c & 1u) == 0; }));
456+
457+
// Touch the memory to ensure it's backed by RAM
458+
std::span committed(static_cast<std::byte*>(base), commit_size);
459+
std::ranges::fill(committed, std::byte(0xAB));
460+
461+
// Verify pages are resident using mincore
462+
EXPECT_TRUE(std::ranges::all_of(getResidency(base, commit_size),
463+
[](uint8_t c) { return (c & 1u) == 1; }));
464+
465+
// Decommit
466+
#if 0
467+
void* remap = mmap(base, commit_size, PROT_NONE,
468+
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED, -1, 0);
469+
ASSERT_EQ(remap, base) << "Failed to remap to decommit pages";
470+
#else
471+
// See MADV_FREE discussion here: https://github.com/golang/go/issues/42330
472+
prot_result = mprotect(base, commit_size, PROT_NONE);
473+
ASSERT_EQ(prot_result, 0) << "Failed to mprotect committed region back to PROT_NONE";
474+
int madvise_result = madvise(base, commit_size, MADV_DONTNEED);
475+
ASSERT_EQ(madvise_result, 0) << "Failed to release pages with madvise";
476+
#endif
477+
EXPECT_TRUE(std::ranges::all_of(getResidency(base, commit_size),
478+
[](uint8_t c) { return (c & 1u) == 0; }));
479+
480+
// Cleanup
481+
munmap(base, reserve_size);
482+
}
483+
#endif

0 commit comments

Comments
 (0)