2024-05-09
编程
00
请注意,本文编写于 130 天前,最后修改于 130 天前,其中某些信息可能已经过时。

目录

基础方法
进阶方法
针对多线程的优化

默认情况下,C++的newdelete提供了基本的内存分配和释放功能,但缺乏监控和统计内存使用的能力,有时我们会忘记使用delete造成内存泄漏。本文将介绍一些方法,使得你可以实时的监控newdelete分配和释放内存的过程,并且使你可以在newdelete中加入额外的功能(如日志打印等),增强程序的健壮性和可维护性。

基础方法

cpp
void *operator new(size_t size) { std::cout << "Allocate " << size << " bytes.\n"; return malloc(size); } void operator delete(void *memory, size_t size) { std::cout << "Free " << size << " bytes.\n"; free(memory); }

cpp
void operator delete(void* memory, size_t size);

是在C++14中引入的delete版本,如果编译器不支持C++14会发生编译错误

通过对newdelete进行重载,在其中加入打印语句,使得你可以实时跟踪当下的内存分配和释放。不过这个方案只能看临时的状态,不能看全局的内存分配和释放情况。此外需要注意的时,使用这个技巧会降低程序的性能。不过会使程序更加便于调试,你可以在newdelete中打断点,来跟踪内存分配的情况。

进阶方法

cpp
#include <iostream> #include <memory> #include <new> // for std::bad_alloc struct AllocationMetrics { uint64_t TotalAllocated = 0; uint64_t TotalFreed = 0; uint64_t CurrentUsage() { return TotalAllocated - TotalFreed; } }; static AllocationMetrics s_AllocationMetrics; // 使用更紧凑的结构以减少每次分配的额外开销 struct AllocationHeader { size_t size; }; // 提供宏定义以允许在生产环境中开启或关闭内存追踪 #ifdef ENABLE_MEMORY_TRACKING void* operator new(size_t size) { size_t totalSize = size + sizeof(AllocationHeader); s_AllocationMetrics.TotalAllocated += size; void* block = malloc(totalSize); if (!block) throw std::bad_alloc(); ((AllocationHeader*)block)->size = size; return (char*)block + sizeof(AllocationHeader); } void operator delete(void* memory) noexcept { char* block = (char*)memory - sizeof(AllocationHeader); size_t size = ((AllocationHeader*)block)->size; s_AllocationMetrics.TotalFreed += size; free(block); } #else void* operator new(size_t size) { return malloc(size); } void operator delete(void* memory) noexcept { free(memory); } #endif struct Object { int x, y, z; }; static void PrintMemoryUsage() { std::cout << "Memory Usage: " << s_AllocationMetrics.CurrentUsage() << " bytes\n"; } int main() { PrintMemoryUsage(); // 如果支持C++14 // { // std::unique_ptr<Object> obj = std::make_unique<Object>(); // PrintMemoryUsage(); // } // PrintMemoryUsage(); Object* obj = new Object; PrintMemoryUsage(); delete obj; PrintMemoryUsage(); std::string string = "aaab aaab aaab aaab aaab "; PrintMemoryUsage(); return 0; }

在这段代码中,我删除了不带size参数的delete操作符,使得这段代码可以在C++11的编译环境下运行。为了精确跟踪每次内存释放的大小,我采用了使用内存分配头的方法。在每次分配内存时,我们在分配的内存块的前面附加一个小的头部来存储对象的大小,也就是代码中的AllocationHeader。这样,当调用 delete 时,我们就可以通过读取这个头部来找出即将被释放的内存块的大小。

同时还引入了宏ENABLE_MEMORY_TRACKING,你根据需求在编译时选择是否开启内存追踪。

运行截图:

image.png

针对多线程的优化

cpp
//... #include <mutex> #include <atomic> //... struct AllocationMetrics { std::atomic<uint64_t> TotalAllocated{0}; std::atomic<uint64_t> TotalFreed{0}; uint64_t CurrentUsage() { return TotalAllocated.load() - TotalFreed.load(); } }; static AllocationMetrics s_AllocationMetrics; static std::mutex s_Mutex; struct AllocationHeader { size_t size; }; void* operator new(size_t size) { std::lock_guard<std::mutex> lock(s_Mutex); size_t totalSize = size + sizeof(AllocationHeader); s_AllocationMetrics.TotalAllocated += size; void* block = malloc(totalSize); if (!block) throw std::bad_alloc(); ((AllocationHeader*)block)->size = size; return (char*)block + sizeof(AllocationHeader); } void operator delete(void* memory) noexcept { std::lock_guard<std::mutex> lock(s_Mutex); char* block = (char*)memory - sizeof(AllocationHeader); size_t size = ((AllocationHeader*)block)->size; s_AllocationMetrics.TotalFreed += size; free(block); }

在多线程环境中,当多个线程试图同时访问和修改相同的数据时,就会产生所谓的竞态条件。这可能导致数据损坏或程序行为不确定。为了解决这一问题,我在代码中做了以下具体改进:

  1. 使用 std::mutex 保护全局变量
  • 在每次使用 operator newoperator delete 来分配和释放内存时,我都通过一个互斥锁 (std::mutex) 来确保对全局状态 s_AllocationMetrics 的修改是互斥的。这意味着在任一时刻,只有一个线程可以执行这些修改操作,从而防止了多个线程同时修改这些数据。
  • 使用 std::lock_guard 自动管理互斥锁的锁定和解锁,以保证即使在异常发生时也能正确释放锁,避免死锁。
  1. 将全局变量的成员改为原子类型
  • TotalAllocatedTotalFreed 被改为 std::atomic<uint64_t> 类型。这保证了这些成员的每次读写都是原子操作,即不可中断的单元操作,从而不需要额外的同步机制就能在多线程中安全地访问。
  • 原子类型的使用减少了对互斥锁的依赖,提高了代码的执行效率,特别是在只进行简单增减操作时。

通过这些改进,代码现在可以安全地在多线程环境下运行,而不必担心数据竞争或其他线程安全问题。这样的处理确保了即使在高并发的情况下,内存分配和释放的度量也是准确和一致的。

本文作者:Rowlet

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!