diff --git a/.gitignore b/.gitignore index a08f415..5f7502e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ /build* /.vscode/ +/.idea/ +*.log +*.so diff --git a/CMakeLists.txt b/CMakeLists.txt index c82c5be..5e9a230 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,19 @@ add_library(memstats) target_sources(memstats PRIVATE memstats.cc) target_link_libraries(memstats PRIVATE $ $) +option(USE_MEMORY_TRACER "Enable memory tracing" OFF) + +if(USE_MEMORY_TRACER) + target_compile_definitions(memstats PRIVATE MEMSTATS_USE_MEMORY_TRACER) + + set(PINTOOL_PATH "" CACHE PATH "") + + if (NOT PINTOOL_PATH) + message(FATAL_ERROR "For using memstats with memory tracing, the path to the directory of pintool is required.") + endif() + execute_process(COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/memory_tracer/compile.sh ${PINTOOL_PATH}) +endif() + include(GNUInstallDirs) install(FILES memstats.hh DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") @@ -88,7 +101,6 @@ export(EXPORT memstats-targets NAMESPACE MemStats:: ) - if(memstats_IS_TOP_LEVEL) add_executable(example_01 example_01.cc) target_link_libraries(example_01 PUBLIC MemStats::MemStats) @@ -101,4 +113,13 @@ if(memstats_IS_TOP_LEVEL) target_link_libraries(example_03 PUBLIC MemStats::MemStats) target_compile_features(example_03 PUBLIC cxx_std_11) endif() -endif() + + add_executable(example_04 example_04.cc) + target_link_libraries(example_04 PUBLIC MemStats::MemStats) + + add_executable(example_05 example_05.cc) + target_link_libraries(example_05 PUBLIC MemStats::MemStats) + + add_executable(example_06 example_06.cc) + target_link_libraries(example_05 PUBLIC MemStats::MemStats) +endif() \ No newline at end of file diff --git a/README.md b/README.md index a6fb804..f4adf06 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ _**Note**: This library only instruments the C++ operators `new` and `delete`, m * Thread Safe * Low overhead when disabled * Portable: Compatible with GCC, Clang, and MVSC with C++11 support +* Memory tracer using Intel PIN for x86 architectures to detect dynamic allocation of arrays containing arrays and memory that was allocated but never used ## Environmental options @@ -27,6 +28,13 @@ _**Note**: This library only instruments the C++ operators `new` and `delete`, m | `memstats_report(name)` | Reports statistics on `new` calls since last report. Not thread-safe. | | `memstats_[enable\|disable]_thread_instrumentation()` | Enables/disables instrumentation on the calling thread. Thread-safe. | +To enable or disable the memory tracer when using it, one just needs to define the following dummy funcions and call them to enable/disable: + +```c++ +void __attribute__((optimize("O0"))) disable_memory_tracer(void) {} + +void __attribute__((optimize("O0"))) enable_memory_tracer(void) {} +``` ## CMake @@ -49,6 +57,12 @@ add_executable(example_01 example_01.cc) target_link_libraries(example_01 PUBLIC MemStats::MemStats) ``` +When calling `cmake ...` using the options `-DUSE_MEMORY_TRACER=ON -DPINTOOL_PATH=/path/to/intelpin`, the memory tracer will be built. + +To execute a program with the memory tracer, one can use the following command: + +/path/to/intelpin/pin -t /path/to/memorytracer/memorytracer.so -- /path/to/program + ## Motivation In a world of increasing abstractions, it's increasibly hard to reason about what calls of our program may have expensive logic. One of such expensive calls are memory allocations because they may end up on system calls or have internal syncronization mechanisims. diff --git a/example_04.cc b/example_04.cc new file mode 100644 index 0000000..3c123c3 --- /dev/null +++ b/example_04.cc @@ -0,0 +1,9 @@ +volatile void * do_not_optimize; + +int main() +{ + auto val = new int; + do_not_optimize = val; + + return 0; +} diff --git a/example_05.cc b/example_05.cc new file mode 100644 index 0000000..1ae54d3 --- /dev/null +++ b/example_05.cc @@ -0,0 +1,10 @@ +volatile void * do_not_optimize; + +int main() { + int* ptr = new int; + do_not_optimize = ptr; + delete ptr; + delete ptr; + + return 0; +} \ No newline at end of file diff --git a/example_06.cc b/example_06.cc new file mode 100644 index 0000000..b221280 --- /dev/null +++ b/example_06.cc @@ -0,0 +1,20 @@ +volatile void * do_not_optimize; + +int main() { + // Allocate array of arrays + int** ptr = new int*[5]; + do_not_optimize = ptr; + for (int i = 0; i < 5; i++) { + ptr[i] = new int[20]; + do_not_optimize = ptr[i]; + } + + ptr[1][0] = 42; + + for (int i = 0; i < 5; i++) { + delete[] ptr[i]; + } + delete[] ptr; + + return 0; +} \ No newline at end of file diff --git a/memory_tracer/compile.sh b/memory_tracer/compile.sh new file mode 100755 index 0000000..a3d0ed4 --- /dev/null +++ b/memory_tracer/compile.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Current directory +DIR=$(dirname "$(realpath $0)") +PINTOOL_PATH=$1 + +# Remove memorytracer.so if it exists +rm -f $DIR/memorytracer.so + +# Copy files to the pintool directory +cp -f $DIR/memorytracer.cc $PINTOOL_PATH/source/tools/ManualExamples/memorytracer.cpp +cp -f $DIR/memorytracer.hh $PINTOOL_PATH/source/tools/ManualExamples/memorytracer.hh + +# Compile the memory tracer +cd $PINTOOL_PATH/source/tools/ManualExamples || exit +make obj-intel64/memorytracer.so +cp -f obj-intel64/memorytracer.so $DIR/ + +# Remove copied files +rm -f obj-intel64/memorytracer.so +rm -f obj-intel64/memorytracer.o +rm -f memorytracer.cpp memorytracer.hh +cd $DIR || exit \ No newline at end of file diff --git a/memory_tracer/memorytracer.cc b/memory_tracer/memorytracer.cc new file mode 100644 index 0000000..733fd51 --- /dev/null +++ b/memory_tracer/memorytracer.cc @@ -0,0 +1,183 @@ +#include "memorytracer.hh" +#include "pin.H" +#include +#include +#include +#include +#include + +static bool memstats_memory_tracing = true; + +bool memstats_do_memory_tracing() { + return memstats_memory_tracing; +} + +template +T exchange(T &obj, U &&new_value) { + T old_value = std::move(obj); + obj = std::forward(new_value); + return old_value; +} + +bool memstats_enable_memory_tracer() { + return exchange(memstats_memory_tracing, true); +} + +bool memstats_disable_memory_tracer() { + return exchange(memstats_memory_tracing, false); +} + +std::vector MemoryTracer::operations; +std::vector MemoryTracer::mallocOperations; + +VOID malloc_before(ADDRINT size) { + if (!memstats_do_memory_tracing()) + return; + + // Record malloc operation with nullptr as address since the adress is not known at this point + MemoryTracer::mallocOperations.push_back({nullptr, size}); +} + +VOID malloc_after(ADDRINT ret) { + if (!memstats_do_memory_tracing()) + return; + + // Update the address of the last malloc operation + MemoryTracer::mallocOperations.back().address = reinterpret_cast(ret); +} + +VOID Image(IMG img, VOID *v) { + RTN disableRtn = RTN_FindByName(img, "disable_memory_tracer"); + if (RTN_Valid(disableRtn)) { + RTN_Open(disableRtn); + RTN_InsertCall(disableRtn, IPOINT_BEFORE, AFUNPTR(memstats_disable_memory_tracer), IARG_END); + RTN_Close(disableRtn); + } + + RTN enableRtn = RTN_FindByName(img, "enable_memory_tracer"); + if (RTN_Valid(enableRtn)) { + RTN_Open(enableRtn); + RTN_InsertCall(enableRtn, IPOINT_BEFORE, AFUNPTR(memstats_enable_memory_tracer), IARG_END); + RTN_Close(enableRtn); + } + + RTN mallocRtn = RTN_FindByName(img, "malloc"); + if (RTN_Valid(mallocRtn)) { + RTN_Open(mallocRtn); + RTN_InsertCall(mallocRtn, IPOINT_BEFORE, AFUNPTR(malloc_before), IARG_FUNCARG_ENTRYPOINT_VALUE, 0, IARG_END); + RTN_InsertCall(mallocRtn, IPOINT_AFTER, AFUNPTR(malloc_after), IARG_FUNCRET_EXITPOINT_VALUE, IARG_END); + RTN_Close(mallocRtn); + } +} + +int MemoryTracer::Init(int argc, char *argv[]) { + + if(PIN_Init(argc,argv)) + { + std::cout << "Error PIN_Init" << std::endl; + return 1; + } + + PIN_InitSymbols(); + IMG_AddInstrumentFunction(Image, 0); + INS_AddInstrumentFunction([](INS ins, VOID* v) { + if (INS_IsMemoryRead(ins)) { + INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)RecordMemoryRead, + IARG_INST_PTR, IARG_MEMORYREAD_EA, IARG_MEMORYREAD_SIZE, IARG_THREAD_ID, IARG_END); + } + if (INS_IsMemoryWrite(ins)) { + INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)RecordMemoryWrite, + IARG_INST_PTR, IARG_MEMORYWRITE_EA, IARG_MEMORYWRITE_SIZE, IARG_THREAD_ID, IARG_END); + } + }, nullptr); + PIN_AddFiniFunction(Finalize, 0); + PIN_StartProgram(); + + return 0; +} + +void detect_arrays_of_arrays(const std::vector& calls) { + for (size_t i = 0; i < calls.size(); ++i) { + const auto& base_call = calls[i]; + + // Check if it is a potential pointer array allocation (small size, multiple of 8) + if (base_call.size >= 8 && base_call.size % sizeof(void*) == 0) { + size_t n = base_call.size / sizeof(void*); + + std::vector candidates; + size_t consistent_size = 0; + + // Search for n subsequent allocations with similar size + for (size_t j = i + 1; j < calls.size(); ++j) { + const auto& sub_call = calls[j]; + + if (candidates.empty()) { + consistent_size = sub_call.size; // expected size + } + + if (sub_call.size == consistent_size) { + candidates.push_back(sub_call.address); + } else { + break; // if size is not consistent, stop searching + } + + if (candidates.size() >= n) { + break; // found enough candidates! + } + } + + if (candidates.size() >= n / 2) { // Toleranz für unvollständige Allokation + std::cout << "Possibly an array of arrays!\n"; + std::cout << "Pointer-Array at: " << base_call.address << " (" << base_call.size << " Bytes)\n"; + std::cout << "Arrays found (" << candidates.size() << " candidates with size " << consistent_size << "):\n"; + for (auto addr : candidates) { + std::cout << " -> " << addr << "\n"; + } + std::cout << "--------------------------------------\n"; + } + } + } +} + +void MemoryTracer::Finalize(INT32 code, VOID* v) { + detect_arrays_of_arrays(MemoryTracer::mallocOperations); + + // find allocations that were never used + std::map allocations; + for (const auto& op : MemoryTracer::mallocOperations) { + allocations[op.address] = op.size; + } + for (const auto& op : MemoryTracer::operations) { + for (auto it = op.address; it < op.address + op.size; it += 8) { + allocations.erase(reinterpret_cast(it)); + } + } + for (const auto& [address, size] : allocations) { + std::cout << "Allocation at " << address << " (" << size << " bytes) was never used" << std::endl; + } +} + +const std::vector& MemoryTracer::GetOperations() { + return operations; +} + +void MemoryTracer::RecordMemoryRead(void* ip, void* addr, uint32_t size, uint32_t tid) { + auto address = reinterpret_cast(addr); + if (!memstats_do_memory_tracing()) + return; + + operations.push_back({address, size, false, tid}); +} + +void MemoryTracer::RecordMemoryWrite(void* ip, void* addr, uint32_t size, uint32_t tid) { + auto address = reinterpret_cast(addr); + if (!memstats_do_memory_tracing()) + return; + + operations.push_back({address, size, true, tid}); +} + +int main(int argc, char *argv[]) { + MemoryTracer::Init(argc, argv); + return 0; +} \ No newline at end of file diff --git a/memory_tracer/memorytracer.hh b/memory_tracer/memorytracer.hh new file mode 100644 index 0000000..dc46b10 --- /dev/null +++ b/memory_tracer/memorytracer.hh @@ -0,0 +1,49 @@ +#ifndef MEMSTATS_MEMORYTRACER_HH +#define MEMSTATS_MEMORYTRACER_HH + +#include +#include + +struct MemoryOperation { + uintptr_t address; + size_t size; + bool isWrite; + uint32_t threadId; +}; + +struct MallocOperation { + void* address; + size_t size; +}; + +class MemoryTracer { +public: + static int Init(int argc, char *argv[]); + static void Finalize(INT32 code, VOID *v); + static const std::vector& GetOperations(); + static const std::vector& GetMallocOperations(); + + static std::vector mallocOperations; + +private: + static void RecordMemoryRead(void* ip, void* addr, uint32_t size, uint32_t tid); + static void RecordMemoryWrite(void* ip, void* addr, uint32_t size, uint32_t tid); + + static std::vector operations; +}; + +/** @brief Enable memory tracing for reads and writes of all memory blocks. + * @details Thread-local. Do not call during static- or dynamic-initialization phase. + * @return Whether memory tracing was enabled before to this call + */ +bool memstats_enable_memory_tracer(); + +/** @brief Disable memory tracing for reads and writes of all memory blocks. + * @details Thread-local. Do not call during static- or dynamic-initialization phase. + * @return Whether memory tracing was enabled before to this call + */ +bool memstats_disable_memory_tracer(); + +bool memstats_do_memory_tracing(); + +#endif //MEMSTATS_MEMORYTRACER_HH diff --git a/memstats.cc b/memstats.cc index b4cdd39..e6dc1e9 100644 --- a/memstats.cc +++ b/memstats.cc @@ -18,7 +18,9 @@ #include #if __has_include() + #include + #endif #if MEMSTAT_HAVE_STACKTRACE @@ -38,19 +40,20 @@ #include "memstats.hh" // all allocations within this library need to use malloc/free instad of new/delete -template -class MallocAllocator -{ +template +class MallocAllocator { public: using value_type = T; constexpr MallocAllocator() noexcept = default; - template - constexpr MallocAllocator(const MallocAllocator &) noexcept {} + + template + constexpr MallocAllocator(const MallocAllocator &) noexcept { + } + ~MallocAllocator() noexcept = default; - T *allocate(std::size_t n) - { + T *allocate(std::size_t n) { if (n > this->max_size()) throw std::bad_alloc(); @@ -60,26 +63,43 @@ class MallocAllocator return ret; } - void deallocate(T *p, std::size_t) - { + void deallocate(T *p, std::size_t) { std::free(p); } - std::size_t max_size() const noexcept - { + std::size_t max_size() const noexcept { return std::size_t(-1) / sizeof(T); } - friend bool operator==(const MallocAllocator&, const MallocAllocator&){ + friend bool operator==(const MallocAllocator &, const MallocAllocator &) { return true; } - friend bool operator!=(const MallocAllocator&, const MallocAllocator&){ + + friend bool operator!=(const MallocAllocator &, const MallocAllocator &) { return false; } }; -struct MemStatsInfo -{ +#if MEMSTATS_USE_MEMORY_TRACER +void __attribute__((optimize("O0"))) disable_memory_tracer(void) { +} + +void __attribute__((optimize("O0"))) enable_memory_tracer(void) { +} + +class MemoryTracerGuard { +public: + MemoryTracerGuard() { + disable_memory_tracer(); + } + + ~MemoryTracerGuard() { + enable_memory_tracer(); + } +}; +#endif + +struct MemStatsInfo { const void *ptr = nullptr; std::size_t size = 0; std::chrono::high_resolution_clock::time_point time = {}; @@ -91,15 +111,18 @@ struct MemStatsInfo static void record(void *ptr, std::size_t sz = 0); }; -bool init_memstats_instrumentation_thread() -{ - if (char *ptr = std::getenv("MEMSTATS_THREAD_INSTRUMENTATION_INIT")) - { +bool init_memstats_instrumentation_thread() { +#if MEMSTATS_USE_MEMORY_TRACER + const MemoryTracerGuard guard; +#endif + + if (char *ptr = std::getenv("MEMSTATS_THREAD_INSTRUMENTATION_INIT")) { if (std::strcmp(ptr, "true") == 0 or std::strcmp(ptr, "1") == 0) return true; if (std::strcmp(ptr, "false") == 0 or std::strcmp(ptr, "0") == 0) return false; - std::cerr << "Option 'MEMSTATS_THREAD_INSTRUMENTATION_INIT=" << ptr << "' not known. Fallback on default 'false'\n"; + std::cerr << "Option 'MEMSTATS_THREAD_INSTRUMENTATION_INIT=" << ptr + << "' not known. Fallback on default 'false'\n"; } return false; } @@ -127,18 +150,24 @@ static std::recursive_mutex memstats_lock = {}; #if __cpp_lib_constexpr_vector >= 201907L MEMSTATS_CONSTINIT #endif -static std::vector> memstats_events = {}; +static std::vector > memstats_events = {}; #endif +std::mutex memstats_events_mutex; + // Zero- and dynamic-initialization of a thread-local variable does not necessarily happen on any order related to the global ones static thread_local bool memstats_instrumentation_thread = init_memstats_instrumentation_thread(); // guard thread-local variable to instrument further delets at exit -bool init_memstats_instrumentation_thread_guard() -{ - std::atexit([]{ memstats_instrumentation_thread = false; }); +bool init_memstats_instrumentation_thread_guard() { +#if MEMSTATS_USE_MEMORY_TRACER + const MemoryTracerGuard guard; +#endif + + std::atexit([] { memstats_instrumentation_thread = false; }); return true; } + const static thread_local bool memstats_instrumentation_thread_guard = init_memstats_instrumentation_thread_guard(); // We need to make absolutely sure this is constinit so that 'memstats_instrumentation_global' is const-initialized, @@ -149,18 +178,21 @@ MEMSTATS_CONSTINIT static std::atomic memstats_instrumentation_global{fals #error "MemStats needs a conforming C++ standard library where 'std::atomic' can be const-initialized, i.e. its constructor is 'constexpr'!" #endif -bool init_memstats_instrumentation_guard() -{ +bool init_memstats_instrumentation_guard() { +#if MEMSTATS_USE_MEMORY_TRACER + const MemoryTracerGuard guard; +#endif + bool instrument = false; // Note this variable is const-initialized to false. Here we change it to true and syncronize other threads during dynamic initialization - if (char *ptr = std::getenv("MEMSTATS_ENABLE_INSTRUMENTATION")) - { + if (char *ptr = std::getenv("MEMSTATS_ENABLE_INSTRUMENTATION")) { if (std::strcmp(ptr, "true") == 0 or std::strcmp(ptr, "1") == 0) instrument = true; else if (std::strcmp(ptr, "false") == 0 or std::strcmp(ptr, "0") == 0) instrument = false; else - std::cerr << "Option 'MEMSTATS_ENABLE_INSTRUMENTATION=" << ptr << "' not known. Fallback on default 'false'\n"; + std::cerr << "Option 'MEMSTATS_ENABLE_INSTRUMENTATION=" << ptr + << "' not known. Fallback on default 'false'\n"; } memstats_instrumentation_global.store(instrument, std::memory_order_release); return instrument; @@ -173,26 +205,30 @@ bool init_memstats_instrumentation_guard() // dynamic-initialized in the correct order by delaying its initialization by a non-constexpr function. static bool memstats_instrumentation_guard = init_memstats_instrumentation_guard(); -bool init_memstats_at_exit() -{ +bool init_memstats_at_exit() { +#if MEMSTATS_USE_MEMORY_TRACER + const MemoryTracerGuard guard; +#endif + static std::once_flag report_flag; std::call_once(report_flag, - []{ std::atexit([]{ - memstats_instrumentation_global.store(false, std::memory_order_release); - bool do_report_at_exit = true; - if (char *ptr = std::getenv("MEMSTATS_REPORT_AT_EXIT")) - { - if (std::strcmp(ptr, "true") == 0 or std::strcmp(ptr, "1") == 0) - do_report_at_exit = true; - else if (std::strcmp(ptr, "false") == 0 or std::strcmp(ptr, "0") == 0) - do_report_at_exit = false; - else - std::cerr << "Option 'MEMSTATS_REPORT_AT_EXIT=" << ptr << "' not known. Fallback on default 'true'\n"; - } - if (do_report_at_exit) - memstats_report("default"); - }); - }); + [] { + std::atexit([] { + memstats_instrumentation_global.store(false, std::memory_order_release); + bool do_report_at_exit = true; + if (char *ptr = std::getenv("MEMSTATS_REPORT_AT_EXIT")) { + if (std::strcmp(ptr, "true") == 0 or std::strcmp(ptr, "1") == 0) + do_report_at_exit = true; + else if (std::strcmp(ptr, "false") == 0 or std::strcmp(ptr, "0") == 0) + do_report_at_exit = false; + else + std::cerr << "Option 'MEMSTATS_REPORT_AT_EXIT=" << ptr + << "' not known. Fallback on default 'true'\n"; + } + if (do_report_at_exit) + memstats_report("default"); + }); + }); return true; } @@ -215,23 +251,29 @@ static const bool memstats_at_exit_guard = init_memstats_at_exit(); */ // bin representation of percentage from 0% to 100% -static const std::array memstats_str_precentage_punctuation{" ", ".", ":", "!"}; -static const std::array memstats_str_precentage_circle{" ", ".", "o", "O"}; -static const std::array memstats_str_precentage_shadow{" ", "░", "▒", "▓", "█"}; -static const std::array memstats_str_precentage_wire{" ", "-", "~", "=", "#"}; -static const std::array memstats_str_precentage_box{" ", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"}; -static const std::array memstats_str_precentage_number{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}; - -std::pair memstats_str_hist_representation() -{ - if (const char *ptr = std::getenv("MEMSTATS_HISTOGRAM_REPRESENTATION")) - { +static const std::array memstats_str_precentage_punctuation{" ", ".", ":", "!"}; +static const std::array memstats_str_precentage_circle{" ", ".", "o", "O"}; +static const std::array memstats_str_precentage_shadow{" ", "░", "▒", "▓", "█"}; +static const std::array memstats_str_precentage_wire{" ", "-", "~", "=", "#"}; +static const std::array memstats_str_precentage_box{" ", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"}; +static const std::array memstats_str_precentage_number{ + "0", "1", "2", "3", "4", "5", "6", "7", "8", + "9" +}; + +std::pair memstats_str_hist_representation() { +#if MEMSTATS_USE_MEMORY_TRACER + const MemoryTracerGuard guard; +#endif + + if (const char *ptr = std::getenv("MEMSTATS_HISTOGRAM_REPRESENTATION")) { if (std::strcmp(ptr, "box") == 0) return std::make_pair(memstats_str_precentage_box.data(), memstats_str_precentage_box.size()); if (std::strcmp(ptr, "number") == 0) return std::make_pair(memstats_str_precentage_number.data(), memstats_str_precentage_number.size()); if (std::strcmp(ptr, "punctuation") == 0) - return std::make_pair(memstats_str_precentage_punctuation.data(), memstats_str_precentage_punctuation.size()); + return std::make_pair(memstats_str_precentage_punctuation.data(), + memstats_str_precentage_punctuation.size()); if (std::strcmp(ptr, "shadow") == 0) return std::make_pair(memstats_str_precentage_shadow.data(), memstats_str_precentage_shadow.size()); if (std::strcmp(ptr, "wire") == 0) @@ -243,24 +285,26 @@ std::pair memstats_str_hist_representation() return std::make_pair(memstats_str_precentage_box.data(), memstats_str_precentage_box.size()); } -unsigned short memstats_bins() -{ - if (const char *ptr = std::getenv("MEMSTATS_BINS")) - { - try - { +unsigned short memstats_bins() { +#if MEMSTATS_USE_MEMORY_TRACER + const MemoryTracerGuard guard; +#endif + + if (const char *ptr = std::getenv("MEMSTATS_BINS")) { + try { return std::stoi(ptr); - } - catch (...) - { + } catch (...) { std::cerr << "Option 'MEMSTATS_BINS=" << ptr << "' not known. Fallback on default '15'\n"; } } return 15; } -void MemStatsInfo::record(void *ptr, std::size_t sz) -{ +void MemStatsInfo::record(void *ptr, std::size_t sz) { +#if MEMSTATS_USE_MEMORY_TRACER + const MemoryTracerGuard guard; +#endif + auto time = std::chrono::high_resolution_clock::now(); MemStatsInfo info; info.ptr = ptr; @@ -276,13 +320,17 @@ void MemStatsInfo::record(void *ptr, std::size_t sz) memstats_events.emplace_back(std::move(info)); } -template -using unordered_map = std::unordered_map, std::equal_to, MallocAllocator>>; -using string = std::basic_string, MallocAllocator>; -using stringstream = std::basic_stringstream, MallocAllocator>; +template +using unordered_map = std::unordered_map, std::equal_to, MallocAllocator > >; +using string = std::basic_string, MallocAllocator >; +using stringstream = std::basic_stringstream, MallocAllocator >; + +void print_legend() { +#if MEMSTATS_USE_MEMORY_TRACER + const MemoryTracerGuard guard; +#endif -void print_legend() -{ std::cout << "\nMemStats Legend:\n\n"; std::cout << " [{hist}]{max} | {accum}({count}) | {pos}\n\n"; std::cout << "• hist: Distribution of number of 'new' allocations for a given number of bytes\n"; @@ -295,33 +343,66 @@ void print_legend() string buffer; double per_width = 100. / str_precentage.second; for (std::size_t i = 0; i != str_precentage.second; ++i) - std::cout << "• \'" << str_precentage.first[i] << "\' -> [" << std::fixed + std::cout << "• \'" << str_precentage.first[i] << "\' -> [" << std::fixed << std::setw(4) << std::setprecision(1) << i * per_width << "%, " << std::setw(5) << (i + 1) * per_width << '%' << (i + 1 == str_precentage.second ? ']' : ')') << std::endl; } -void memstats_report(const char * report_name) -{ +void report_memory_leaks() { + std::cout << "\nMemory leaks:\n"; + + // Report allocations without deallocations + for (int i = 0; i < memstats_events.size(); ++i) { + // Skip deallocations (only allocations can have size > 0) + if (memstats_events[i].size == 0) continue; + bool freed = false; + for (int j = i + 1; j < memstats_events.size(); ++j) { + if (memstats_events[i].ptr == memstats_events[j].ptr && memstats_events[j].size == 0 && + memstats_events[i].thread == memstats_events[j].thread) { + freed = true; + } + } + + if (!freed) { + std::cout << "Pointer " << memstats_events[i].ptr << " was never freed in Thread " + << memstats_events[i].thread << "." << std::endl; +#if MEMSTAT_HAVE_STACKTRACE + std::cout << "Current stacktrace:\n" << memstats_events[i].stacktrace << std::endl; +#endif + } + } +} + +void memstats_report(const char *report_name) { +#if MEMSTATS_USE_MEMORY_TRACER + const MemoryTracerGuard guard; +#endif + auto lock = std::unique_lock{memstats_lock}; if (memstats_events.size() == 0) return; + std::cout << "\n------------------- MemStats " << report_name << " -------------------\n"; - struct Stats - { + struct Stats { std::size_t count{0}, size{0}, max_size{0}; unordered_map size_freq; }; Stats global_stats; unordered_map thread_stats; + unordered_map > > ptr_collec; + struct PtrStats { + int times_freed; + const void *ptr; + }; + std::vector ptr_stats; #if MEMSTAT_HAVE_STACKTRACE unordered_map>, Stats> stacktrace_stats; unordered_map stacktrace_entry_stats; #endif - for (const MemStatsInfo &info : memstats_events) - { - auto register_stats = [&](Stats &stats) - { + + for (const MemStatsInfo &info: memstats_events) { + auto register_stats = [&](Stats &stats) { if (info.size) ++stats.count; stats.size += info.size; @@ -333,18 +414,26 @@ void memstats_report(const char * report_name) register_stats(global_stats); register_stats(thread_stats[info.thread]); + if (ptr_collec[info.ptr][info.thread].second && info.size > 0) { + PtrStats stats = {ptr_collec[info.ptr][info.thread].first, info.ptr}; + ptr_stats.push_back(stats); + ptr_collec[info.ptr][info.thread].first = 0; + ptr_collec[info.ptr][info.thread].second = false; + } else if (info.size == 0) { + ++ptr_collec[info.ptr][info.thread].first; + ptr_collec[info.ptr][info.thread].second = true; + } + + #if MEMSTAT_HAVE_STACKTRACE register_stats(stacktrace_stats[info.stacktrace]); for (auto entry : info.stacktrace) register_stats(stacktrace_entry_stats[entry]); #endif } - // clean up vector - memstats_events.clear(); static const std::array metric_prefix{' ', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q'}; - auto bytes_to_string = [&](std::size_t bytes) - { + auto bytes_to_string = [&](std::size_t bytes) { stringstream stream; short base = std::floor(std::log2(bytes) / 10); if (base > metric_prefix.size()) @@ -353,8 +442,7 @@ void memstats_report(const char * report_name) return stream.str(); }; - auto int_to_string = [&](std::size_t val) - { + auto int_to_string = [&](std::size_t val) { stringstream stream; short base = std::floor(std::log10(val) / 3); if (base > metric_prefix.size()) @@ -364,12 +452,10 @@ void memstats_report(const char * report_name) }; const auto str_precentage = memstats_str_hist_representation(); const auto bins = memstats_bins(); - auto format_histogram = [&](const Stats &stats) - { - std::vector> hist(bins, 0); + auto format_histogram = [&](const Stats &stats) { + std::vector > hist(bins, 0); std::size_t max_size = 0; - for (const auto &frec : stats.size_freq) - { + for (const auto &frec: stats.size_freq) { std::size_t size = frec.first, count = frec.second; assert(size <= stats.max_size); auto bin = (bins * (size - 1)) / (stats.max_size); @@ -377,28 +463,28 @@ void memstats_report(const char * report_name) } stringstream stream; stream << "["; - for (auto size : hist) { - const std::size_t bin_entry = - (size * str_precentage.second) / max_size; - // maximum value (size==max_size) will be out of range so we need to guard agains that - stream << str_precentage.first[std::min(bin_entry, str_precentage.second - 1)]; + for (auto size: hist) { + const std::size_t bin_entry = + (size * str_precentage.second) / max_size; + // maximum value (size==max_size) will be out of range so we need to guard agains that + stream << str_precentage.first[std::min(bin_entry, str_precentage.second - 1)]; } - stream << "]" << std::left<< std::setw(6) << bytes_to_string(stats.max_size); + stream << "]" << std::left << std::setw(6) << bytes_to_string(stats.max_size); return stream.str(); }; std::cout << format_histogram(global_stats) << " | " << std::right - << std::setw(6) << bytes_to_string(global_stats.size) << '(' - << std::left << std::setw(5) << int_to_string(global_stats.count) - << ") | Total\n"; - - for (const auto &pair : thread_stats) - if (pair.second.size) { - std::cout << format_histogram(pair.second) << " | " << std::right - << std::setw(6) << bytes_to_string(pair.second.size) << '(' - << std::left << std::setw(5) << int_to_string(pair.second.count) - << ") | Thread " << pair.first << std::endl; - } + << std::setw(6) << bytes_to_string(global_stats.size) << '(' + << std::left << std::setw(5) << int_to_string(global_stats.count) + << ") | Total\n"; + + for (const auto &pair: thread_stats) + if (pair.second.size) { + std::cout << format_histogram(pair.second) << " | " << std::right + << std::setw(6) << bytes_to_string(pair.second.size) << '(' + << std::left << std::setw(5) << int_to_string(pair.second.count) + << ") | Thread " << pair.first << std::endl; + } #if MEMSTAT_HAVE_STACKTRACE for (auto [stacktrace_entry, stats] : stacktrace_entry_stats) @@ -412,43 +498,73 @@ void memstats_report(const char * report_name) } } #endif + + report_memory_leaks(); + + std::cout << "\nDouble freed pointers:\n"; + + // Report double deallocations + for (const auto &entry: ptr_stats) { + if (entry.times_freed > 1) + std::cout << "Pointer " << entry.ptr << " was freed " << entry.times_freed << " times." << std::endl; + } + + // clean up vector + memstats_events.clear(); + // avoid printing legend several times, so call once at exit static std::once_flag legend_flag; - std::call_once(legend_flag, []() - { std::atexit(print_legend); }); + std::call_once(legend_flag, []() { std::atexit(print_legend); }); } template -T exchange(T& obj, U&& new_value) -{ +T exchange(T &obj, U &&new_value) { +#if MEMSTATS_USE_MEMORY_TRACER + const MemoryTracerGuard guard; +#endif + T old_value = std::move(obj); obj = std::forward(new_value); return old_value; } -bool memstats_enable_thread_instrumentation() -{ +bool memstats_enable_thread_instrumentation() { +#if MEMSTATS_USE_MEMORY_TRACER + const MemoryTracerGuard guard; +#endif + return exchange(memstats_instrumentation_thread, true); } -bool memstats_disable_thread_instrumentation() -{ +bool memstats_disable_thread_instrumentation() { +#if MEMSTATS_USE_MEMORY_TRACER + const MemoryTracerGuard guard; +#endif + return exchange(memstats_instrumentation_thread, false); } -bool memstats_do_instrument() -{ +bool memstats_do_instrument() { +#if MEMSTATS_USE_MEMORY_TRACER + const MemoryTracerGuard guard; +#endif + return memstats_instrumentation_thread and memstats_instrumentation_global.load(std::memory_order_acquire); } // instrumentation of new -void *operator new(std::size_t sz) -{ +void *operator new(std::size_t sz) { +#if MEMSTATS_USE_MEMORY_TRACER + const MemoryTracerGuard guard; +#endif + + // prevent multiple thread from allocating memory at the same time s.t. events are in order + //std::lock_guard lock(memstats_events_mutex); + if (sz == 0) sz = 1; void *ptr; - while ((ptr = std::malloc(sz)) == nullptr) - { + while ((ptr = std::malloc(sz)) == nullptr) { std::new_handler handler = std::get_new_handler(); if (handler) handler(); @@ -457,38 +573,45 @@ void *operator new(std::size_t sz) } if (memstats_do_instrument()) MemStatsInfo::record(ptr, sz); + return ptr; } // instrumentation of new -void *operator new(std::size_t sz, std::nothrow_t) noexcept -{ - try - { +void *operator new(std::size_t sz, std::nothrow_t) noexcept { +#if MEMSTATS_USE_MEMORY_TRACER + const MemoryTracerGuard guard; +#endif + + try { return ::operator new(sz); - } - catch (...) - { + } catch (...) { } return nullptr; } // instrumentation of delete -void operator delete(void *ptr) noexcept -{ +void operator delete(void *ptr) noexcept { +#if MEMSTATS_USE_MEMORY_TRACER + const MemoryTracerGuard guard; +#endif + + // prevent multiple thread from deallocating memory at the same time s.t. events are in order + //std::lock_guard lock(memstats_events_mutex); + if (memstats_do_instrument()) MemStatsInfo::record(ptr); std::free(ptr); } // instrumentation of delete -void operator delete(void *ptr, std::nothrow_t) noexcept -{ - try - { +void operator delete(void *ptr, std::nothrow_t) noexcept { +#if MEMSTATS_USE_MEMORY_TRACER + const MemoryTracerGuard guard; +#endif + + try { return ::operator delete(ptr); - } - catch (...) - { + } catch (...) { } }