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**。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-11-04 14:31:02 +08:00
1 parent abcf3e6c44
commit 34365b3c1c
23 files changed
+256 -94

No files matched your search

+49
View File
@@ -286,6 +286,55 @@ JDI_TEST(text_trim_and_utf8_conversion)
JDI_CHECK(!jdi::ContainsInsensitive(L"HelloWorld", L"xyz"));
}
/**
* @brief `FormatInteger*` 必须与 `std::to_wstring` / `std::to_string` 语义**完全一致**。
*
* 这组函数替代了全仓 76 + 2 处标准库转换,目的是让产物不再依赖
* libstdc++ 的 `to_string`(它内部转调 `__mingw_swprintf`)。任何一位差异都会
* 直接改变界面文本,因此按「逐字符 + 与标准库对照」双重判定。
*/
JDI_TEST(integer_replacement_matches_std)
{
static const long long samples[] = {0,
1,
-1,
9,
10,
99,
100,
-100,
1234567890123ll,
-1234567890123ll,
0x7FFFFFFFFFFFFFFFll, // LLONG_MAX
(-0x7FFFFFFFFFFFFFFFll) - 1ll}; // LLONG_MIN
for (const long long value : samples)
{
const std::wstring wide = jdi::FormatInteger(value);
const std::wstring reference = std::to_wstring(value);
JDI_CHECK_WIDE_EQ(wide, reference);
// 窄版本:内容必须与宽版本逐字符相同(只是字符宽度不同)
const std::string narrow = jdi::FormatIntegerNarrow(value);
JDI_CHECK_EQ(static_cast<long long>(narrow.size()), static_cast<long long>(wide.size()));
bool same = (narrow.size() == wide.size());
for (std::size_t index = 0; same && index < narrow.size(); ++index)
{
same = (narrow[index] == static_cast<char>(wide[index]));
}
JDI_CHECK(same);
}
// 无符号版本(`size_t` 常走这条):覆盖 ULLONG_MAX
static const unsigned long long unsignedSamples[] = {0ull, 1ull, 42ull, 4294967295ull,
18446744073709551615ull};
for (const unsigned long long value : unsignedSamples)
{
const std::wstring ours = jdi::FormatIntegerUnsigned(value);
const std::wstring reference = std::to_wstring(value);
JDI_CHECK_WIDE_EQ(ours, reference);
}
}
JDI_TEST(ticks_nice_step_and_build_ticks)
{
// `NiceStep` 必须落在 1/2/2.5/5 × 10^n 这类"好读"的格点上