Added the stand-alone C++ test program "heap_frag_test.cpp"

This is a small CLI program that attempts to fragment the heap by
allocating a bunch of random data from multiple threads.  It then
stops and asks the user to hit enter.

It prints memory usage for physical and virtual memory used by the
process, and at the end it compares the amount of memory still mapped
(even after deallocating all buffers).

It can be used as a crude way to estimate the efficiency of an allocator
and how good it is at releasing memory to the OS.
This commit is contained in:
Calin Culianu 2020-08-28 21:21:01 +03:00
parent d5ef33f485
commit de8d5b681f
No known key found for this signature in database
GPG key ID: 21810A542031C02C

339
test/heap_frag_test.cpp Normal file
View file

@ -0,0 +1,339 @@
#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <future>
#include <iostream>
#include <list>
#include <mutex>
#include <random>
#include <sstream>
#include <string>
#include <type_traits>
#include <utility>
namespace {
struct MemInfo {
size_t physUsed{}, virtUsed{};
};
// fwddecl
void platformInit();
MemInfo getProcessMemUsage();
}
#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <psapi.h>
namespace {
MemInfo getProcessMemUsage() {
PROCESS_MEMORY_COUNTERS_EX pmc;
GetProcessMemoryInfo(GetCurrentProcess(), (PROCESS_MEMORY_COUNTERS*)&pmc, sizeof(pmc));
return { size_t{pmc.WorkingSetSize}, size_t{pmc.PrivateUsage} };
}
bool setupLFH() {
if (const auto *p = std::getenv("NOLFH"); p && std::string(p) != "0") {
std::cerr << "Windows: NOLFH set in env, not force-setting LFH" << std::endl;
return false;
}
HANDLE h = GetProcessHeap();
if (!h) {
auto err = GetLastError();
std::cerr << "Windows: GetProcessHeap error code " << err << std::endl;
return false;
}
ULONG HeapInfo = 2 /* setup LFH */;
auto bResult = HeapSetInformation(h, HeapCompatibilityInformation, &HeapInfo, sizeof(HeapInfo));
if (bResult != FALSE) {
std::cerr << "Windows: The low-fragmentation heap has been enabled" << std::endl;
return true;
} else {
auto err = GetLastError();
std::cerr << "Windows: Failed to set low-fragmentation heap, error code " << err << std::endl;
return false;
}
}
int queryHeapInfo() {
constexpr ULONG HEAP_STANDARD = 0;
constexpr ULONG HEAP_LAL = 1;
constexpr ULONG HEAP_LFH =2;
BOOL bResult;
HANDLE hHeap;
ULONG HeapInformation;
//
// Get a handle to the default process heap.
//
hHeap = GetProcessHeap();
if (hHeap == NULL) {
auto err = GetLastError();
std::cerr << "Windows: Failed to retrieve default process heap with LastError " << err << std::endl;
return 1;
}
//
// Query heap features that are enabled.
//
bResult = HeapQueryInformation(hHeap,
HeapCompatibilityInformation,
&HeapInformation,
sizeof(HeapInformation),
NULL);
if (bResult == FALSE) {
auto err = GetLastError();
std::cerr << "Windows: Failed to retrieve heap features with LastError " << err << std::endl;
return 1;
}
//
// Print results of the query.
//
std::cerr << "Windows: HeapCompatibilityInformation is " << HeapInformation << " - ";
switch(HeapInformation)
{
case HEAP_STANDARD:
std::cerr << "The default process heap is a standard heap." << std::endl;
break;
case HEAP_LAL:
std::cerr << "The default process heap supports look-aside lists." << std::endl;
break;
case HEAP_LFH:
std::cerr << "The default process heap has the low-fragmentation heap enabled." << std::endl;
break;
default:
std::cerr << "Unrecognized HeapInformation reported for the default process heap," << std::endl;
return 2;
}
return 0;
}
void platformInit() {
setupLFH();
queryHeapInfo();
}
} // namespace
#elif defined(__APPLE__)
#include<mach/mach.h>
namespace {
void platformInit() {}
MemInfo getProcessMemUsage() {
struct task_basic_info t_info;
mach_msg_type_number_t t_info_count = TASK_BASIC_INFO_COUNT;
if (KERN_SUCCESS != task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&t_info, &t_info_count)) {
return {};
}
return { size_t{t_info.resident_size}, size_t{t_info.virtual_size} };
}
}
#elif defined(__linux__)
#include <fstream>
#include <strings.h>
namespace {
void platformInit() {}
MemInfo getProcessMemUsage() {
MemInfo ret;
std::ifstream file("/proc/self/status", std::ios_base::in);
if (!file) return ret;
std::array<char, 256> buf;
buf[0] = 0;
// sizes are in kB
while (file.getline(buf.data(), buf.size()) && (ret.physUsed == 0 || ret.virtUsed == 0)) {
if (strncasecmp(buf.data(), "VmSize:", 7) == 0) {
std::istringstream is(buf.data() + 7);
is >> std::skipws >> ret.virtUsed;
ret.virtUsed *= 1024;
} else if (strncasecmp(buf.data(), "VmRSS:", 6) == 0) {
std::istringstream is(buf.data() + 6);
is >> std::skipws >> ret.physUsed;
ret.physUsed *= 1024;
}
}
return ret;
}
}
#else
namespace {
void platformInit() {}
// Unknown platform -- this will always return 0
MemInfo getProcessMemUsage() {
std::cerr << "Warning: Unknown platform, " << __func__ << " returning 0!" << std::endl;
return {};
}
}
#endif
namespace {
template <typename Num>
std::enable_if_t<std::is_integral_v<Num>, std::string>
commaNum(Num num, bool showpos = false)
{
std::string s;
if (!num) {
if (showpos) s.insert(s.begin(), '+');
s += "0";
return s;
}
bool neg{};
if (num < 0) {
neg = true;
num = -num;
}
for (int i = 0; num; ++i) {
if (i && i % 3 == 0)
s += ',';
s += '0' + (num % 10);
num /= 10;
}
if (neg) s += '-';
else if (showpos) s += '+';
std::reverse(s.begin(), s.end());
return s;
}
//! Thread-safe stdout printer
class Print {
std::ostringstream oss{};
public:
static thread_local std::string name;
~Print() {
static std::mutex mut;
oss.flush();
std::unique_lock g(mut);
if (!name.empty())
std::cout << "[" << name << "] ";
std::cout << oss.str() << std::endl << std::flush;
}
template <typename T>
auto &operator<<(T && t) { return oss << t; }
};
thread_local std::string Print::name;
inline constexpr auto MEAN = 1000, STDEV = 990, FACTOR = 100, RATIO = 8;
inline constexpr auto periter = 100'000u;
std::atomic<size_t> iterCtr = 0;
using ThreadFuncPromise = std::promise<size_t>;
using ThreadFuncFuture = std::shared_future<void>;
void threadFunc(int tnum, unsigned iters, ThreadFuncPromise promise, ThreadFuncFuture future)
{
std::random_device rd;
std::mt19937 rng(rd());
std::array<std::normal_distribution<>, 2> dists = { std::normal_distribution<>{MEAN, STDEV},
std::normal_distribution<>{MEAN*FACTOR, STDEV*FACTOR} };
static_assert (RATIO > 0);
auto gen = [&dists, &rng, i=0U]() mutable {
auto &dist = dists[i++ % RATIO == 0 ? 1 : 0];
return std::max(size_t(std::round(std::abs(dist(rng)))), size_t(1));
};
constexpr auto avg2 = [](auto x) { return (x*(RATIO-1) + x*FACTOR) / RATIO; };
Print::name = dynamic_cast<std::ostringstream &&>(std::ostringstream{} << tnum).str();
Print() << "Iterating " << commaNum(iters) << " time(s), with " << commaNum(periter)
<< " allocations per iteration, avg. size " << avg2(MEAN) << ", median size: " << MEAN;
size_t nBytesTotal{};
struct Buffer {
char * const data;
Buffer(const size_t size) : data(new char[size]) {
if (size)
data[size-1] = 0xef;
}
~Buffer() { delete [] data; }
};
std::list<Buffer> allocs;
for (auto iter = 0U; iter < iters; ++iter) {
allocs.clear();
Print() << "Iteration " << commaNum(iter) << ", nBytes so far: " << commaNum(nBytesTotal);
size_t nBytes{};
for (auto i = 0U; i < periter; ++i) {
const auto nb = gen();
nBytes += nb;
nBytesTotal += nb;
allocs.emplace_back(nb);
iterCtr.fetch_add(1, std::memory_order::memory_order_relaxed);
}
Print() << "Allocated " << commaNum(nBytes) << " this iteration";
}
promise.set_value(nBytesTotal);
future.wait(); // wait for main thread to tell us to clear the list
allocs.clear();
Print() << "Thread exited";
}
} // namespace
int main(int argc, const char *argv[])
{
platformInit();
using std::cout; using std::cerr; using std::endl; using std::flush;
unsigned iters = 10, nthr = std::min(std::max(std::thread::hardware_concurrency(), 1u), 6u);
if (argc > 1) {
const auto usage = [prog = argv[0]] {
cerr << "Usage: " << prog << " ITERS [NTHREADS]" << endl;
std::exit(1);
};
if (argc > 3)
usage();
if (int val{}; std::sscanf(argv[1], "%u", &val) != 1 || val <= 0)
usage();
else
iters = val;
if (argc == 3) {
if (int val{}; std::sscanf(argv[2], "%d", &val) != 1 || val <= 0)
usage();
else
nthr = val;
}
}
const auto readLine = [] {
std::array<char, 256> buf;
if (!std::cin.getline(buf.data(), buf.size()))
buf[0] = 0;
return std::string(buf.data());
};
const auto getMemUsedStr = [] {
const auto [phys, virt] = getProcessMemUsage();
std::ostringstream oss;
oss << "Mem. usage - phys: " << commaNum(phys/1'000UL) << " kB, virt: " << commaNum(virt/1'000UL) << " kB" << flush;
return oss.str();
};
const auto startingMem = getProcessMemUsage();
{
using TheradAndFuture = std::pair<std::thread, decltype(std::declval<ThreadFuncPromise>().get_future())>;
std::list<TheradAndFuture> threads;
std::promise<void> kickThreads;
{
auto kickSharedFut = kickThreads.get_future().share();
cout << "Starting " << nthr << " threads" << endl;
for (auto i = 0u; i < nthr; ++i) {
ThreadFuncPromise p;
auto fut = p.get_future();
auto t = std::thread(threadFunc, i+1, iters, std::move(p), kickSharedFut);
threads.emplace_back(std::move(t), std::move(fut));
}
}
size_t nBytesTotal{};
for (auto & [_, fut] : threads)
nBytesTotal += fut.get();
auto memNow = getProcessMemUsage();
cout << "Finished!" << " Total allocated: " << commaNum(nBytesTotal/1'000UL) << " kB, iterations: "
<< commaNum(iterCtr.load())
<< endl << endl << getMemUsedStr() << endl << endl
<< "Hit enter to clear list(s): " << flush;
readLine();
kickThreads.set_value();
for (auto & [thread, _] : threads)
thread.join();
memNow = getProcessMemUsage();
cout << "List(s) cleared" << endl << endl << getMemUsedStr() << endl << endl << "Hit enter to exit program: " << flush;
// scope end hopefully does even more cleanup
}
readLine();
const auto endingMem = getProcessMemUsage();
cout << endl
<< "Delta from program start - phys: " << commaNum((ssize_t(endingMem.physUsed) - ssize_t(startingMem.physUsed))/1'000L, true) << " kB, "
<< "virt: " << commaNum((ssize_t(endingMem.physUsed) - ssize_t(startingMem.physUsed))/1'000L, true) << " kB" << endl;
return 0;
}