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:
1 parent
abcf3e6c44
commit
34365b3c1c
23 files changed
+256
-94
No files matched your search
@@ -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
|
||||
|
||||
@@ -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:" 前缀)。
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
else
|
||||
{
|
||||
std::swprintf(buffer, 64, L"%g", value);
|
||||
return FormatInteger(static_cast<long long>(value));
|
||||
}
|
||||
// 浮点分支必须保留 `%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();
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user