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

+11 -11
View File
@@ -2380,7 +2380,7 @@ namespace jdi
m_dragMode = PlotDragMode::NONE;
if (ScrollLaneIntoView(index))
{
SetHint(L"正在滚动到 G" + std::to_wstring(index + 1) + L" 网格…");
SetHint(L"正在滚动到 G" + FormatInteger(index + 1) + L" 网格…");
}
return true;
}
@@ -2554,7 +2554,7 @@ namespace jdi
const ResampleBuild& first = m_pendingResampleItems.front();
SetHint(L"正在构建重采样信号(间隔 " +
FormatIntervalText(first.interval) + L",共 " +
std::to_wstring(m_pendingResampleItems.size()) + L" 条)…");
FormatInteger(m_pendingResampleItems.size()) + L" 条)…");
m_pendingResampleLane = index;
}
m_dragMode = PlotDragMode::NONE;
@@ -2632,10 +2632,10 @@ namespace jdi
{
SetHint(onDestructive
? L"正在按选区截取原始信号(" +
std::to_wstring(m_pendingClipItems.size()) +
FormatInteger(m_pendingClipItems.size()) +
L" 条,区间外数据将被丢弃)…"
: L"正在按选区截取为新信号(" +
std::to_wstring(m_pendingClipItems.size()) + L" 条)…");
FormatInteger(m_pendingClipItems.size()) + L" 条)…");
m_pendingClipLane = index;
}
m_dragMode = PlotDragMode::NONE;
@@ -2991,9 +2991,9 @@ namespace jdi
if (!m_pendingWeakPeakItems.empty())
{
const WeakPeakBuild& first = m_pendingWeakPeakItems.front();
SetHint(L"正在构建弱峰调整信号(幅度 " + std::to_wstring(first.amountPercent) +
L"% / " + std::to_wstring(first.iterations) + L" 次迭代,共 " +
std::to_wstring(m_pendingWeakPeakItems.size()) + L" 条)…");
SetHint(L"正在构建弱峰调整信号(幅度 " + FormatInteger(first.amountPercent) +
L"% / " + FormatInteger(first.iterations) + L" 次迭代,共 " +
FormatInteger(m_pendingWeakPeakItems.size()) + L" 条)…");
m_pendingWeakPeakLane = index;
}
m_dragMode = PlotDragMode::NONE;
@@ -3029,9 +3029,9 @@ namespace jdi
if (!m_pendingMeanFilterItems.empty())
{
const MeanFilterBuild& first = m_pendingMeanFilterItems.front();
SetHint(L"正在构建均值滤波信号(" + std::to_wstring(first.points) +
L" 点 " + std::to_wstring(first.iterations) + L" 次迭代,共 " +
std::to_wstring(m_pendingMeanFilterItems.size()) + L" 条)…");
SetHint(L"正在构建均值滤波信号(" + FormatInteger(first.points) +
L" 点 " + FormatInteger(first.iterations) + L" 次迭代,共 " +
FormatInteger(m_pendingMeanFilterItems.size()) + L" 条)…");
m_pendingMeanFilterLane = index;
}
m_dragMode = PlotDragMode::NONE;
@@ -3633,7 +3633,7 @@ namespace jdi
m_dragMode = PlotDragMode::NONE;
if (ScrollLaneIntoView(panelLane))
{
SetHint(L"正在滚动到 G" + std::to_wstring(panelLane + 1) + L" 网格…");
SetHint(L"正在滚动到 G" + FormatInteger(panelLane + 1) + L" 网格…");
}
return true;
}
+13 -13
View File
@@ -1461,7 +1461,7 @@ namespace jdi
// 因此不会出现直角白边。
UiFillRoundRect(g, badge, radius, m_theme->accentSoft);
UiDrawRoundRect(g, badge, radius, m_theme->accent, 1.0f);
UiText(g, L"G" + std::to_wstring(m_hoverBadge + 1), m_fonts->monoSmall, badge,
UiText(g, L"G" + FormatInteger(m_hoverBadge + 1), m_fonts->monoSmall, badge,
m_theme->textStrong, TextAlign::CENTER);
}
}
@@ -2556,7 +2556,7 @@ namespace jdi
{
UiFillRoundRect(g, badgeRect, badgeHeight * 0.5f, m_theme->cardBg);
UiDrawRoundRect(g, badgeRect, badgeHeight * 0.5f, m_theme->border, 1.0f);
UiText(g, L"G" + std::to_wstring(laneIndex + 1), m_fonts->monoSmall, badgeRect,
UiText(g, L"G" + FormatInteger(laneIndex + 1), m_fonts->monoSmall, badgeRect,
m_theme->text, TextAlign::CENTER);
for (const LegendItem& item : items)
@@ -2601,7 +2601,7 @@ namespace jdi
{
// 图例空间不足时提示剩余信号数量,避免误认为信号丢失。
const std::wstring moreText =
L"+" + std::to_wstring(visibleCount - static_cast<int>(items.size()));
L"+" + FormatInteger(visibleCount - static_cast<int>(items.size()));
UiText(g, moreText, m_fonts->uiTiny, moreTextRect, m_theme->textMuted, TextAlign::LEFT);
}
}
@@ -5106,7 +5106,7 @@ namespace jdi
badgeHot ? m_theme->accentSoft : m_theme->cardBg);
UiDrawRoundRect(g, badgeRect, badgeRect.Height * 0.5f,
badgeHot ? m_theme->accent : m_theme->border, badgeHot ? 1.4f : 1.0f);
UiText(g, L"G" + std::to_wstring(index + 1), m_fonts->monoSmall, badgeRect,
UiText(g, L"G" + FormatInteger(index + 1), m_fonts->monoSmall, badgeRect,
badgeHot ? m_theme->textStrong : m_theme->text, TextAlign::CENTER);
const std::wstring title =
meanFilter ? std::wstring(L"均值滤波")
@@ -5585,7 +5585,7 @@ namespace jdi
static_cast<double>(maximum - minimum))
: 0.0;
UiDrawSlider(g, track, ratio, m_theme->accent, active, m_scale);
UiTextClipped(g, std::to_wstring(current), m_fonts->monoSmall,
UiTextClipped(g, FormatInteger(current), m_fonts->monoSmall,
gd::RectF(valueRect.X, valueRect.Y, valueColumn, valueRect.Height),
m_theme->textStrong, TextAlign::LEFT, layout.content);
// 复位按钮:悬停高亮 + 与「系数/偏移」菜单同款图标。
@@ -5688,7 +5688,7 @@ namespace jdi
? layout.rowLevelResets[rowIndex]
: gd::RectF(),
kWeakPeakLevelLabel, levelRatio,
(peakAmount > 0 ? L"+" : L"") + std::to_wstring(peakAmount) + L"%",
(peakAmount > 0 ? L"+" : L"") + FormatInteger(peakAmount) + L"%",
levelActive, false);
}
if (rowIndex < layout.rowIterTracks.size() &&
@@ -5703,7 +5703,7 @@ namespace jdi
? layout.rowIterResets[rowIndex]
: gd::RectF(),
kWeakPeakIterationsSliderLabel, iterRatio,
std::to_wstring(peakIter), iterActive, editingMax);
FormatInteger(peakIter), iterActive, editingMax);
}
}
else
@@ -5831,7 +5831,7 @@ namespace jdi
++markerCount;
}
}
const std::wstring countText = std::to_wstring(markerCount) + L" 标记点";
const std::wstring countText = FormatInteger(markerCount) + L" 标记点";
const float countWidth =
UiMeasureTextWidth(m_fonts->monoSmall, countText) + 2.0f * m_scale;
const float countRight = group.close.X - 5.0f * m_scale;
@@ -5902,7 +5902,7 @@ namespace jdi
}
if (maxSerial > 0)
{
measure(EdgePanelLayout::MARKER_COL_SERIAL, std::to_wstring(maxSerial));
measure(EdgePanelLayout::MARKER_COL_SERIAL, FormatInteger(maxSerial));
}
for (const FreeMarkerContentEntry& entry : contents)
{
@@ -5993,7 +5993,7 @@ namespace jdi
UiTextClipped(g, text, m_fonts->monoSmall, columnRect(column), color,
TextAlign::LEFT, layout.content);
};
paintCell(EdgePanelLayout::MARKER_COL_SERIAL, std::to_wstring(markerRow.serial),
paintCell(EdgePanelLayout::MARKER_COL_SERIAL, FormatInteger(markerRow.serial),
m_theme->textMuted);
paintCell(EdgePanelLayout::MARKER_COL_VALUE, entry.valueText,
m_theme->textStrong);
@@ -6092,7 +6092,7 @@ namespace jdi
sliderActive, m_scale);
const int percent =
static_cast<int>(tabLane.freeMarkerOpacity * 100.0f + 0.5f);
UiText(g, std::to_wstring(percent) + L"%", m_fonts->monoSmall,
UiText(g, FormatInteger(percent) + L"%", m_fonts->monoSmall,
layout.opacityValue, m_theme->textStrong, TextAlign::RIGHT);
}
}
@@ -6244,7 +6244,7 @@ namespace jdi
: ((m_editTarget == PlotEditTarget::MEAN_POINTS_MAX)
? kMeanFilterPointsDefaultMax
: kMeanFilterIterationsDefaultMax));
const std::wstring hint = L"输入上限(当前" + std::to_wstring(currentMax) + L")";
const std::wstring hint = L"输入上限(当前" + FormatInteger(currentMax) + L")";
const float hintLeft = fieldRect.X + 11.0f * m_scale;
UiTextAtClipped(g, hint, m_fonts->monoSmall, hintLeft,
gd::RectF(hintLeft, fieldRect.Y,
@@ -6414,7 +6414,7 @@ namespace jdi
const std::wstring hint = (target == sourceLane)
? std::wstring(L"松开左键放回本网格")
: (L"移动到网格 " + std::to_wstring(target + 1) +
: (L"移动到网格 " + FormatInteger(target + 1) +
L":松开左键确认");
const float textWidth = UiMeasureTextWidth(m_fonts->uiSmall, hint) + 16.0f * m_scale;
const float height = 20.0f * m_scale;