Files
Just-DataInspector/Makefile
T
DeepSeek V4.1 Flash 34365b3c1c perf(size): 用自写整数格式化替换全部 std::to_wstring / std::to_string
背景:Code Review 报告 §10.3 / §11.5 建议「替换 std::to_wstring / std::to_string,
可去掉 gdtoa 链(≈33 KB)」。本轮把它做完,并给出这条建议的**真实边界**。

一、新增 `Utf8Util::FormatInteger*`(手写十进制展开,不经 printf)
* `FormatInteger(long long)` / `FormatIntegerUnsigned(unsigned long long)` /
  `FormatIntegerNarrow(long long)`(后者供 MatLoader 拼 HDF5 成员路径)。
* 语义与 `std::to_wstring` / `std::to_string` 完全一致:`0` → `"0"`、负数带 `-`、
  `LLONG_MIN` 用模 2^64 语义取绝对值(不会溢出)。
* 这样做的直接理由:预编译 libstdc++ 的 `to_string` 内部转调 `__mingw_swprintf`,
  工程里只要留一处,MinGW 的 printf 引擎(`mingw_wpformat.o`)+ gdtoa 就必然被链入。

二、全仓替换(76 处 `to_wstring` + 2 处 `to_string`,20 个文件)
* `MatLoader.cpp` 的 `TrimNumber` 整数分支也改为 `FormatInteger`(它曾是产物里
  **最后一个**引用 MinGW printf 的调用点)。
* 需要的地方补 `#include "Utf8Util.hpp"`(仅 `MatLoader.cpp` 一个新 include,其余由
  `PlotViewInternal` / `SignalPanelInternal` / `ApplicationInternal` / `DataModelInternal`
  已经包含)。

三、⚠️ 实测结论:gdtoa **去不掉**(把这件事彻底查清并写进 Makefile 注释)
* 只替换 `to_wstring`/`to_string`:**−1,024 B**(787,968 → 786,944)。
* 再叠加 `-D__USE_MINGW_ANSI_STDIO=0`(把 `swprintf` 让给系统 UCRT):**0 字节**。
* 根因(定位到文件与行):libstdc++ 的
  `lib/gcc/.../include/c++/x86_64-w64-mingw32/bits/os_defines.h:48-51` **无条件**执行
  `#undef __USE_MINGW_ANSI_STDIO` + `#define __USE_MINGW_ANSI_STDIO 1`
  (注释:libstdc++ depends on POSIX-definitions of those functions)。
  `g++ -E -dM` 实测最终值恒为 1 ⇒ **任何 C++ TU 的 `swprintf` / `strtod` 都是 MinGW 版本**,
  命令行无法覆盖。
* 要真省这 33 KB 只能自己实现「浮点 → 十进制(正确舍入)」与「浮点解析的
  十六进制/inf/nan 分支」,等价于重写 gdtoa。**判定:不做**(风险 >> 收益)。
* 该开关已回退,Makefile 里保留一段说明,避免后续接手者重复这两次实验。

四、验证
* 新增单元测试 `integer_replacement_matches_std`:12 个有符号样本(含 LLONG_MIN/MAX)
  + 5 个无符号样本(含 ULLONG_MAX),与 `std::to_wstring` **逐字符对照**,
  并校验窄/宽版本内容一致。
* `UnitTests`:`checks=260 failed=0 cases=19`(新增 41 条断言)。
* `OffscreenChecks -all`:6 个检查全 PASS,`checks=87 failed=0`。
* `panels -mode compare`:7 个面板像素哈希与几何**逐项不变**
  (整数文本从 `to_wstring` 换到手写实现后,渲染结果零差异 —— 这是最强的语义证据)。
* release 全量零 error / 零 warning;产物 787,968 → **786,944 B**。
2026-11-04 14:31:02 +08:00

360 lines
14 KiB
Makefile
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 目标文件名称
TARGET_FILE_NAME = Target
# 编译目录
BUILD_DIR = Build
# Release 编译子目录
SUB_DIR_RELEASE = Release
# Debug 编译子目录
SUB_DIR_DEBUG = Debug
# ⚠️ 产物体积**预警阈值**(用户 2026-10-29 更新):**1MB(1,048,576 B)**。
# 这不是"不许超过":**超过仍可发布**,但到达阈值时必须主动提醒用户,
# 并说明"超在哪一段(`.text` / `.rdata` / `.xdata`+`.pdata` / `.rsrc`)、由哪个改动贡献"。
# 旧红线 900KB / 800KB 均已作废。
# 查段构成的命令(排查超限原因的标准做法):objdump -h Build/Release/Target.exe
RELEASE_OPT = -Oz
# ---- 体积相关编译开关 ------------------------------------------------------
# ⚠️ 曾经有一个 `LAYOUT_BOUNDS` 开关(声称能剔除"显示布局边界"调试叠加层),
# 但它从来没有被任何 `#if` 引用过 —— 是个**名实不符**的死开关,已删除(2026-10-29 审计)。
# 该叠加层现在是**正式功能**(设置菜单里的开关),随产物一起编译;
# 要真正把它变成可剔除项,需要给每个组件的 `BuildBoxes` 重载都加 `#if`,
# 代价远大于它占的十几 KB(当前产物 791,552 B,离 1MB 预警线还有 250+ KB)。
# NO_EXCEPTIONS:1 = 用 `-fno-exceptions` 构建(省 `.xdata`+`.pdata` 约 28KB)。
# ⚠️ 代价:所有"按数据规模预留大块内存"的地方必须用 `MemProbe.hpp` 的
# `MemReserveIfRoom()` **预检验**,不能再靠 try/catch。
# 兜底:`Application::Initialize` 里安装了 `MemInstallTerminateHandler()`
# (万一仍有遗漏,OOM 时弹一次带建议的模态提示后干净退出,而不是静默消失)。
NO_EXCEPTIONS = 1
# LTO:1 = 链接时优化(实测 −94KB)。构建时间约翻倍。
# ⚠️ **切换 LTO 必须 `make clean`**:LTO 与非 LTO 目标文件混链会让
# GCC 15.2.0 在 `binds_to_current_def_p` 处 ICE(已实测复现)。
# ⚠️ 开启 LTO 后 `-Wa,...,-ahlms` 的 `.lst` 汇编清单不再生成(lto-wrapper 会丢弃并告警),
# 因此 `ASM_LISTING` 在 LTO 下为空。
LTO = 1
# ---------------------------------------------------------------------------
# Debug 优化等级
DEBUG_OPT = -O0
# GUI或CUI编译选项 [0]CUI/Console [1]GUI
GUI = 1
# C编译标准
C_STD = c17
# C++编译标准
CXX_STD = c++17
# 源文件编码定义
INPUT_CHAR_ENCODING = UTF-8
# 编译产物单字节字符(char)编码定义
OUTPUT_CHAR_ENCODING = GBK
# 编译产物宽字符(wchar_t)编码定义
OUTPUT_WCHAR_ENCODING = UTF-16LE
# 编译工具前缀
COMPLIER_PREFIX = \
# C编译工具
C_COMPLIER = $(strip $(COMPLIER_PREFIX))gcc
# C++编译工具
CXX_COMPLIER = $(strip $(COMPLIER_PREFIX))g++
# Windows 资源文件编译工具
WIN_RES_COMPLIER = windres
##################################################################################
# 链接库
LIB_LINK = \
-lgdi32 \
-lgdiplus \
-lmsimg32 \
-lcomdlg32 \
-ld3d11 \
-ldwmapi \
-ld3dcompiler \
# C定义
C_DEFS = \
_UNICODE \
UNICODE \
# C头文件目录
C_INCLUDES_PATHS = \
# C源文件目录
C_SOURCES_PATHS = \
# C额外单个源文件
C_EXTERA_SOURCES = \
# C++定义
CXX_DEFS = \
_UNICODE \
UNICODE \
# C++ 头文件目录
# ⚠️ 目录约定:**所有头文件(.hpp)只放在 `Inc`**,`Src` 只放实现(.cpp)。
# `*Internal.hpp`(组件内部共享的常量/工具,如 `PlotViewInternal.hpp`)同样属于头文件,
# 历史上曾被放在 `Src` 下(那里也能编译,因为编译器会先找 `#include` 所在目录),
# 但那是靠“同目录优先”隐式生效的 —— 一旦文件被搬走就会无声失效。现已全部移入 `Inc`。
CXX_INCLUDES_PATHS = \
Utilities/Inc \
Resources/Inc \
Components/Main/Inc \
Components/Shared/Inc \
Components/Application/Inc \
Components/TabBar/Inc \
Components/Toolbar/Inc \
Components/SignalPanel/Inc \
Components/PlotView/Inc \
# C++源文件目录
CXX_SOURCES_PATHS = \
Utilities/Src \
Components/Main/Src \
Components/Shared/Src \
Components/Application/Src \
Components/TabBar/Src \
Components/Toolbar/Src \
Components/SignalPanel/Src \
Components/PlotView/Src \
# C++额外单个源文件
CXX_EXTERA_SOURCES = \
# Windows 资源文件脚本头文件路径
WIN_RESOURCE_INCLUDES_PATHS = \
Resources \
Resources/Inc \
# Windows 资源文件脚本输入代码页(对应 INPUT_CHAR_ENCODING = UTF-8)
WIN_RESOURCE_CODEPAGE = 65001
# Windows 资源文件脚本列表
WIN_RESOURCE_SCRIPTS = \
Resources/JustDataInspector.rc \
##################################################################################
# C编译选项
CFLAGS = $(foreach text,$(C_DEFS),$(addprefix -D,$(text))) \
$(foreach path,$(C_INCLUDES_PATHS),$(addprefix -I,$(path))) \
-std=$(strip $(C_STD)) \
-finput-charset=$(strip $(INPUT_CHAR_ENCODING)) \
-fexec-charset=$(strip $(OUTPUT_CHAR_ENCODING)) \
-fwide-exec-charset=$(strip $(OUTPUT_WCHAR_ENCODING)) \
-static \
$(FUNC_SECTIONS) \
-Wall \
-MMD -MP -MF"$(@:%.o=%.d)" \
# C++编译选项
CXXFLAGS = $(foreach text,$(CXX_DEFS),$(addprefix -D,$(text))) \
$(foreach path,$(CXX_INCLUDES_PATHS),$(addprefix -I,$(path))) \
-std=$(strip $(CXX_STD)) \
-finput-charset=$(strip $(INPUT_CHAR_ENCODING)) \
-fexec-charset=$(strip $(OUTPUT_CHAR_ENCODING)) \
-fwide-exec-charset=$(strip $(OUTPUT_WCHAR_ENCODING)) \
-static \
$(FUNC_SECTIONS) \
-Wall \
-MMD -MP -MF"$(@:%.o=%.d)" \
-lstdc++ \
# 链接选项
LDFLAGS = -Wl,-Map,$(basename $@).map \
$(GC_SECTIONS) \
##################################################################################
# GUI与CUI选项附加
ifeq ($(GUI), 1)
LDFLAGS += -mwindows
else
LDFLAGS += -mconsole
endif
# ---- 体积/行为相关开关(必须在 CFLAGS / CXXFLAGS / LDFLAGS 定义之后)--------
# 说明:这些是 `=` / `+=` 的递归变量,make 在展开 recipe 时才求值,
# 因此放在 LDFLAGS 定义之后仍然能正确作用到前面的 CFLAGS / LDFLAGS。
ifeq ($(strip $(NO_EXCEPTIONS)), 1)
CXXFLAGS += -fno-exceptions
endif
# ---- RTTI:全项目不使用 `dynamic_cast` / `typeid` ----
# 2026-11-04 审查:唯一的 `dynamic_cast`(Toolbar::BuildBoxes)是多余的强转,
# 已改为直接使用已有的 `UiButtonWidget*`。RTTI 会链入 `cp-demangle.o`
# (Release map 实测 56,320 B)+ `type_info` 表,最小复现程序实测 +75,264 B。
NO_RTTI = 1
ifeq ($(strip $(NO_RTTI)), 1)
CXXFLAGS += -fno-rtti
endif
# ---- SEH 展开表:只对 Release 关闭 `-fasynchronous-unwind-tables` ----
# 2026-11-04 审查实测:**只对 Release** 关闭后产物从 829,440 → 793,088 B(**−36,352 B**),
# 因为 `-fno-exceptions` 下绝大多数函数不再需要 `.pdata`/`.xdata`。
# * `.pdata` 只用于栈回溯(崩溃诊断);本程序不打印栈回溯,功能不受影响。
# * ⚠️ 代价:Release 崩溃时 WER 拿不到可靠调用栈(Debug 构建保留完整展开表)。
# * ⚠️ 全关(`-fno-unwind-tables`)会链接失败(历史踩坑),**不要**打开。
RELEASE_ONLY_OPT = -fno-asynchronous-unwind-tables
# ---- 关于「把 `swprintf` 交给系统 UCRT」:**在 C++ 里做不到**,别再试 ----
# 动机:MinGW 的 ANSI stdio(`mingw_wpformat.o`)会链入 gdtoa 整套
# (`strtodg` / `gdtoa` / `gethex` / `hexnan`),map 实测 ≈33 KB。
#
# 本轮实测(2026-11-04,两次独立实验):
# 1. 把全仓 76 处 `std::to_wstring` + 2 处 `std::to_string` 换成
# `Utf8Util::FormatInteger*`(手写十进制展开):**−1,024 B**(787,968 → 786,944)。
# 这一步是有效的,且去掉了 libstdc++ 的 `to_string` 对 printf 的依赖。
# 2. 再加 `-D__USE_MINGW_ANSI_STDIO=0`:**0 字节**,`mingw_wpformat.o` 仍在。
#
# 原因(已定位到具体文件与行):libstdc++ 在
# lib/gcc/x86_64-w64-mingw32/<ver>/include/c++/x86_64-w64-mingw32/bits/os_defines.h
# 里**无条件**执行:
# #undef __USE_MINGW_ANSI_STDIO
# #define __USE_MINGW_ANSI_STDIO 1
# 注释写着 "libstdc++ depends on POSIX-definitions of those functions"。
# 也就是说:**任何 C++ 翻译单元**里的 `swprintf` / `strtod` 都必然是 MinGW 版本,
# 命令行开关无法覆盖(`g++ -E -dM` 实测最终值恒为 1)。
#
# ⇒ 要真省掉这 33 KB,只能连**浮点格式化**也自己实现(等价于重写 gdtoa 的
# 正确舍入十进制展开)与**浮点解析**的十六进制/inf/nan 分支。风险远大于收益,
# 判定:**不做**。保留证据:`Tools/UnitTests` 的 `stdio_mingw_vs_ucrt_formats`
# (13.6 万次调用逐字符比对,0 差异)说明两套 printf 在这些格式上完全等价,
# 将来若平台换了 C 运行库,这条测试可直接复用。
# 丢弃未被引用的函数/数据(非 LTO 时还需要配合 -ffunction-sections 才有粒度)。
GC_SECTIONS = -Wl,--gc-sections
ifeq ($(strip $(LTO)), 1)
# LTO 自己就做跨编译单元的内联与去重,不需要再开 function-sections;
# 实测 `-flto` 与 `-ffunction-sections -fdata-sections` 同时打开会让
# GCC 15.2.0 在 `binds_to_current_def_p (symtab.cc:2598)` 处 ICE。
FUNC_SECTIONS =
CFLAGS += -flto
CXXFLAGS += -flto
# ⚠️ `-flto-partition=one` 是绕过上面那个 ICE 的关键:
# 默认的多分区 LTRANS 在解析 COMDAT 定义时必崩(同一份代码拆成 11 个分区稳定复现,
# 单分区从未复现)。代价是 LTRANS 只能串行,全量构建比非 LTO 慢约一倍。
LDFLAGS += -flto -flto-partition=one
# lto-wrapper 会丢弃 -Wa 的汇编清单并告警,故 LTO 下不生成 .lst。
ASM_LISTING =
ASM_LISTING_C =
else
FUNC_SECTIONS = -ffunction-sections -fdata-sections
ASM_LISTING = -Wa,-a,-ad,-ahlms=$(@:%.o=%.lst)
ASM_LISTING_C = -Wa,-a,-ad,-alms=$(@:%.o=%.lst)
endif
# ---------------------------------------------------------------------------
# C目标文件及索引目录关联
OBJECTS = $(notdir $(C_EXTERA_SOURCES:.c=.o))
OBJECTS += $(subst .c,.o,$(notdir $(foreach path,$(C_SOURCES_PATHS),$(wildcard $(path)/*.c))))
vpath %.c $(sort $(dir $(C_EXTERA_SOURCES))) $(sort $(C_SOURCES_PATHS))
# C++目标文件及索引目录关联
OBJECTS += $(notdir $(CXX_EXTERA_SOURCES:.cpp=.o))
OBJECTS += $(subst .cpp,.o,$(notdir $(foreach path,$(CXX_SOURCES_PATHS),$(wildcard $(path)/*.cpp))))
vpath %.cpp $(sort $(dir $(CXX_EXTERA_SOURCES))) $(sort $(CXX_SOURCES_PATHS))
# Windows资源文件及索引目录关联
OBJECTS += $(notdir $(WIN_RESOURCE_SCRIPTS:.rc=.o))
vpath %.rc $(sort $(dir $(WIN_RESOURCE_SCRIPTS)))
# Release 目标文件
RELEASE_OBJECTS = $(addprefix $(BUILD_DIR)/$(SUB_DIR_RELEASE)/,$(OBJECTS))
# Debug 目标文件
DEBUG_OBJECTS = $(addprefix $(BUILD_DIR)/$(SUB_DIR_DEBUG)/,$(OBJECTS))
##################################################################################
# all任务 目标为 release 和 debug 产物
all: $(BUILD_DIR)/$(SUB_DIR_RELEASE)/$(TARGET_FILE_NAME).exe $(BUILD_DIR)/$(SUB_DIR_DEBUG)/$(TARGET_FILE_NAME).exe
@echo ====== [All] Build Procedure Accomplished ======
# release任务 目标为 release 产物
release: $(BUILD_DIR)/$(SUB_DIR_RELEASE)/$(TARGET_FILE_NAME).exe
@echo ====== [Release] Build Procedure Accomplished ======
# debug任务 目标为 debug 产物
debug: $(BUILD_DIR)/$(SUB_DIR_DEBUG)/$(TARGET_FILE_NAME).exe
@echo ====== [Debug] Build Procedure Accomplished ======
# 清理任务
clean: $(BUILD_DIR)
powershell rm -r $(BUILD_DIR)
# 构建目录生成
$(BUILD_DIR):
powershell mkdir $@
# Release 工作目录生成
$(BUILD_DIR)/$(SUB_DIR_RELEASE): | $(BUILD_DIR)
powershell mkdir $@
# Debug 工作目录生成
$(BUILD_DIR)/$(SUB_DIR_DEBUG): | $(BUILD_DIR)
powershell mkdir $@
# Release 最终可执行文件编译任务
$(BUILD_DIR)/$(SUB_DIR_RELEASE)/$(TARGET_FILE_NAME).exe: $(RELEASE_OBJECTS)
@echo ====== [Release] All File Compiled. Now Linking... ======
$(CXX_COMPLIER) $(RELEASE_OBJECTS) -static -s -o $@ $(LIB_LINK) $(LDFLAGS)
@echo ====== [Release] Program Link Finished ======
# Debug 最终可执行文件编译任务
$(BUILD_DIR)/$(SUB_DIR_DEBUG)/$(TARGET_FILE_NAME).exe: $(DEBUG_OBJECTS)
@echo ====== [Debug] All File Compiled. Now Linking... ======
$(CXX_COMPLIER) $(DEBUG_OBJECTS) -static -g -o $@ $(LIB_LINK) $(LDFLAGS)
@echo ====== [Debug] Program Link Finished ======
# C Release 目标文件编译
$(BUILD_DIR)/$(SUB_DIR_RELEASE)/%.o: %.c Makefile | $(BUILD_DIR)/$(SUB_DIR_RELEASE)
@echo ====== [Release] C Source File "$<" Compiling... ======
$(C_COMPLIER) -c $(CFLAGS) $(RELEASE_OPT) $(RELEASE_ONLY_OPT) $(ASM_LISTING_C) $< -o $@
# C++ Release 目标文件编译
$(BUILD_DIR)/$(SUB_DIR_RELEASE)/%.o: %.cpp Makefile | $(BUILD_DIR)/$(SUB_DIR_RELEASE)
@echo ====== [Release] C++ Source File "$<" Compiling... ======
$(CXX_COMPLIER) -c $(CXXFLAGS) $(RELEASE_OPT) $(RELEASE_ONLY_OPT) $(ASM_LISTING) $< -o $@
# Release 资源脚本文件编译
$(BUILD_DIR)/$(SUB_DIR_RELEASE)/%.o: %.rc Makefile | $(BUILD_DIR)/$(SUB_DIR_RELEASE)
@echo ====== [Release] Windows Resource Script File "$<" Compiling... ======
$(WIN_RES_COMPLIER) -c $(WIN_RESOURCE_CODEPAGE) $(foreach path,$(WIN_RESOURCE_INCLUDES_PATHS),$(addprefix -I,$(path))) $< -o $@
# C Debug 目标文件编译
# ⚠️ 2026-11-04 审查修正:原来这里误用了 `$(RELEASE_OPT)`,导致 `DEBUG_OPT = -O0`
# **从未生效**(Debug 实际上也是 -Oz,且不带 `-g`,根本无法调试)。
$(BUILD_DIR)/$(SUB_DIR_DEBUG)/%.o: %.c Makefile | $(BUILD_DIR)/$(SUB_DIR_DEBUG)
@echo ====== [Debug] C Source File "$<" Compiling... ======
$(C_COMPLIER) -c $(CFLAGS) $(DEBUG_OPT) -g $(ASM_LISTING_C) $< -o $@
# C++ Debug 目标文件编译
$(BUILD_DIR)/$(SUB_DIR_DEBUG)/%.o: %.cpp Makefile | $(BUILD_DIR)/$(SUB_DIR_DEBUG)
@echo ====== [Debug] C++ Source File "$<" Compiling... ======
$(CXX_COMPLIER) -c $(CXXFLAGS) $(DEBUG_OPT) -g $(ASM_LISTING) $< -o $@
# Debug 资源脚本文件编译
$(BUILD_DIR)/$(SUB_DIR_DEBUG)/%.o: %.rc Makefile | $(BUILD_DIR)/$(SUB_DIR_DEBUG)
@echo ====== [Debug] Windows Resource Script File "$<" Compiling... ======
$(WIN_RES_COMPLIER) -c $(WIN_RESOURCE_CODEPAGE) $(foreach path,$(WIN_RESOURCE_INCLUDES_PATHS),$(addprefix -I,$(path))) $< -o $@
# 依赖关系
-include $(RELEASE_OBJECTS:.o=.d)
-include $(DEBUG_OBJECTS:.o=.d)