在一个C++程序中有不同的新运算符:如何?馊主意?

Mic*_*ael 9 c++ memory cuda new-operator

我的代码中有不同的内存分配器:一个用于CUDA(托管与否),一个用于纯主机内存.我还可以设想一种情况,当你想要使用不同的分配算法时 - 例如,一个用于大型长生活区块,另一个用于短生活,小型对象.

我想知道如何正确实施这样一个系统.

安置新的?

我当前的解决方案使用placement new,其中指针决定使用哪个内存和内存分配器.删除/取消分配对象时必须小心.目前,它的工作原理,但我认为这不是一个好的解决方案.

MyObj* cudaObj = new(allocateCudaMemoryField(sizeof(MyObj)) MyObj(arg1, arg2);
MyObj* hostObj = new(allocateHostMemoryField(sizeof(MyObj)) MyObj(arg1, arg2);
Run Code Online (Sandbox Code Playgroud)

重载新,但如何?

我想找一个重载new运算符的解决方案.看起来如下:

MyObj* cudaObj = CudaAllocator::new MyObj(arg1, arg2);
MyObj* hostObj = HostAllocator::new MyObj(arg1, arg2);
CudaAllocator::delete cudaObj;
HostAllocator::delete hostObj;
Run Code Online (Sandbox Code Playgroud)

我想我可以通过拥有一个命名空间来实现这一点,CudaAllocator并且HostAllocator每个都有一个重载newdelete.

两个问题:

  • new在代码中有不同的重载是否合理,或者这是设计缺陷的标志?
  • 如果没关系,如何最好地实现它?

小智 3

new重载运算符/是有时间和地点的delete,但通常只有在用尽更简单的措施时才首选它。

放置的主要缺点new是它要求调用者“记住”对象是如何分配的,并在该对象达到其生命周期结束时采取适当的操作来调用相应的解除分配。此外,要求调用者调用放置new在语法上是很麻烦的(我认为这是您提到的“不是一个好的解决方案”。)

new重载/的主要缺点delete是它对于给定类型只能执行一次(正如 @JSF 指出的那样)。这将对象与其分配/释放的方式紧密耦合。

重载新建/删除

假设这样设置:

#include <memory>
#include <iostream>

void* allocateCudaMemoryField(size_t size)
{
   std::cout << "allocateCudaMemoryField" << std::endl;
   return new char[size]; // simulated
}
void* allocateHostMemoryField(size_t size)
{
   std::cout << "allocateHostMemoryField" << std::endl;
   return new char[size];
}
void deallocateCudaMemoryField(void* ptr, size_t)
{
   std::cout << "deallocateCudaMemoryField" << std::endl;
   delete ptr; // simulated
}
void deallocateHostMemoryField(void* ptr, size_t)
{
   std::cout << "deallocateHostMemoryField" << std::endl;
   delete ptr;
}
Run Code Online (Sandbox Code Playgroud)

这是MyObj重载的new/ delete(你的问题):

struct MyObj
{
   MyObj(int arg1, int arg2)
   {
      cout << "MyObj()" << endl;
   }
   ~MyObj()
   {
      cout << "~MyObj()" << endl;
   }
   static void* operator new(size_t)
   {
      cout << "MyObj::new" << endl;
      return ::operator new(sizeof(MyObj));
   }
   static void operator delete(void* ptr)
   {
      cout << "MyObj::delete" << endl;
      ::operator delete(ptr);
   }
};

MyObj* const ptr = new MyObj(1, 2);
delete ptr;
Run Code Online (Sandbox Code Playgroud)

打印以下内容:

MyObj::new
MyObj()
~MyObj()
MyObj::delete

C Plus Plusy 解决方案

更好的解决方案可能是结合使用 RAII 指针类型和工厂来向调用者隐藏分配和释放的详细信息。此解决方案使用放置new,但通过将删除器回调方法附加到unique_ptr.

class MyObjFactory
{
public:
   static auto MakeCudaObj(int arg1, int arg2)
   {
      constexpr const size_t size = sizeof(MyObj);
      MyObj* const ptr = new (allocateCudaMemoryField(size)) MyObj(arg1, arg2);
      return std::unique_ptr <MyObj, decltype(&deallocateCudaObj)> (ptr, deallocateCudaObj);
   }
   static auto MakeHostObj(int arg1, int arg2)
   {
      constexpr const size_t size = sizeof(MyObj);
      MyObj* const ptr = new (allocateHostMemoryField(size)) MyObj(arg1, arg2);
      return std::unique_ptr <MyObj, decltype(&deallocateHostObj)> (ptr, deallocateHostObj);
   }

private:
   static void deallocateCudaObj(MyObj* ptr) noexcept
   {
      ptr->~MyObj();
      deallocateCudaMemoryField(ptr, sizeof(MyObj));
   }
   static void deallocateHostObj(MyObj* ptr) noexcept
   {
      ptr->~MyObj();
      deallocateHostMemoryField(ptr, sizeof(MyObj));
   }
};

{
  auto objCuda = MyObjFactory::MakeCudaObj(1, 2);
  auto objHost = MyObjFactory::MakeHostObj(1, 2);
}
Run Code Online (Sandbox Code Playgroud)

印刷:

allocateCudaMemoryField
MyObj()
allocateHostMemoryField
MyObj()
~MyObj()
deallocateHostMemoryField
~MyObj()
deallocateCudaMemoryField

通用版

这会变得更好。使用相同的策略,我们可以处理任何类的分配/释放语义。

class Factory
{
public:
   // Generic versions that don't care what kind object is being allocated
   template <class T, class... Args>
   static auto MakeCuda(Args... args)
   {
      constexpr const size_t size = sizeof(T);
      T* const ptr = new (allocateCudaMemoryField(size)) T(args...);
      using Deleter = void(*)(T*);
      using Ptr = std::unique_ptr <T, Deleter>;
      return Ptr(ptr, deallocateCuda <T>);
   }
   template <class T, class... Args>
   static auto MakeHost(Args... args)
   {
      constexpr const size_t size = sizeof(T);
      T* const ptr = new (allocateHostMemoryField(size)) T(args...);
      using Deleter = void(*)(T*);
      using Ptr = std::unique_ptr <T, Deleter>;
      return Ptr(ptr, deallocateHost <T>);
   }

private:
   template <class T>
   static void deallocateCuda(T* ptr) noexcept
   {
      ptr->~T();
      deallocateCudaMemoryField(ptr, sizeof(T));
   }
   template <class T>
   static void deallocateHost(T* ptr) noexcept
   {
      ptr->~T();
      deallocateHostMemoryField(ptr, sizeof(T));
   }
};
Run Code Online (Sandbox Code Playgroud)

与新的 S 级一起使用:

struct S
{
   S(int x, int y, int z) : x(x), y(y), z(z)
   {
      cout << "S()" << endl;
   }
   ~S()
   {
      cout << "~S()" << endl;
   }
   int x, y, z;
};
{
   auto objCuda = Factory::MakeCuda <S>(1, 2, 3);
   auto objHost = Factory::MakeHost <S>(1, 2, 3);
}
Run Code Online (Sandbox Code Playgroud)

印刷:

allocateCudaMemoryField
S()
allocateHostMemoryField
S()
~S()
deallocateHostMemoryField
~S()
deallocateCudaMemoryField

我不想全力以赴地启动模板,但显然该代码已经成熟,可以进行干燥(参数化分配器函数的实现)。

注意事项

当您的对象相对较大并且分配/释放不太频繁时,这种方法效果很好。如果每秒有数百万个对象来来去去,我就不会使用它。

一些相同的策略是有效的,但您还需要考虑类似的策略

  • 在处理阶段开始/结束时批量分配/释放
  • 维护空闲列表的对象池
  • 用于容器的 C++ 分配器对象,例如vector
  • ETC。

这实际上取决于您的需求。

太长了;博士

不,在这种情况下不要超载new/ delete。构建一个委托给通用内存分配器的分配器。