38

上一个标题:“我必须替换全局运算符 new 和 delete 以更改第三方代码中的内存分配策略吗?”

小故事: 我们需要在不改变其源代码的情况下替换第三方库中的内存分配技术。

很长的故事:

考虑进行大量动态分配的内存绑定应用程序(也许,几乎所有可用的系统内存)。我们使用专门的分配器,并在任何地方使用它们(shared_ptr's、容器等)。我们对应用程序中分配的每一个内存字节拥有完全的控制权和权力。

此外,我们需要链接到第三方帮助库。那个讨厌的家伙以某种标准方式进行分配,使用默认运算符, 和newornew[]或其他非标准的东西(让我们概括地说,我们不知道这个库如何管理它的堆分配)。deletedelete[]malloc

如果这个帮助库的分配足够大,我们可能会遇到 HDD 抖动、内存碎片和对齐问题、内存bad_alloc不足和各种问题。

我们不能(或不想)更改库源代码。

第一次尝试:

我们以前从未在发布版本中遇到过如此邪恶的“黑客”。使用覆盖运算符的第一个测试new工作正常,除了:

  • 我们不知道将来会遇到什么问题(这太可怕了)
  • 我们的用户(甚至我们的分配器)现在必须以与我们相同的方式分配

问题:

  1. 有没有办法在不重载全局运算符的情况下挂钩这些分配?(仅限本地库的钩子?)
  2. ...如果我们不知道它到底使用了什么:malloc或者new
  3. 这个签名列表是否完整?(并且没有其他我们必须实现的东西):

    void* operator new (std::size_t size) throw (std::bad_alloc);
    void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) throw();
    void* operator new (std::size_t size, void* ptr) throw();
    void* operator new[] (std::size_t size) throw (std::bad_alloc);
    void* operator new[] (std::size_t size, const std::nothrow_t& nothrow_value) throw();
    void* operator new[] (std::size_t size, void* ptr) throw();
    
    void operator delete (void* ptr) throw();
    void operator delete (void* ptr, const std::nothrow_t& nothrow_constant) throw();
    void operator delete (void* ptr, void* voidptr2) throw();
    void operator delete[] (void* ptr) throw();
    void operator delete[] (void* ptr, const std::nothrow_t& nothrow_constant) throw();
    void operator delete[] (void* ptr, void* voidptr2) throw();
    
  4. 如果该库是动态的,会有什么不同?

编辑#1

如果可能,最好使用跨平台解决方案(看起来不太可能)。如果没有,我们的主要平台:

  • Windows x86/x64 (msvc 10)
  • Linux x86/x64 (gcc 4.6)

编辑#2

快 2 年过去了,很少有操作系统和编译器版本进化,所以我很好奇这个领域是否有一些新的和未开发的东西?有什么标准建议吗?操作系统细节?黑客?您今天如何编写需要大量内存的应用程序?请分享你的经验。

4

3 回答 3

21

呃,我的同情。这将在很大程度上取决于您的编译器、您的 libc 等。过去对我们来说在不同程度上“起作用”的一些橡胶与道路策略(/me 大括号表示反对票)是:

  • 您建议的operator new/operator delete重载 - 尽管请注意,有些编译器对没有throw()规范很挑剔,有些人真的想要它们,有些人希望它们用于新但不用于删除等(我有一个巨大的平台特定#if/#elif块用于所有 4 + 我们现在正在开发的平台)。
  • 另外值得注意的是:您通常可以忽略放置版本,它们不会分配。
  • 看看__malloc_hook和朋友们——请注意,这些已被弃用并且有线程竞争条件——但它们很好,因为 new/delete 往往是根据malloc(但并非总是)实现的。
  • 提供替换malloc, calloc, realloc, 并free以正确的顺序获取链接器参数,以便进行覆盖(这是 gcc 这些天推荐的,尽管我遇到过无法做到的情况,我不得不使用 deprecated __malloc_hook) - 再次,new并且delete 倾向于根据这些来实施,但并非总是如此。
  • 在“我们的代码”中避免使用所有标准分配方法(operator new,malloc等),而是使用自定义函数——对于现有的代码库来说并不容易。
  • 追踪库作者并提供一个野蛮的礼貌请求或补丁来更改他们的库以允许您指定不同的分配器(它可能比自己这样做更快)——我认为这导致了“客户端”的基本规则总是指定分配器或使用我编写的任何库进行分配。

请注意,这不是标准所说的应该发生的答案,只是我的经验。过去,我使用过很多错误/损坏的编译器和 libc 实现,所以 YMMV。我也有幸在相当“密封的系统”上工作,而不是担心任何特定应用程序的可移植性。

关于动态库:我自己目前在这方面有点紧张;我们的“应用程序”作为动态加载,如果它们不是来自我们.so,我们必须非常小心地将任何delete/请求传递回默认分配器。free当前的解决方案是封锁我们对特定区域的分配:如果我们从该地址范围内获得删除/释放,我们将调度到我们的处理程序,否则返回默认值......我什至玩弄过(恐怖) 检查调用者地址以查看它是否在我们的地址空间中的想法。(不过,这种黑客攻击会增加繁荣的可能性。)

即使您是流程负责人并且您正在使用外部库,这也可能是一个有用的策略:标记或限制或以其他方式以某种方式标识您自己的分配(甚至保留您知道的分配列表),并且然后传递任何未知数。不过,所有这些都有丑陋的副作用和局限性。

(期待其他答案!)

于 2013-05-04T19:42:42.607 回答
2

如果无法修改库的源代码 - 或者更好的是,无法影响库的作者来修改它 - 我会说你不走运。

库可能会做一些事情(甚至是无意的),以使其不受您可能采用的任何策略的影响——或者,在最坏的情况下,结果是您的使用会使库变得不稳定,或者可能使您的程序不稳定。例如使用自己的自定义分配器,提供自己的全局operator new()和版本,operator delete()在单个类中覆盖这些运算符等。

一个可能可行的策略是与库供应商合作并进行一些修改。修改(从您的最终)将相当于能够通过指定它使用的分配器来初始化库。对于库来说,这项工作可能很重要(必须触及所有动态分配内存、使用标准容器等的函数),但并非难以处理——在整个代码中使用提供的分配器(或合理的默认值)。

不幸的是,这与您不修改库的要求不一致 - 我对满足这一点的机会持怀疑态度,特别是在您概述的限制范围内(内存渴,托管在 windows/linux 上等)。

于 2016-01-03T12:47:38.020 回答
0

不能在该类库中进行分配,但您可以使用placement new 从该第三方库分配类,即您可以分配内存并在分配的内存上调用这些类的构造函数。所以即使类有它自己的新运算符不会被调用。Howvwer,在类操作内部,未公开的内部类或原语的内存分配将使用第三方库的分配方案完成;除非第三方库允许您指定像 stl 容器这样的分配器,否则无法更改

于 2017-03-10T16:15:25.497 回答