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; +}