使用自定义分配器及其替代方案重载基本类型
Overloading base types with a custom allocator, and its alternatives
所以,这是一个悬而未决的问题。但是假设我有一个大型应用程序,它全局覆盖了各种 new
和 delete
运算符,以便它们使用自制的 jemalloc 风格的竞技场和自定义对齐方式.
一切都很好,但我 运行 遇到了 segfault 问题,因为其他基于 C++ 的 DLL 及其依赖项也在它们不应该使用的重载分配器时使用了它们' t(即 LLVM),让这个小的自定义分配器陷入困境(缺乏内存和更多压力)。
测试解决方法,我已经将那些全局运算符包装(并移动)到 class 中,并且我让所有基础 classes 继承自它。好吧,这适用于 classes,但不适用于基本类型。就是这个问题。
鉴于 C++ 不允许有用的东西,例如每个 namespace
有单独的分配器,或限制每个可执行模块的 new
运算符,在基本数据类型中模拟它的最佳方法是什么,我不能直接将 class 设为 int
?
显而易见的方法是将它们包装在自定义模板中,但问题是性能。我是否必须在第二层下模拟所有数组和索引操作,以便我可以从不同的地方 malloc
而不必更改其余功能代码?有更好的方法吗?
P.S.:我也一直在考虑使用带有额外参数的特殊全局 new
/delete
运算符,同时保留标准的一个人。从而确保我(好吧,我的可执行模块是)唯一调用这些全局函数的人。它应该是一个简单的搜索和替换。
嗯,快点更新。我最后为'解决'这个难题所做的是手动检测调用覆盖的全局分配器的代码是否来自主executable模块并有条件地重定向所有外部 new
/ delete
调用它们相应的 malloc
/ free
,同时仍然为我们自己的 内部代码。
怎么样?在做了一些研发之后,我发现这可以通过使用 MSVC 上的内置 _ReturnAddress()
和 GCC/Clang 上的 __builtin_extract_return_addr(__builtin_return_address(0))
来完成;我可以说到目前为止它在生产软件中似乎运行良好。
现在,当来自我们地址 space 的一些 C++ 代码需要一些内存时,我们可以看到它来自哪里。
但是,我们如何确定该地址是我们进程 space 中其他模块的一部分还是我们自己的?我们可能需要找出主程序的 base 和 end 地址,在启动时将它们缓存为全局变量,并检查 return 地址在范围内。
所有这些都是为了极少的开销。但是,我们的第二个问题是检索基地址在每个平台上都是不同的。经过一些研究,我发现事情比预期的要简单:
在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(冗长且不可靠)遍历处理模块 table。我正在查看链接器映射输出并注意到 _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 中,事情的记录更少,我不得不使用达尔文内核开源代码拼凑自己的东西,并追踪一些晦涩的低级工具作为参考。请记住 _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 引起的,它使用 LLVM(纯 C++ 输入和输出)用于他们的许多 GLSL 着色器编译器,并且喜欢不请自来地吞噬我的小定制内存空间的大块。
重写在结构上依赖于这些分配器的遗留程序是不可能的,因为它的规模和复杂性,所以这是让事情按预期工作的最佳方式。
这只是几行可选的、偷偷摸摸的、针对每个平台的额外代码。
所以,这是一个悬而未决的问题。但是假设我有一个大型应用程序,它全局覆盖了各种 new
和 delete
运算符,以便它们使用自制的 jemalloc 风格的竞技场和自定义对齐方式.
一切都很好,但我 运行 遇到了 segfault 问题,因为其他基于 C++ 的 DLL 及其依赖项也在它们不应该使用的重载分配器时使用了它们' t(即 LLVM),让这个小的自定义分配器陷入困境(缺乏内存和更多压力)。
测试解决方法,我已经将那些全局运算符包装(并移动)到 class 中,并且我让所有基础 classes 继承自它。好吧,这适用于 classes,但不适用于基本类型。就是这个问题。
鉴于 C++ 不允许有用的东西,例如每个 namespace
有单独的分配器,或限制每个可执行模块的 new
运算符,在基本数据类型中模拟它的最佳方法是什么,我不能直接将 class 设为 int
?
显而易见的方法是将它们包装在自定义模板中,但问题是性能。我是否必须在第二层下模拟所有数组和索引操作,以便我可以从不同的地方 malloc
而不必更改其余功能代码?有更好的方法吗?
P.S.:我也一直在考虑使用带有额外参数的特殊全局 new
/delete
运算符,同时保留标准的一个人。从而确保我(好吧,我的可执行模块是)唯一调用这些全局函数的人。它应该是一个简单的搜索和替换。
嗯,快点更新。我最后为'解决'这个难题所做的是手动检测调用覆盖的全局分配器的代码是否来自主executable模块并有条件地重定向所有外部 new
/ delete
调用它们相应的 malloc
/ free
,同时仍然为我们自己的 内部代码。
怎么样?在做了一些研发之后,我发现这可以通过使用 MSVC 上的内置 _ReturnAddress()
和 GCC/Clang 上的 __builtin_extract_return_addr(__builtin_return_address(0))
来完成;我可以说到目前为止它在生产软件中似乎运行良好。
现在,当来自我们地址 space 的一些 C++ 代码需要一些内存时,我们可以看到它来自哪里。
但是,我们如何确定该地址是我们进程 space 中其他模块的一部分还是我们自己的?我们可能需要找出主程序的 base 和 end 地址,在启动时将它们缓存为全局变量,并检查 return 地址在范围内。
所有这些都是为了极少的开销。但是,我们的第二个问题是检索基地址在每个平台上都是不同的。经过一些研究,我发现事情比预期的要简单:
在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(冗长且不可靠)遍历处理模块 table。我正在查看链接器映射输出并注意到
_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 中,事情的记录更少,我不得不使用达尔文内核开源代码拼凑自己的东西,并追踪一些晦涩的低级工具作为参考。请记住
_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 引起的,它使用 LLVM(纯 C++ 输入和输出)用于他们的许多 GLSL 着色器编译器,并且喜欢不请自来地吞噬我的小定制内存空间的大块。
重写在结构上依赖于这些分配器的遗留程序是不可能的,因为它的规模和复杂性,所以这是让事情按预期工作的最佳方式。
这只是几行可选的、偷偷摸摸的、针对每个平台的额外代码。