1
\$\begingroup\$

I've written a memory tracker for use in my game engine. I based it on this slide in a talk by Nicolai Josuttis.

One problem I'm having is that, during reporting, the implicit string conversion and output stream operator functions allocate. This results in calls to new. Since I've overloaded new the tracker tries to track the new allocations and ends up invalidating the results.

Is there a way to output the results without allocating?

Memory.hpp

#pragma once
//https://www.youtube.com/watch?v=e2ZQyYr0Oi0
//C++17 - The Best Features - Nicolai Josuttis [ACCU 2018]

#include "Engine/Core/BuildConfig.hpp"
#include "Engine/Core/ErrorWarningAssert.hpp"

//#include "Engine/Profiling/StackTrace.hpp"

#include <new>
#include <sstream>
#include <iostream>
#include <ostream>

class Memory {
public:

    struct status_t {
        std::size_t leaked_objs  = 0;
        std::size_t leaked_bytes = 0;
        operator bool() const noexcept {
            return leaked_objs || leaked_bytes;
        }
        operator std::string() const noexcept {
#ifdef TRACK_MEMORY
            std::ostringstream ss;
            std::string s = ss.str();
            ss << "Leaked objects: " << leaked_objs << " for " << leaked_bytes << " bytes.\n";
            return s;
#else
            return {};
#endif
        }
        friend std::ostream& operator<<(std::ostream& os, [[maybe_unused]]const status_t& s) noexcept {
#ifdef TRACK_MEMORY
            os << "Leaked objects: " << s.leaked_objs << " for " << s.leaked_bytes << " bytes.\n";
#endif
            return os;
        }
    };
    struct status_frame_t {
        std::size_t frame_id = 0;
        std::size_t leaked_objs = 0;
        std::size_t leaked_bytes = 0;
        operator bool() const noexcept {
            return leaked_objs || leaked_bytes;
        }
        operator std::string() const noexcept {
#ifdef TRACK_MEMORY
            std::ostringstream ss;
            ss << "Frame " << frame_id << ": Leaked objects: " << leaked_objs << " for " << leaked_bytes << " bytes.\n";
            std::string s = ss.str();
            return s;
#else
            return {};
#endif
        }
        friend std::ostream& operator<<(std::ostream& os, [[maybe_unused]]const status_frame_t& s) noexcept {
#ifdef TRACK_MEMORY
            os << "Frame " << s.frame_id << ": Leaked objects: " << s.leaked_objs << " for " << s.leaked_bytes << " bytes.\n";
#endif
            return os;
        }
    };

    [[nodiscard]] static void* allocate(std::size_t n) noexcept {
        if(is_enabled()) {
            ++frameCount;
            frameSize += n;
            ++allocCount;
            allocSize += n;
            if(maxSize < allocSize) {
                maxSize = allocSize;
            }
            if(maxCount < allocCount) {
                maxCount = allocCount;
            }
        }
        return std::malloc(n);
    }

    static void deallocate(void* ptr, std::size_t size) noexcept {
        if(is_enabled()) {
            ++framefreeCount;
            framefreeSize += size;
            ++freeCount;
            freeSize += size;
        }
        std::free(ptr);
    }

    static void enable([[maybe_unused]]bool e) noexcept {
#ifdef TRACK_MEMORY
        _active = e;
#endif
    }

    static bool is_enabled() noexcept {
#ifdef TRACK_MEMORY
        return _active;
#else
        return false;
#endif
    }

    static void trace([[maybe_unused]]bool doTrace) noexcept {
#ifdef TRACK_MEMORY
        _trace = doTrace;
#endif
    }

    static void tick() noexcept {
#ifdef TRACK_MEMORY
        if(auto f = Memory::frame_status()) {
            std::cout << f << '\n';
        }
        ++frameCounter;
        resetframecounters();
#endif
    }

    static void resetframecounters() noexcept {
#ifdef TRACK_MEMORY
        frameSize = 0;
        frameCount = 0;
        framefreeCount = 0;
        framefreeSize = 0;
#endif
    }

    static status_t status() noexcept {
        return { allocCount - freeCount, allocSize - freeSize };
    }

    static status_frame_t frame_status() noexcept {
        return { frameCounter, frameCount - framefreeCount, frameSize - framefreeSize };
    }

    inline static std::size_t maxSize = 0;
    inline static std::size_t maxCount = 0;
    inline static std::size_t allocSize = 0;
    inline static std::size_t allocCount = 0;
    inline static std::size_t frameSize = 0;
    inline static std::size_t frameCount = 0;
    inline static std::size_t frameCounter = 0;
    inline static std::size_t freeCount = 0;
    inline static std::size_t freeSize = 0;
    inline static std::size_t framefreeCount = 0;
    inline static std::size_t framefreeSize = 0;
protected:
private:
    inline static bool _active = false;
    inline static bool _trace = false;
};

#ifdef TRACK_MEMORY

void* operator new(std::size_t size);
void* operator new[](std::size_t size);
void operator delete(void* ptr, std::size_t size) noexcept;
void operator delete[](void* ptr, std::size_t size) noexcept;

#endif

Memory.cpp

#include "Engine/Profiling/Memory.hpp"

#ifdef TRACK_MEMORY

void* operator new(std::size_t size) {
    return Memory::allocate(size);
}

void* operator new[](std::size_t size) {
    return Memory::allocate(size);
}

void operator delete(void* ptr, std::size_t size) noexcept {
    Memory::deallocate(ptr, size);
}

void operator delete[](void* ptr, std::size_t size) noexcept {
    Memory::deallocate(ptr, size);
}

#endif
\$\endgroup\$
  • 1
    \$\begingroup\$ You could always use C strings with a fixed buffer size rather than std::string. Less elegant, but side-steps the allocation issue. \$\endgroup\$ – user1118321 Aug 25 at 4:54
  • \$\begingroup\$ @user1118321 looks like you have a candidate for an answer there. \$\endgroup\$ – Vaillancourt Aug 25 at 12:30
1
\$\begingroup\$

I used a combination of the new-fangled (and non-allocating!) std::to_chars and std::string_view with a static std::array for a buffer. It works okay, but having to deal with the buffer being overwritten is a little annoying. The use of std::strlen is unavoidable but shouldn't be an issue due to the short strings.

I also refactored the code to not use the std::string implicit operators. The use case didn't need them anyway since the results were being output into an in-memory stream.

Updated status_t::operator<<:

        friend std::ostream& operator<<(std::ostream& os, [[maybe_unused]]const status_t& s) noexcept {
#ifdef TRACK_MEMORY
            static std::array<char, 1024> str{ "Leaked objects: %f for %f bytes.\n" };
            std::to_chars_result first_result;
            if (first_result = std::to_chars(str.data() + 16, str.data() + 20, static_cast<float>(s.leaked_objs));
                first_result.ec != std::errc()) {
                DebuggerPrintf("Error code %d: Memory profiler could not convert leaked objects value for printing", first_result.ec);
                return os;
            }
            //Put remainder of string back into buffer because to_chars overwrites.
            std::memcpy(first_result.ptr, " for %f bytes.\n", 16);
            std::to_chars_result second_result;
            if (second_result = std::to_chars(first_result.ptr + 5, first_result.ptr + 11, static_cast<float>(s.leaked_bytes));
                second_result.ec != std::errc()) {
                DebuggerPrintf("Error code %d: Memory profiler could not convert leaked bytes value for printing", second_result.ec);
                return os;
            }
            std::memcpy(second_result.ptr, " bytes.\n", 9);
            os << std::string_view(str.data(), std::strlen(str.data()));
#endif
            return os;
        }

Updated status_frame_t::operator<<:

        friend std::ostream& operator<<(std::ostream& os, [[maybe_unused]]const status_frame_t& s) noexcept {
#ifdef TRACK_MEMORY
            static std::array<char, 1024> str{ "Frame %f: Leaked objects: %f for %f bytes.\n" };
            std::to_chars_result frame_result;
            if (frame_result = std::to_chars(str.data() + 6, str.data() + 26, static_cast<float>(s.frame_id));
                frame_result.ec != std::errc()) {
                DebuggerPrintf("Error code %d: Memory profiler could not convert frame id value for printing", frame_result.ec);
                return os;
            }
            //Put remainder of string back into buffer because to_chars overwrites.
            std::memcpy(frame_result.ptr, ": Leaked objects: %f for %f bytes.\n", 36);
            std::to_chars_result objects_result;
            if (objects_result = std::to_chars(frame_result.ptr + 18, frame_result.ptr + 38, static_cast<float>(s.leaked_objs));
                objects_result.ec != std::errc()) {
                DebuggerPrintf("Error code %d: Memory profiler could not convert leaked objects value for printing", objects_result.ec);
                return os;
            }
            std::memcpy(objects_result.ptr, " for %f bytes.\n", 16);
            std::to_chars_result bytes_result;
            if (bytes_result = std::to_chars(objects_result.ptr + 5, objects_result.ptr + 25, static_cast<float>(s.leaked_bytes));
                bytes_result.ec != std::errc()) {
                DebuggerPrintf("Error code %d: Memory profiler could not convert leaked bytes value for printing", bytes_result.ec);
                return os;
            }
            std::memcpy(bytes_result.ptr, " bytes.\n", 9);
            os << std::string_view(str.data(), std::strlen(str.data()));
#endif
            return os;
        }
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.