在一个C++程序中使用不同的新运算符:如何实现?是否明智?

9
我在代码中使用了不同的内存分配器:一个用于CUDA(管理的或非管理的),一个用于纯主机内存。我也可以想象在某些情况下你可能想要使用不同的分配算法——例如,一个算法用于大型、长期存在的块,另一个算法用于短暂的、小型的对象。
我想知道如何正确地实现这样的系统。
使用放置 new吗?
我的当前解决方案使用了放置 new,在其中指针决定使用哪个内存和内存分配器。然后需要注意删除/释放对象时的操作。目前它可以工作,但我认为这不是一个好的解决方案。
MyObj* cudaObj = new(allocateCudaMemoryField(sizeof(MyObj)) MyObj(arg1, arg2);
MyObj* hostObj = new(allocateHostMemoryField(sizeof(MyObj)) MyObj(arg1, arg2);

重载 new,但是怎么做呢?

我想采用重载 new 操作符的解决方案。具体实现如下:

MyObj* cudaObj = CudaAllocator::new MyObj(arg1, arg2);
MyObj* hostObj = HostAllocator::new MyObj(arg1, arg2);
CudaAllocator::delete cudaObj;
HostAllocator::delete hostObj;

我认为可以通过使用命名空间CudaAllocatorHostAllocator,分别重载newdelete来实现这一点。
两个问题:
  • 在代码中有不同的new重载是否合理,还是这是设计缺陷的标志?
  • 如果可以,如何最好地实现它?

1
首先要考虑那些不是直接分配的对象,比如 std::vector 中的对象。通常情况下,在重载 new 时,您希望将重载与正在分配的类型相关联,就像您在许多在线示例中找到的那样,因此在容器中分配的 MyObj 将获得与您自己分配的对象相同的分配器。您的想法可能更适合您的目标。您举了一个使用不同分配器分配相同类型的示例。我只是说在做出决定之前应该仔细考虑更难的情况。 - JSF
1
CUDA自定义内存分配器在CUDA Thrust库中已经基本上得到解决(例如,请参见此处)。使用现有的Thrust基础设施可能与(重新)发明您自己的基础设施一样容易。 - talonmies
继承自定义分配器的想法似乎不错,但我认为thrust库本身在这里并没有帮助。我必须使对象在GPU上可用,而且我不知道thrust如何帮助我以一种不被CUDA管理内存所覆盖的方式来实现这一点。 - Michael
你应该看一下 https://dev59.com/i2025IYBdhLWcg3wHR8Y。分配函数必须在全局或类作用域中。 - Coder
1个回答

3

在使用it技术时,有时候会用到重载运算符new/delete,但通常只有在其他简单措施已经无法解决问题的情况下才会使用。

放置new的主要缺点是需要调用者“记住”对象的分配方式,并在该对象达到生命周期终点时采取适当的操作来调用相应的释放操作。此外,要求调用者调用放置new语法上也是负担(我想这就是您提到的“不好的解决方案”)。

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

重载new/delete

假设有以下设置:

#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;
}

这是带有重载new/deleteMyObj(你的问题):

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;

打印以下内容:
MyObj::new MyObj() ~MyObj() MyObj::delete C++解决方案
更好的解决方案可能是使用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);
}

输出:

分配Cuda内存字段
MyObj()
分配主机内存字段
MyObj()
~MyObj()
释放主机内存字段
~MyObj()
释放Cuda内存字段

通用版本

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

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));
   }
};

与新的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);
}

打印:

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

我不想完全使用模板,但很明显那段代码需要DRY(在分配器函数上对实现进行参数化)。

考虑因素

当您的对象相对较大且不频繁分配/释放时,这种方法非常有效。如果您每秒有数百万个对象进出,则不建议使用此方法。

一些相同的策略可以使用,但您还需要考虑诸如以下策略:

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

它真的取决于您的需求。

tl;dr

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


如果你想在STL容器中使用这个对象,除非你自己创建一个指针容器并执行内存分配/释放,否则这个解决方案无法胜任。 - xryl669

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