【问题标题】:Overloading base types with a custom allocator, and its alternatives使用自定义分配器重载基本类型及其替代方案
【发布时间】:2017-11-14 14:25:49
【问题描述】:

所以,这是一个悬而未决的问题。但是假设我有一个大型应用程序,它全局覆盖各种 newdelete 运算符,以便它们使用自制的 jemalloc 风格的竞技场和自定义对齐方式。

一切都很好,但我一直遇到 segfault 问题,因为其他基于 C++ 的 DLL 及其依赖项也在不应该使用重载分配器(即 LLVM)时使用自定义分配器跪了(缺乏记忆和更多的压力)。

测试变通方法,我已将这些全局运算符包装(并移动)到一个类中,并让所有基类都继承自它。好吧,这适用于类,但不适用于基本类型。这就是问题所在。


鉴于 C++ 不允许有用的东西,例如每个 namespace 有单独的分配器,或限制每个可执行模块的 new 运算符,在基本数据类型中模拟它的最佳方法是什么,我不能直接继承 int?

显而易见的方法是将它们包装在自定义模板中,但问题在于性能。我是否必须在第二层下模拟所有数组和索引操作,以便我可以从不同的地方malloc 而不必更改其余的功能代码?有更好的方法吗?

P.S.:我也一直在考虑使用带有额外参数的特殊全局 new/delete 运算符,而不使用标准运算符。从而确保我(好吧,我的可执行模块是)唯一调用这些全局函数的人。它应该是一个简单的搜索和替换。

【问题讨论】:

  • This 可能感兴趣

标签: c++ templates operator-overloading global-variables dynamic-allocation


【解决方案1】:

好吧,快速更新。我最终为“解决”这个难题所做的是手动检测调用覆盖的全局分配器的代码是否来自主可执行模块并有条件地重定向所有外部 new / delete 调用它们对应的malloc / free,同时仍然为我们自己的内部代码使用自定义竞技场分配器。


怎么样?在做了一些研发之后,我发现这可以通过使用 MSVC 上的内置 _ReturnAddress() 和 GCC/Clang 上的 __builtin_extract_return_addr(__builtin_return_address(0)) 来完成;我可以说,到目前为止,它在生产软件中似乎运行良好。

现在,当我们地址空间中的一些 C++ 代码需要一些内存时,我们可以看到它来自哪里。

但是,我们如何确定该地址是我们进程空间中某个其他模块的一部分还是我们自己的?我们可能需要找出主程序的 baseend 地址,在启动时将它们缓存为全局地址,并检查返回地址是否在界限内。

只需极少的开销。但是,我们的第二个问题是检索基地址在每个平台上都是不同的。经过一番研究,我发现事情比预期的要简单:

  • 在 Windows/Win32 中我们可以简单地这样做:

    #include <windows.h>
    #include <psapi.h>
    
    inline void __initialize_base_address()
    {
        MODULEINFO minfo;
    
        GetModuleInformation(GetCurrentProcess(), GetModuleHandle(NULL), &minfo, sizeof(minfo));
    
        base_addr = (uintptr_t) minfo.lpBaseOfDll;
        base_end  = (uintptr_t) minfo.lpBaseOfDll + minfo.SizeOfImage;
    }
    
  • 在 Linux 中有上千种方法可以做到这一点,包括链接器全局变量和一些 debuggey(冗长且不可靠)遍历进程模块表的方法。我正在查看链接器映射输出并注意到_init_fini 函数似乎总是包装其余的.text 部分符号。有时很难找到适用于任何地方的最简单的解决方案:

    #include <link.h>
    
    inline void __initialize_base_address()
    {
        void *handle = dlopen(0, RTLD_NOW);
    
        base_addr = (uintptr_t) dlsym(handle, "_init");
        base_end  = (uintptr_t) dlsym(handle, "_fini");
    
        dlclose(handle);
    }
    
  • 虽然在 macOS 中记录的东西更少,我不得不使用 Darwin 内核开源代码拼凑我自己的东西,并追踪一些晦涩的低级工具作为参考。请记住,_NSGetMachExecuteHeader() 只是内部_mh_execute_header 链接器全局的包装器。如果您需要对解析 Mach-O 格式及其结构做任何事情,那么getsect.h 是您的最佳选择:

    #include <mach-o/getsect.h>
    #include <mach-o/ldsyms.h>
    #include <crt_externs.h>
    
    inline void __initialize_base_address()
    {
        size_t size;
    
        void *ptr = getsectiondata(&_mh_execute_header, SEG_TEXT, SECT_TEXT, &size);
    
        base_addr = (uintptr_t) _NSGetMachExecuteHeader();
        base_end  = (uintptr_t) ptr + size;
    }
    

要记住的另一件事是,这个 some-other-cpp-module-is-using-our-internal-allocator-that-globally-overrides-new-causing-weird-bugs 问题似乎是一个问题Linux,也许是 macOS,我在 Windows 中没有这个问题,可能是因为在这个过程中没有加载冲突的 DLL,主要是基于 C API。我认为,或者平台可能为每个模块使用不同的 C++ 运行时。

我遇到的主要问题是由 Mesa3D 引起的,它在许多 GLSL 着色器编译器中使用 LLVM(纯 C++ 进出),并且喜欢在不请自来的情况下吞噬我的小型定制内存领域的大块。

由于其庞大的规模和复杂性,重写在结构上依赖于这些分配器的遗留程序是不可能的,因此事实证明这是使事情按预期工作的最佳方法。

这只是几行可选的、偷偷摸摸的、额外的每个平台代码。

【讨论】:

    猜你喜欢
    • 2017-05-05
    • 2013-12-17
    • 2017-11-05
    • 2023-03-17
    • 1970-01-01
    • 2010-11-13
    • 2020-02-09
    • 1970-01-01
    • 2020-10-22
    相关资源
    最近更新 更多