From de8d5b681f7acc551fb0358988f01bc9af5385ec Mon Sep 17 00:00:00 2001 From: Calin Culianu Date: Fri, 28 Aug 2020 21:21:01 +0300 Subject: [PATCH] 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. --- test/heap_frag_test.cpp | 339 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 test/heap_frag_test.cpp diff --git a/test/heap_frag_test.cpp b/test/heap_frag_test.cpp new file mode 100644 index 0000000..d45ed8a --- /dev/null +++ b/test/heap_frag_test.cpp @@ -0,0 +1,339 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + struct MemInfo { + size_t physUsed{}, virtUsed{}; + }; + // fwddecl + void platformInit(); + MemInfo getProcessMemUsage(); +} + +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#include +#include + +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 +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 +#include +namespace { + void platformInit() {} + MemInfo getProcessMemUsage() { + MemInfo ret; + std::ifstream file("/proc/self/status", std::ios_base::in); + if (!file) return ret; + std::array 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 +std::enable_if_t, 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 + 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 iterCtr = 0; +using ThreadFuncPromise = std::promise; +using ThreadFuncFuture = std::shared_future; +void threadFunc(int tnum, unsigned iters, ThreadFuncPromise promise, ThreadFuncFuture future) +{ + std::random_device rd; + std::mt19937 rng(rd()); + std::array, 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{} << 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 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 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().get_future())>; + std::list threads; + std::promise 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; +}