C++高效使用new运算符

3

当使用new实例化一个类时,如果不删除内存,通过重用对象可以获得哪些好处?

new的过程是什么?是否会发生上下文切换?分配新内存时由谁执行分配?操作系统?


7
除非基于性能分析结果或设计需要(例如,内存池),否则应使用标准分配器以外的其他分配器。除此之外,实现中提供的分配器对于大多数情况来说已经足够好了。 - Khaled Alshaya
确实是个好问题。我曾经有一个案例,在分析过后,我为许多小型和恒定大小的对象切换到了池分配器。我认为只有在一次性分配多个页面(使用Linux上的mmap)时才会发生上下文切换,并且这些页面在分配器内部被重复使用。现在,如果您预先知道要分配的对象都具有相同的大小,则有不同的分配策略。标准库分配器针对通用情况进行了调整,而不是特定情况。我听说有些分配器为小、中、大型对象维护多个内存池。 - Alexandre C.
3个回答

5

您在这里提出了几个问题...

除了删除内存,基于对象的重用我们可以获得哪些好处?

这完全取决于您的应用程序。即使我知道应用程序是什么,您还未指定另一个细节 - 重用背后的策略是什么?但即使知道了这一点,很难预测或通用地回答。尝试一些方法并进行测量。

作为经验法则,我喜欢最小化最不必要的分配。虽然这主要是过早优化。只有在数千次调用中才会有所区别。

new的过程是什么?

完全依赖于实现。但分配器使用的一般策略是具有空闲列表,即已在进程中释放的块的列表。当空闲列表为空或包含不足的连续空闲空间时,它必须向内核请求内存,内核只能以固定页面大小的块分配内存。(x86上的4096)。分配器还必须决定何时切割、填充或合并块。多线程也可能对分配器施加压力,因为它们必须同步其空闲列表。

通常这是一个非常昂贵的操作。相对于您正在做的其他事情可能并不那么昂贵。但它不便宜。

是否发生上下文切换?
完全有可能。也可能不会。您的操作系统可以在任何时候获得中断或系统调用时进行上下文切换,因此...这可能发生在很多时候;我没有看到这与您的分配器之间有任何特殊关系。

新内存是由谁分配的?操作系统?
如果来自空闲列表,则无需涉及系统调用,因此无需操作系统的帮助。但如果空闲列表无法满足请求,则可能来自操作系统。此外,即使它来自空闲列表,您的内核也可能对该数据进行了页面处理,因此您可以在访问时遇到页错误,内核的分配器将启动。因此,我想这将是一个混合包。当然,您可以拥有符合规范的实现,这些实现可以做出各种疯狂的事情。


有一种特殊的关系存在于分配器、内核和锁之间。如果堆内存不足,显然会涉及到系统调用,这将进行上下文切换。如果对堆的访问必须串行化,则会涉及到锁,这可能再次需要系统调用和上下文切换。对于高性能应用程序来说,这种可能的系统调用非常重要,即使在大多数情况下它不会发生。 - edA-qa mort-ora-y

2
  • new在堆上为类分配内存,并调用构造函数。
  • 上下文切换不必发生。
  • C++运行时使用其自己的堆实现在空闲存储区域上分配内存,使用任何它认为适合的机制。

通常情况下,C++运行时使用操作系统的内存管理函数分配大块内存,然后使用自己的堆实现将其细分。Microsoft C++运行时大多使用Win32堆函数,这些函数在用户模式中实现,并使用虚拟内存API分配操作系统内存。因此,在当前分配的虚拟内存需要并且需要去操作系统分配更多内存之前,不会发生上下文切换。

在分配内存时存在一个理论问题,即如何找到可用块的堆遍历时间没有上限。但实际上,堆分配通常很快。

除了线程应用程序。由于大多数C++运行时在多个线程之间共享单个堆,因此需要对堆的访问进行串行化。这可能会严重影响某些依赖于多个线程能够新建和删除多个对象的应用程序类别的性能。


glibc具有一个分配器,不会在多线程应用程序中降级。 - edA-qa mort-ora-y
那很好知道。另一方面,msvc使用操作系统分配器,在NT 6.1之前它的性能不太好。 - Chris Becke

1

如果您使用newdelete创建或删除地址,则会将其标记为已占用未分配。实现并不总是与内核通信。更大的内存块被保留并在用户空间中划分为较小的块,以供应用程序使用。

由于newdelete是可重入的(或者根据实现方式是线程安全的),因此可能会发生上下文切换,但是在使用默认的newdelete时,您的实现仍然是线程安全的。

在C++中,您可以覆盖newdelete运算符,例如放置您的内存管理:

#include <cstdlib> //declarations of malloc and free
#include <new>
#include <iostream>
using namespace std;

class C {
public:
  C(); 
  void* operator new (size_t size); //implicitly declared as a static member function
  void operator delete (void *p); //implicitly declared as a static member function
};

void* C::operator new (size_t  size) throw (const char *){
  void * p = malloc(size);
  if (p == 0)  throw "allocation failure";  //instead of std::bad_alloc
  return p; 
}

void C::operator delete (void *p){  
  C* pc = static_cast<C*>(p); 
  free(p);  
}

int main() { 
   C *p = new C; // calls C::new
   delete p;  // calls C::delete
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接