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
+255 -93

No files matched your search

+3 -3
View File
@@ -265,7 +265,7 @@ namespace jdi
// 取当前未被占用的最小编号,使关闭标签页后编号可以被复用(3 开 2 关再新建回到「视图 2」)。
for (int number = 1; number <= static_cast<int>(m_tabs.size()) + 1; ++number)
{
const std::wstring candidate = L"视图 " + std::to_wstring(number);
const std::wstring candidate = L"视图 " + FormatInteger(number);
bool used = false;
for (const TabConfig& tab : m_tabs)
{
@@ -280,7 +280,7 @@ namespace jdi
return candidate;
}
}
return L"视图 " + std::to_wstring(m_tabs.size() + 1);
return L"视图 " + FormatInteger(m_tabs.size() + 1);
}
void Application::Relayout()
@@ -1879,7 +1879,7 @@ namespace jdi
if (loaded > 0)
{
SetStatusMessage(L"已加载 " + std::to_wstring(loaded) +
SetStatusMessage(L"已加载 " + FormatInteger(loaded) +
L" 个文件,请在左侧勾选需要查看的信号。",
false);
}
@@ -90,7 +90,7 @@ namespace jdi
}
for (int suffix = 2; suffix < 1000; ++suffix)
{
const std::wstring candidate = base + L" (" + std::to_wstring(suffix) + L")";
const std::wstring candidate = base + L" (" + FormatInteger(suffix) + L")";
if (!taken(candidate))
{
return candidate;
@@ -319,7 +319,7 @@ namespace jdi
if (loaded > 0)
{
SetStatusMessage(L"已加载 " + std::to_wstring(loaded) +
SetStatusMessage(L"已加载 " + FormatInteger(loaded) +
L" 个文件,请在左侧勾选需要查看的信号。",
false);
}
@@ -59,14 +59,14 @@ namespace jdi
{
ComputeWeakPeakAdjust(source.values, static_cast<double>(request.amountPercent) / 100.0,
request.iterations, derived_values);
derived.name = baseName + L" (弱峰调整 " + std::to_wstring(request.amountPercent) + L"% " +
std::to_wstring(request.iterations) + L"次迭代)";
derived.name = baseName + L" (弱峰调整 " + FormatInteger(request.amountPercent) + L"% " +
FormatInteger(request.iterations) + L"次迭代)";
}
else
{
ComputeMovingAverageIterated(source.values, request.points, request.iterations, derived_values);
derived.name = baseName + L" (" + std::to_wstring(request.points) + L"点均值 " +
std::to_wstring(request.iterations) + L"次迭代)";
derived.name = baseName + L" (" + FormatInteger(request.points) + L"点均值 " +
FormatInteger(request.iterations) + L"次迭代)";
}
// 2. 组装新信号(外观一律用默认值:变换已经烘进采样数据,不再是"继承的指纹")。
@@ -654,7 +654,7 @@ namespace jdi
}
if (built > 0)
{
SetStatusMessage(L"已构建 " + std::to_wstring(built) + L" 条重采样信号(频率 " +
SetStatusMessage(L"已构建 " + FormatInteger(built) + L" 条重采样信号(频率 " +
FormatFrequency(lastInterval > 0.0 ? 1.0 / lastInterval : 0.0) +
L")。",
false);
@@ -692,7 +692,7 @@ namespace jdi
if (built > 0)
{
SetStatusMessage((items.front().destructive ? L"已截取 " : L"已截取为新信号 ") +
std::to_wstring(built) + L" 条。",
FormatInteger(built) + L" 条。",
false);
}
return built > 0;
@@ -770,16 +770,16 @@ namespace jdi
if (built > 0)
{
std::wstring text = L"已构建 " + std::to_wstring(built) + L" 条 ";
std::wstring text = L"已构建 " + FormatInteger(built) + L" 条 ";
if (last.weakPeak)
{
text += L"弱峰调整 " + std::to_wstring(last.amountPercent) + L"% " +
std::to_wstring(last.iterations) + L"次迭代信号。";
text += L"弱峰调整 " + FormatInteger(last.amountPercent) + L"% " +
FormatInteger(last.iterations) + L"次迭代信号。";
}
else
{
text += std::to_wstring(last.points) + L"点均值 " +
std::to_wstring(last.iterations) + L"次迭代滤波信号。";
text += FormatInteger(last.points) + L"点均值 " +
FormatInteger(last.iterations) + L"次迭代滤波信号。";
}
SetStatusMessage(text, false);
}
@@ -76,10 +76,10 @@ namespace jdi
timeNames += axis.name;
}
std::wstring message =
L"已加载 组" + std::to_wstring(lastGroupIndex + 1) + L"|" + loadedGroup->name +
L"已加载 组" + FormatInteger(lastGroupIndex + 1) + L"|" + loadedGroup->name +
L"(" + FormatCount(static_cast<unsigned long long>(rowCount)) + L" 行采样," +
std::to_wstring(loadedGroup->signals.size()) + L" 条信号," +
std::to_wstring(loadedGroup->axes.size()) + L" 条时间轴:" + timeNames + L")";
FormatInteger(loadedGroup->signals.size()) + L" 条信号," +
FormatInteger(loadedGroup->axes.size()) + L" 条时间轴:" + timeNames + L")";
if (anyNonMonotonic)
{
message += L" 提示:时间列非单调递增,光标取值使用最近邻策略。";
@@ -280,7 +280,7 @@ namespace jdi
if (loaded > 0)
{
SetStatusMessage(L"已加载 " + std::to_wstring(loaded) +
SetStatusMessage(L"已加载 " + FormatInteger(loaded) +
L" 个文件,请在左侧勾选需要查看的信号。",
false);
}
@@ -22,7 +22,7 @@ namespace jdi
MenuItem header;
header.kind = MenuItemKind::HEADER;
header.label = L"网格 " + std::to_wstring(laneIndex + 1);
header.label = L"网格 " + FormatInteger(laneIndex + 1);
items.push_back(header);
// 当前网格的 Y 轴上下限值直接在菜单内的输入框里显示,单击即可就地编辑(菜单不关闭)。
@@ -64,7 +64,7 @@ namespace jdi
MenuItem splitLane;
const std::size_t laneSignalCount = m_plot.LaneSignalCount(laneIndex);
splitLane.label = (laneSignalCount > 1)
? (L"拆分当前网格(" + std::to_wstring(laneSignalCount) + L"个信号)")
? (L"拆分当前网格(" + FormatInteger(laneSignalCount) + L"个信号)")
: L"拆分当前网格";
splitLane.id = PLOT_CMD_LANE_SPLIT;
splitLane.enabled = laneSignalCount > 1;
+3 -3
View File
@@ -753,7 +753,7 @@ namespace jdi
*/
inline std::wstring EdgeRisingText(std::size_t rising, bool complete)
{
std::wstring text = L"上升沿 " + std::to_wstring(rising);
std::wstring text = L"上升沿 " + FormatInteger(rising);
if (!complete)
{
text += L" …";
@@ -768,7 +768,7 @@ namespace jdi
*/
inline std::wstring EdgeFallingText(std::size_t falling, bool complete)
{
std::wstring text = L"下降沿 " + std::to_wstring(falling);
std::wstring text = L"下降沿 " + FormatInteger(falling);
if (!complete)
{
text += L" …";
@@ -792,7 +792,7 @@ namespace jdi
*/
inline std::wstring EdgeCountCellText(std::size_t value, bool complete)
{
std::wstring text = std::to_wstring(value);
std::wstring text = FormatInteger(value);
if (!complete)
{
text += L" …";
+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;
+2 -2
View File
@@ -519,7 +519,7 @@ namespace jdi
case EditField::B:
{
const int index = static_cast<int>(field) - 1;
m_edit.Begin(std::to_wstring(ToByte(m_channels[static_cast<std::size_t>(index)])), true,
m_edit.Begin(FormatInteger(ToByte(m_channels[static_cast<std::size_t>(index)])), true,
EditFilter::BYTE, 0u);
break;
}
@@ -1069,7 +1069,7 @@ namespace jdi
}
else
{
UiText(g, std::to_wstring(ToByte(m_channels[slot])), m_fonts->monoSmall, m_values[slot],
UiText(g, FormatInteger(ToByte(m_channels[slot])), m_fonts->monoSmall, m_values[slot],
m_theme->text, TextAlign::RIGHT);
}
}
+2 -2
View File
@@ -1398,7 +1398,7 @@ namespace jdi
std::wstring SignalPanel::GroupLabelText(int groupIndex)
{
return L"组" + std::to_wstring(groupIndex + 1);
return L"组" + FormatInteger(groupIndex + 1);
}
std::wstring SignalPanel::GroupStatsText(int row) const
@@ -1409,7 +1409,7 @@ namespace jdi
}
// 项数:普通模式 = 信号总数;筛选模式 = 该组内**符合筛选条件**的信号个数
//(用户 2026-10-16 要求)。两者都在 `BuildRows` 里写入 `displayCount`。
return std::to_wstring(m_rows[static_cast<std::size_t>(row)].displayCount) + L" 项";
return FormatInteger(m_rows[static_cast<std::size_t>(row)].displayCount) + L" 项";
}
gd::RectF SignalPanel::FilterFieldRect() const
@@ -171,7 +171,7 @@ namespace jdi
{
return false;
}
initial = std::to_wstring(static_cast<int>(style->opacity * 100.0f + 0.5f));
initial = FormatInteger(static_cast<int>(style->opacity * 100.0f + 0.5f));
break;
}
case StyleEditField::SIGNAL_NAME:
@@ -250,7 +250,7 @@ namespace jdi
MenuItem entry;
// ⚠️ 显示形式为「组号 + 竖线 + 组名」(用户 2026-11-03 明确要求),
// **不要**用括号把组名括起来(旧实现 `分组 1(名称)` 已被要求改掉)。
entry.label = std::to_wstring(index + 1) + L" | " + group->name;
entry.label = FormatInteger(index + 1) + L" | " + group->name;
entry.id = CMD_GROUP_MOVE_BASE + static_cast<int>(index);
entry.checked = (static_cast<int>(index) == ref.groupIndex);
moveMenu.submenu.push_back(entry);
@@ -350,8 +350,8 @@ namespace jdi
for (std::size_t index = 0; index < lanes.size(); ++index)
{
MenuItem lane;
lane.label = L"网格 " + std::to_wstring(index + 1) + L"(" +
std::to_wstring(lanes[index].signals.size()) + L" 条信号)";
lane.label = L"网格 " + FormatInteger(index + 1) + L"(" +
FormatInteger(lanes[index].signals.size()) + L" 条信号)";
lane.id = CMD_LANE_BASE + static_cast<int>(index);
lane.checked = (currentLane == static_cast<int>(index));
items.push_back(lane);
@@ -81,8 +81,8 @@ namespace jdi
std::wstring title = L"信号";
if (m_dataSet != nullptr && m_tab != nullptr)
{
title += L" · " + std::to_wstring(m_tab->VisibleCount()) + L"/" +
std::to_wstring(TotalSignalCount()) + L" 已选";
title += L" · " + FormatInteger(m_tab->VisibleCount()) + L"/" +
FormatInteger(TotalSignalCount()) + L" 已选";
}
gd::RectF titleRect(m_headerRect.X + kPadding * m_scale, m_headerRect.Y,
@@ -751,7 +751,7 @@ namespace jdi
badgeHot ? m_theme->accentSoft : m_theme->cardBg);
UiDrawRoundRect(g, badgeRect, kBadgeHeight * m_scale * 0.5f,
badgeHot ? m_theme->accent : m_theme->border, 1.0f);
PaintRowText(g, L"G" + std::to_wstring(m_tab->LaneOfSignal(ref) + 1), m_fonts->monoSmall,
PaintRowText(g, L"G" + FormatInteger(m_tab->LaneOfSignal(ref) + 1), m_fonts->monoSmall,
badgeRect, badgeHot ? m_theme->textStrong : m_theme->text, TextAlign::CENTER);
}
@@ -992,7 +992,7 @@ namespace jdi
else
{
const int percent = static_cast<int>(style->opacity * 100.0f + 0.5f);
UiText(g, std::to_wstring(percent) + L"%", m_fonts->monoSmall, m_opacityValueRect,
UiText(g, FormatInteger(percent) + L"%", m_fonts->monoSmall, m_opacityValueRect,
m_theme->textStrong, TextAlign::RIGHT);
}
paintStyleReset(m_opacityResetRect, StyleHit::OPACITY_RESET,
@@ -1059,7 +1059,7 @@ namespace jdi
badgeHot ? m_theme->accentSoft : m_theme->cardBg);
UiDrawRoundRect(g, m_targetBadgeRect, kBadgeHeight * m_scale * 0.5f,
badgeHot ? m_theme->accent : m_theme->border, 1.0f);
UiText(g, L"G" + std::to_wstring(m_targetGeomLane + 1), m_fonts->monoSmall,
UiText(g, L"G" + FormatInteger(m_targetGeomLane + 1), m_fonts->monoSmall,
m_targetBadgeRect, badgeHot ? m_theme->textStrong : m_theme->text, TextAlign::CENTER);
}
}
+23 -10
View File
@@ -209,17 +209,30 @@ endif
# * ⚠️ 全关(`-fno-unwind-tables`)会链接失败(历史踩坑),**不要**打开。
RELEASE_ONLY_OPT = -fno-asynchronous-unwind-tables
# ---- 曾评估并放弃:把 `swprintf` 交给系统 UCRT(`-D__USE_MINGW_ANSI_STDIO=0`)----
# ---- 关于「把 `swprintf` 交给系统 UCRT」:**在 C++ 里做不到**,别再试 ----
# 动机:MinGW 的 ANSI stdio(`mingw_wpformat.o`)会链入 gdtoa 整套
#(`strtodg`/`gdtoa`/`gethex`/`hexnan`),map 实测 `.text` 合计 ≈33 KB。
# 实测结论(2026-11-04):**加这个宏体积一个字节都不减**。
# 原因:预编译的 libstdc++ 自己就引用 `__mingw_swprintf` ——
# 只要工程里还调用 `std::to_wstring` / `std::to_string`(全仓约 80 处),
# 链接器就必须把 MinGW 的 printf 引擎拉进来(用 3 行最小程序 `std::to_wstring(5)` 实测)。
# ⇒ 要真省下这 33 KB,得把 80 处 `std::to_*string` 全换成自写整数格式化,
# 其中若干处实参是浮点(语义会变),风险与收益不匹配。
# 保留证据:`Tools/UnitTests` 的 `stdio_mingw_vs_ucrt_formats`(13.6 万次调用逐字符比对,0 差异)
# 证明两套 printf 在这些格式上**完全等价**,将来若真要做迁移,那条测试就是验收标准。
# (`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
+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 这类"好读"的格点上
+1 -1
View File
@@ -463,7 +463,7 @@ namespace jdi
for (std::size_t index = 0; index < lanes.size(); ++index)
{
PruneLaneSignalItems(lanes[index]);
lanes[index].name = L"网格 " + std::to_wstring(index + 1);
lanes[index].name = L"网格 " + FormatInteger(index + 1);
}
}
} // namespace dmdetail
+34
View File
@@ -129,6 +129,40 @@ namespace jdi
*/
std::wstring FormatCount(unsigned long long value);
/**
* @brief 把整数转换为**纯十进制**宽字符串(`std::to_wstring` 的替代品)。
*
* ⚠️ 为什么不用 `std::to_wstring`:预编译的 libstdc++ 里它转调 `__mingw_swprintf`,
* 会把 MinGW 的 printf 引擎(`mingw_wpformat.o`)连同 gdtoa 整套
* (`strtodg` / `gdtoa` / `gethex` / `hexnan`)一起拉进产物,map 实测 `.text`
* 合计 ≈33 KB。只要工程里还有一处 `std::to_wstring` / `std::to_string`,
* 这条依赖链就断不掉(本轮实测:只把 `swprintf` 让给 UCRT 是 **0 字节**收益)。
*
* 本实现手写十进制展开:无本地化、无格式串解析、无堆分配(除返回的字符串本身),
* 语义与 `std::to_wstring` 完全一致(`-0` → `"0"`,最小值也能正确取绝对值)。
*
* @param [IN] value 待转换整数(含符号)。
* @return 十进制文本,例如 `-123`。
*/
std::wstring FormatInteger(long long value);
/**
* @brief 把无符号整数转换为纯十进制宽字符串(避免 `size_t` 走有符号重载的隐式转换告警)。
* @param [IN] value 待转换整数。
* @return 十进制文本。
*/
std::wstring FormatIntegerUnsigned(unsigned long long value);
/**
* @brief 把整数转换为纯十进制**窄**字符串(`std::to_string` 的替代品)。
*
* 只在构造 HDF5 成员路径时使用(`MatLoader`),语义同 `FormatInteger`。
*
* @param [IN] value 待转换整数。
* @return 十进制文本。
*/
std::string FormatIntegerNarrow(long long value);
/**
* @brief 把时间间隔格式化为「Δt 语义」的复合单位文本(不含 "Δt:" 前缀)。
*
+1 -1
View File
@@ -276,7 +276,7 @@ namespace jdi
std::wstring text = FieldToText(span);
if (text.empty())
{
text = L"Column" + std::to_wstring(table.headers.size() + 1);
text = L"Column" + FormatInteger(table.headers.size() + 1);
}
table.headers.push_back(text);
}
+1 -1
View File
@@ -351,7 +351,7 @@ namespace jdi
signal.name = Trim(table.headers[column]);
if (signal.name.empty())
{
signal.name = L"Column" + std::to_wstring(column + 1);
signal.name = L"Column" + FormatInteger(column + 1);
}
signal.axisIndex = axisIndex;
+3 -3
View File
@@ -609,7 +609,7 @@ namespace jdi
for (std::size_t index = 0; index < m_lanes.size(); ++index)
{
m_lanes[index].name = L"网格 " + std::to_wstring(index + 1);
m_lanes[index].name = L"网格 " + FormatInteger(index + 1);
}
return LaneOfSignal(ref);
}
@@ -743,7 +743,7 @@ namespace jdi
// 统一重命名,保证编号连续(GX 编号在各显示位置均由此位置推导)。
for (std::size_t index = 0; index < m_lanes.size(); ++index)
{
m_lanes[index].name = L"网格 " + std::to_wstring(index + 1);
m_lanes[index].name = L"网格 " + FormatInteger(index + 1);
}
return laneIndex;
}
@@ -876,7 +876,7 @@ namespace jdi
{
for (std::size_t index = 0; index < m_lanes.size(); ++index)
{
m_lanes[index].name = L"网格 " + std::to_wstring(index + 1);
m_lanes[index].name = L"网格 " + FormatInteger(index + 1);
}
}
+10 -8
View File
@@ -6,6 +6,7 @@
#include "MatLoader.hpp"
#include "MatH5.hpp"
#include "Utf8Util.hpp"
#include <algorithm>
#include <cmath>
@@ -34,15 +35,16 @@ namespace jdi
std::wstring TrimNumber(double value)
{
wchar_t buffer[64] = {};
// ⚠️ 整数分支改走 `FormatInteger`(不再经 `swprintf`):本文件是产物里
// 最后一个引用 MinGW printf 引擎的地方,留着它 gdtoa 就断不掉。
if (value == static_cast<double>(static_cast<long long>(value)))
{
std::swprintf(buffer, 64, L"%lld", static_cast<long long>(value));
return FormatInteger(static_cast<long long>(value));
}
else
{
// 浮点分支必须保留 `%g` 语义(去掉尾随 0 的最短表示),因此仍走 `swprintf`;
// 此时库函数来自系统 UCRT(见 Makefile 的 `UCRT_STDIO` 开关)。
wchar_t buffer[64] = {};
std::swprintf(buffer, 64, L"%g", value);
}
return buffer;
}
@@ -341,7 +343,7 @@ namespace jdi
signal.signalName = element.name;
if (signal.signalName.empty())
{
signal.signalName = L"信号" + std::to_wstring(++unnamedSignals);
signal.signalName = L"信号" + FormatInteger(++unnamedSignals);
}
signal.timeStream = element.timeStream;
signal.dataStream = element.dataStream;
@@ -385,7 +387,7 @@ namespace jdi
// 数据列:直接解码到最终 vector(不再经过 `Dataset.data` 的整块字节)。
std::vector<double> values;
const std::uint64_t dataObject = file.Member(sigstream, std::to_string(signal.dataStream));
const std::uint64_t dataObject = file.Member(sigstream, FormatIntegerNarrow(signal.dataStream));
if (dataObject == 0 || !ReadStream(file, dataObject, values) || values.empty())
{
if (!reportStream())
@@ -408,7 +410,7 @@ namespace jdi
if (signal.timeStream >= 0)
{
const std::uint64_t timeObject =
file.Member(sigstream, std::to_string(signal.timeStream));
file.Member(sigstream, FormatIntegerNarrow(signal.timeStream));
haveTime = timeObject != 0 && ReadStream(file, timeObject, time) &&
!time.empty();
}
+6 -6
View File
@@ -1285,13 +1285,13 @@ namespace jdi
for (const std::size_t value : axisIndex)
{
suffix += L"[";
suffix += std::to_wstring(value);
suffix += FormatInteger(value);
suffix += L"]";
}
}
else
{
suffix = L"[" + std::to_wstring(index) + L"]";
suffix = L"[" + FormatInteger(index) + L"]";
}
element.name = spec.name + suffix;
out.push_back(std::move(element));
@@ -2018,7 +2018,7 @@ namespace jdi
std::wstring name = group.channels[c].name;
if (name.empty())
{
name = L"Channel" + std::to_wstring(c + 1);
name = L"Channel" + FormatInteger(c + 1);
}
block.table.headers.push_back(name);
block.table.columns.push_back(std::move(group.values[c]));
@@ -2045,15 +2045,15 @@ namespace jdi
std::wstring detail;
if (skippedNoLayout > 0)
{
detail += L"数据块布局无法解析 " + std::to_wstring(skippedNoLayout) + L" 组;";
detail += L"数据块布局无法解析 " + FormatInteger(skippedNoLayout) + L" 组;";
}
if (skippedNoAxis > 0)
{
detail += L"缺少可用时间通道 " + std::to_wstring(skippedNoAxis) + L" 组;";
detail += L"缺少可用时间通道 " + FormatInteger(skippedNoAxis) + L" 组;";
}
if (skippedEmpty > 0)
{
detail += L"空数据组 " + std::to_wstring(skippedEmpty) + L" 组;";
detail += L"空数据组 " + FormatInteger(skippedEmpty) + L" 组;";
}
error = L"未在文件中找到可绘制的数值通道。";
if (!detail.empty())
+65 -1
View File
@@ -527,7 +527,7 @@ namespace jdi
{
text += L" ";
}
text += std::to_wstring(amount);
text += FormatInteger(amount);
text += unit;
};
append(hours, L"h");
@@ -832,6 +832,70 @@ namespace jdi
return result;
}
/**
* @brief 把整数展开成十进制数字(内部实现,宽/窄共用同一套算法)。
*
* ⚠️ 这里**不能**用 `swprintf`:本函数存在的全部意义就是让产物不再引用
* MinGW 的 printf 引擎(见头文件注释)。手写 10 次除法比一次格式串解析还快,
* 且无本地化影响。
*
* @param [OUT] buffer 输出缓冲(从尾部向前写)。
* @param [IN] value 待转换整数。
* @return 文本起始指针(指向 buffer 内部)。
*/
template <typename Char>
static const Char* ExpandInteger(Char* buffer, std::size_t capacity, long long value,
std::size_t& length)
{
Char* cursor = buffer + capacity;
const bool negative = (value < 0);
// `0 - (unsigned long long)value` 对 LLONG_MIN 也成立(模 2^64 语义)。
unsigned long long magnitude = negative ? (0ull - static_cast<unsigned long long>(value))
: static_cast<unsigned long long>(value);
do
{
*--cursor = static_cast<Char>('0' + static_cast<int>(magnitude % 10ull));
magnitude /= 10ull;
} while (magnitude != 0ull);
if (negative)
{
*--cursor = static_cast<Char>('-');
}
length = static_cast<std::size_t>((buffer + capacity) - cursor);
return cursor;
}
std::wstring FormatInteger(long long value)
{
wchar_t buffer[24]; // -9223372036854775808 = 20 字符 + 余量
std::size_t length = 0;
const wchar_t* begin = ExpandInteger(buffer, sizeof(buffer) / sizeof(buffer[0]), value, length);
return std::wstring(begin, length);
}
std::wstring FormatIntegerUnsigned(unsigned long long value)
{
// 上限 20 位十进制数字 + 余量(用有符号重载会溢出,故单独展开一遍)。
wchar_t buffer[24];
wchar_t* cursor = buffer + (sizeof(buffer) / sizeof(buffer[0]));
unsigned long long magnitude = value;
do
{
*--cursor = static_cast<wchar_t>(L'0' + static_cast<int>(magnitude % 10ull));
magnitude /= 10ull;
} while (magnitude != 0ull);
return std::wstring(cursor, static_cast<std::size_t>(buffer + (sizeof(buffer) / sizeof(buffer[0])) -
cursor));
}
std::string FormatIntegerNarrow(long long value)
{
char buffer[24];
std::size_t length = 0;
const char* begin = ExpandInteger(buffer, sizeof(buffer) / sizeof(buffer[0]), value, length);
return std::string(begin, length);
}
double NiceStep(double range, int targetTicks)
{
if (!(range > 0.0) || !std::isfinite(range) || targetTicks <= 0)