// 内存预检验与"分配失败"兜底(配合 `-fno-exceptions` 构建)。 // // 背景:本项目关闭了 C++ 异常(`.xdata`+`.pdata` 占产物体积约 9%,关闭后**实测省 27,136 B**: // 909,824 → 882,688;再叠加 LTO 共减 120,832 B)。关闭后 `std::bad_alloc` 不再可捕获, // 任何分配失败都会直接 terminate。因此: // 1. 凡"按数据规模预留大块内存"的地方改用 `MemReserveIfRoom()`(预检验); // 2. 万一仍有遗漏,由 `MemInstallTerminateHandler()` 给出可见提示而不是静默消失。 #include "MemProbe.hpp" #include #include #include #include namespace { /** * @brief 「未捕获异常 / 分配失败」的可见提示(`std::terminate` 的兜底路径)。 * * ⚠️ **为什么放在全局匿名命名空间**:下面要在 `namespace __gnu_cxx` 里覆盖 * libstdc++ 的默认终止处理器,而 `__gnu_cxx` 必须是**全局**命名空间 * (不能嵌套在本项目自己的 `jdi` 里)。 * * ⚠️ 本来这个函数由 libstdc++ 的 `__gnu_cxx::__verbose_terminate_handler()` 提供, * 但它所在的 `vterminate.o` 会链入 `cp-demangle.o` * (Release map 实测 `.text` 49,600 + `.rdata` 6,720 ≈ **56 KB** 的 C++ 名字反修饰器, * 对一个不打印栈回溯的 GUI 程序毫无用处)。 * 本文件自行定义该函数 ⇒ 归档成员 `vterminate.o` 不再被拉入 ⇒ demangler 消失 * (2026-11-04 代码审查,实测 −55 KB 量级)。 * * ⚠️ 这里**不能**用任何会再次分配内存的机制(此刻堆可能已不可用): * 只用静态字面量与 Win32 API。 */ void MemTerminateFallbackImpl() { MessageBoxW(nullptr, L"内存不足:本次操作需要的内存超过系统可用量。\n\n" L"建议:减少同时显示的网格数或信号数,或关闭占用内存较大的程序后重试。", L"Just DataInspector", MB_ICONERROR | MB_OK | MB_SETFOREGROUND); std::_Exit(EXIT_FAILURE); } } // namespace namespace jdi { namespace { /** @brief 可用内存查询的缓存有效期(毫秒)。 */ constexpr std::uint64_t kMemCacheMs = 200; /** * @brief 预留后必须保留的可用内存比例(1/8)。 * * 不把可用内存吃光:Windows 在物理内存接近耗尽时会大量换页, * 表现为"整个程序卡死"而不是"分配失败",对用户更糟。 */ constexpr std::size_t kMemHeadroomRatio = 8; std::atomic g_memCachedAt{0}; std::atomic g_memCachedBytes{0}; /** * @brief 应用安装的 terminate 处理(`MemInstallTerminateHandler` 注册)。 * * ⚠️ 与文件末尾的 `__gnu_cxx::__verbose_terminate_handler()` 覆盖共用同一条提示路径。 */ void OnTerminate() { MemTerminateFallbackImpl(); } } // namespace std::size_t MemAvailablePhysical() { const std::uint64_t now = GetTickCount64(); const std::uint64_t last = g_memCachedAt.load(std::memory_order_relaxed); if (last != 0 && (now - last) < kMemCacheMs) { const std::size_t cached = g_memCachedBytes.load(std::memory_order_relaxed); if (cached != 0) { return cached; } } MEMORYSTATUSEX status = {}; status.dwLength = sizeof(status); std::size_t available = std::numeric_limits::max(); if (GlobalMemoryStatusEx(&status) != FALSE) { available = static_cast(status.ullAvailPhys); } g_memCachedBytes.store(available, std::memory_order_relaxed); g_memCachedAt.store(now, std::memory_order_relaxed); return available; } bool MemCanAllocate(std::size_t bytes) { if (bytes == 0) { return true; } const std::size_t available = MemAvailablePhysical(); if (available == std::numeric_limits::max()) { return true; // 查询失败:按"内存充足"处理(宁可尝试也不要误拒) } const std::size_t headroom = available / kMemHeadroomRatio; return bytes <= (available - std::min(available, headroom)); } void MemInstallTerminateHandler() { static bool installed = false; if (installed) { return; } installed = true; std::set_terminate(&OnTerminate); } } // namespace jdi /** * @brief **覆盖** libstdc++ 的默认终止处理器(省掉 56 KB 的 C++ 名字反修饰器)。 * * `std::terminate()` 定义在 `eh_terminate.o` 里,它引用 `__gnu_cxx::__verbose_terminate_handler()`。 * 该符号原本由归档成员 `vterminate.o` 提供,而 `vterminate.o` 又引用 `__cxa_demangle` * ⇒ 链接器被迫拉入 `cp-demangle.o`(Release map 实测 `.text` 49,600 B + `.rdata` 6,720 B)。 * 我们在自己的目标文件里提供同名定义,`eh_terminate.o` 的未定义引用就被满足, * `vterminate.o` 与整个 demangler 都不会进入最终产物。 * * ⚠️ 必须定义在**全局** `__gnu_cxx` 命名空间(不能嵌套在 `jdi` 内)。 * ⚠️ 应用启动后会通过 `jdi::MemInstallTerminateHandler()` 换上自己的处理函数, * 因此这个默认实现只在「启动早期崩溃」时生效 —— 行为与 libstdc++ 的版本等效 * (libstdc++ 版会打印反修饰后的异常类型名,我们只给内存提示)。 */ namespace __gnu_cxx { void __verbose_terminate_handler() { MemTerminateFallbackImpl(); } } // namespace __gnu_cxx