Files
Just-DataInspector/Tools/UnitTests/Src/Utf8UtilTests.cpp
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

486 lines
15 KiB
C++
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.
/**
* @file Utf8UtilTests.cpp
* @brief `Utilities/Utf8Util` 的数值解析 / 文本格式化语义回归。
*
* 重点用例:
* * `parse.bit_exact_vs_strtod` —— **逐位**对比 `TryParseDouble` 与 libc `strtod`。
* 这是"自写 strtod 替身"类体积优化唯一可信的验收标准(见 Code Review 报告 §10.4):
* 常见写法(≤15 位有效数字、|十进制指数| ≤ 22)必须逐位相同;
* 17 位以上的长数字允许 ≤1 ULP 偏差(实现里已注释该取舍)。
* * `format.*` —— 只断言文档里给出的实例与"绝不出现科学计数法"这类硬约束,
* 避免把测试绑死在内部格式细节上。
*/
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cwchar>
#include <random>
#include <string>
#include <vector>
#include <stdio.h>
#include <wchar.h>
#include "UnitTestFramework.hpp"
#include "Utf8Util.hpp"
namespace
{
/** @brief 取 double 的位模式。 */
std::uint64_t BitsOf(const double value)
{
std::uint64_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
return bits;
}
/** @brief 两个 double 之间相差多少个 ULP(异号时返回一个很大的数)。 */
long long UlpDifference(const double left, const double right)
{
if (left == right)
{
return 0;
}
const std::uint64_t a = BitsOf(left);
const std::uint64_t b = BitsOf(right);
if ((a >> 63) != (b >> 63))
{
return 1000000;
}
return (a > b) ? static_cast<long long>(a - b) : -static_cast<long long>(b - a);
}
/** @brief 用 `TryParseDouble` 解析整段文本。 */
bool Parse(const std::string& text, double& value)
{
return jdi::TryParseDouble(text.c_str(), text.c_str() + text.size(), value);
}
/** @brief 统计十进制有效数字位数(忽略符号/小数点/前导零/指数部分)。 */
int SignificantDigits(const std::string& text)
{
int digits = 0;
bool sawNonZero = false;
for (const char character : text)
{
if (character == 'e' || character == 'E')
{
break;
}
if (character < '0' || character > '9')
{
continue;
}
if (character != '0')
{
sawNonZero = true;
}
if (sawNonZero)
{
++digits;
}
}
return digits;
}
/** @brief 生成 `数字.数字e指数` 形式的随机十进制文本。 */
std::string MakeRandomNumber(std::mt19937& rng, const int maxDigits, const bool allowFraction,
const bool allowExponent, const int maxExponent)
{
std::string text;
if ((rng() & 1u) != 0u)
{
text.push_back('-');
}
const int integerDigits = 1 + static_cast<int>(rng() % static_cast<unsigned>(maxDigits));
for (int index = 0; index < integerDigits; ++index)
{
text.push_back(static_cast<char>('0' + (rng() % 10u)));
}
if (allowFraction && (rng() % 3u) != 0u)
{
text.push_back('.');
const int fractionDigits = 1 + static_cast<int>(rng() % static_cast<unsigned>(maxDigits));
for (int index = 0; index < fractionDigits; ++index)
{
text.push_back(static_cast<char>('0' + (rng() % 10u)));
}
}
if (allowExponent && (rng() % 2u) == 0u)
{
text.push_back((rng() & 1u) != 0u ? 'e' : 'E');
const bool negativeExponent = (rng() & 1u) != 0u;
text.push_back(negativeExponent ? '-' : '+');
const int exponent = static_cast<int>(rng() % static_cast<unsigned>(maxExponent + 1));
const std::string digits = std::to_string(exponent);
text += digits;
}
return text;
}
} // namespace
JDI_TEST(parse_fixed_corpus_matches_strtod)
{
static const char* const corpus[] = {
"0", "1", "-1", "+1", "0.1", "-0.1", "1.5",
"123.456", "-123.456", "1e10", "1e-10", "1E+5", "1e22", "1e-22",
"3.141592653589793", "2.718281828459045", "6.02214076e23", "1.602176634e-19", "0.000001",
"1000000", "-0", "0.0", "123456789012345",
};
for (const char* const text : corpus)
{
double ours = 0.0;
const bool accepted = Parse(text, ours);
char* stop = nullptr;
const double reference = std::strtod(text, &stop);
const bool referenceAccepted = (stop != text);
JDI_REQUIRE(text, accepted == referenceAccepted);
if (accepted && referenceAccepted)
{
JDI_CHECK_BIT_EQUAL(ours, reference);
}
}
}
JDI_TEST(parse_acceptance_semantics)
{
double value = 0.0;
JDI_CHECK(!Parse("", value));
JDI_CHECK(!Parse(" ", value));
JDI_CHECK(!Parse("abc", value));
JDI_CHECK(!Parse("e5", value));
// 尾部杂字符:与 `strtod` 一致,接受前缀数值(历史语义,不要改)
JDI_CHECK(Parse("12abc", value));
JDI_CHECK_NEAR(value, 12.0, 0.0);
JDI_CHECK(Parse("1,5", value));
JDI_CHECK_NEAR(value, 1.0, 0.0);
// 首尾空白应被忽略
JDI_CHECK(Parse(" -2.5\t\r\n", value));
JDI_CHECK_NEAR(value, -2.5, 0.0);
// 十六进制浮点 / inf / nan 走标准库回退路径,语义必须与 strtod 相同
static const char* const exotic[] = {"0x1p3", "inf", "-inf", "nan", "infinity", "1e9999", "1e-9999"};
for (const char* const text : exotic)
{
double ours = 0.0;
char* stop = nullptr;
const double reference = std::strtod(text, &stop);
const bool referenceAccepted = (stop != text);
JDI_REQUIRE(text, Parse(text, ours) == referenceAccepted);
if (referenceAccepted)
{
JDI_CHECK_BIT_EQUAL(ours, reference);
}
}
}
JDI_TEST(parse_random_corpus_bit_exact)
{
std::mt19937 rng(20261104u);
int mismatches = 0;
int beyondOneUlp = 0;
int cases = 0;
// 常见写法:≤15 位有效数字、|指数| ≤ 22 ⇒ 必须逐位相同。
for (int iteration = 0; iteration < 40000; ++iteration)
{
const std::string text = MakeRandomNumber(rng, 6, true, true, 22);
double ours = 0.0;
char* stop = nullptr;
const double reference = std::strtod(text.c_str(), &stop);
if (stop == text.c_str())
{
continue;
}
++cases;
if (!Parse(text, ours))
{
++mismatches;
continue;
}
if (BitsOf(ours) != BitsOf(reference))
{
++mismatches;
if (SignificantDigits(text) <= 15 && std::fabs(reference) > 1e-300 &&
std::fabs(reference) < 1e300)
{
std::printf(" [INFO] bit mismatch on short literal: %s ours=%.17g ref=%.17g\n", text.c_str(),
ours, reference);
}
}
}
JDI_CHECK_EQ(cases, 40000);
JDI_CHECK_EQ(mismatches, 0);
// 长数字(最多 19 位):允许 ≤1 ULP 偏差(实现里有明确注释与容差约定)。
for (int iteration = 0; iteration < 20000; ++iteration)
{
const std::string text = MakeRandomNumber(rng, 19, true, true, 22);
double ours = 0.0;
char* stop = nullptr;
const double reference = std::strtod(text.c_str(), &stop);
if (stop == text.c_str() || !Parse(text, ours))
{
continue;
}
if (UlpDifference(ours, reference) > 1)
{
++beyondOneUlp;
}
}
JDI_CHECK_EQ(beyondOneUlp, 0);
}
JDI_TEST(format_decimal_never_scientific)
{
JDI_CHECK_WIDE_EQ(jdi::FormatDecimal(1.0e-5), std::wstring(L"0.00001"));
JDI_CHECK_WIDE_EQ(jdi::FormatDecimal(0.0), std::wstring(L"0"));
JDI_CHECK_WIDE_EQ(jdi::FormatDecimalFixed(700.0, 3), std::wstring(L"700.000"));
static const double values[] = {1.0e-9, 1.0e-6, 1.0e-5, 0.000123456, 1.0, 1234.5678, 1.0e9};
for (const double value : values)
{
const std::wstring text = jdi::FormatDecimal(value);
JDI_REQUIRE("FormatDecimal must not use exponent", text.find(L'e') == std::wstring::npos &&
text.find(L'E') == std::wstring::npos);
}
// 文档实例:`FormatValue` = `%.6g`
JDI_CHECK_WIDE_EQ(jdi::FormatValue(123.456789), std::wstring(L"123.457"));
JDI_CHECK_WIDE_EQ(jdi::FormatValue(1.0), std::wstring(L"1"));
}
JDI_TEST(format_byte_size_and_frequency)
{
const std::wstring bytes = jdi::FormatByteSize(1024ull * 1024ull);
JDI_REQUIRE("FormatByteSize(1MB) contains MB", bytes.find(L"MB") != std::wstring::npos);
const std::wstring hertz = jdi::FormatFrequency(270.5166);
JDI_REQUIRE("FormatFrequency contains unit", hertz.find(L"Hz") != std::wstring::npos);
JDI_REQUIRE("FormatFrequency has no exponent", hertz.find(L'e') == std::wstring::npos);
const std::wstring duration = jdi::FormatDurationCompound(5.06);
JDI_REQUIRE("FormatDurationCompound has no exponent", duration.find(L'e') == std::wstring::npos);
JDI_REQUIRE("FormatDurationCompound ends with unit", duration.find(L's') != std::wstring::npos);
}
JDI_TEST(text_trim_and_utf8_conversion)
{
JDI_CHECK_WIDE_EQ(jdi::Trim(L" abc \t"), std::wstring(L"abc"));
JDI_CHECK_WIDE_EQ(jdi::Trim(L" \t\r\n "), std::wstring());
JDI_CHECK_WIDE_EQ(jdi::ToLowerAscii(L"AbC-129"), std::wstring(L"abc-129"));
const std::wstring wide = jdi::Utf8ToWide("abc");
JDI_CHECK_WIDE_EQ(wide, std::wstring(L"abc"));
JDI_CHECK(jdi::ContainsInsensitive(L"HelloWorld", L"world"));
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 这类"好读"的格点上
static const double ranges[] = {1.0, 10.0, 123.0, 0.0003, 98765.0};
for (const double range : ranges)
{
const double step = jdi::NiceStep(range, 10);
JDI_REQUIRE("NiceStep > 0", step > 0.0);
const double ratio = step / std::pow(10.0, std::floor(std::log10(step)));
const bool nice = std::fabs(ratio - 1.0) < 1e-9 || std::fabs(ratio - 2.0) < 1e-9 ||
std::fabs(ratio - 2.5) < 1e-9 || std::fabs(ratio - 5.0) < 1e-9;
JDI_REQUIRE("NiceStep is a round number", nice);
}
std::vector<double> ticks;
jdi::BuildTicks(0.0, 10.0, 1.0, ticks);
JDI_CHECK_EQ(ticks.size(), 11);
if (ticks.size() == 11)
{
JDI_CHECK_NEAR(ticks.front(), 0.0, 0.0);
JDI_CHECK_NEAR(ticks.back(), 10.0, 0.0);
}
}
/**
* @brief MinGW ANSI stdio 与 UCRT printf 在**本程序实际使用的格式**上是否逐字符一致。
*
* 用途:评估"把 `swprintf` 让给系统 UCRT 实现(`__USE_MINGW_ANSI_STDIO=0`)"
* 这条体积优化是否可行 —— 若成立可以省掉整个 `mingw_wpformat` + `gdtoa` 链(≈33KB)。
* 判据极其严格:**逐字符相同**,一个字符的差异都不接受(显示文本是产品语义)。
*/
JDI_TEST(stdio_mingw_vs_ucrt_formats)
{
std::mt19937 rng(20261107u);
std::uniform_real_distribution<double> distribution(-1.0e7, 1.0e7);
std::uniform_real_distribution<double> magnitude(-40.0, 40.0);
int mismatches = 0;
int compared = 0;
static const int decimalsList[] = {0, 1, 2, 3, 4, 6, 8, 12};
for (int iteration = 0; iteration < 8000; ++iteration)
{
double value = distribution(rng);
// 覆盖极小 / 极大 / 整数量级,以及 FormatDecimal 会遇到的量级
if ((iteration % 7) == 0)
{
value = std::pow(10.0, magnitude(rng)) * ((iteration % 2 == 0) ? 1.0 : -1.0);
}
if ((iteration % 11) == 0)
{
value = static_cast<double>(iteration);
}
wchar_t ours[128] = {};
wchar_t theirs[128] = {};
for (const int decimals : decimalsList)
{
swprintf(ours, 128, L"%.*f", decimals, value);
_snwprintf(theirs, 128, L"%.*f", decimals, value);
++compared;
if (wcscmp(ours, theirs) != 0)
{
if (mismatches < 5)
{
std::printf(" [INFO] %%.%df value=%.17g ours=\"%ls\" ucrt=\"%ls\"\n", decimals, value, ours,
theirs);
}
++mismatches;
}
}
struct WideFormatProbe
{
const wchar_t* format;
};
static const WideFormatProbe probes[] = {{L"%.6g"}, {L"%.4e"}, {L"%.2f"}, {L"%g"}};
for (const WideFormatProbe& probe : probes)
{
swprintf(ours, 128, probe.format, value);
_snwprintf(theirs, 128, probe.format, value);
++compared;
if (wcscmp(ours, theirs) != 0)
{
if (mismatches < 5)
{
std::printf(" [INFO] \"%ls\" value=%.17g ours=\"%ls\" ucrt=\"%ls\"\n", probe.format, value,
ours, theirs);
}
++mismatches;
}
}
// 非浮点格式(颜色十六进制 / 计数 / 百分比尾缀)
const unsigned long long counter = static_cast<unsigned long long>(iteration) * 7919ull;
swprintf(ours, 128, L"%llu", counter);
_snwprintf(theirs, 128, L"%llu", counter);
++compared;
if (wcscmp(ours, theirs) != 0)
{
++mismatches;
}
swprintf(ours, 128, L"%02X%02X%02X", iteration & 0xFF, (iteration >> 3) & 0xFF, (iteration >> 6) & 0xFF);
_snwprintf(theirs, 128, L"%02X%02X%02X", iteration & 0xFF, (iteration >> 3) & 0xFF,
(iteration >> 6) & 0xFF);
++compared;
if (wcscmp(ours, theirs) != 0)
{
++mismatches;
}
swprintf(ours, 128, L"%.2f%%", value / 1000.0);
_snwprintf(theirs, 128, L"%.2f%%", value / 1000.0);
++compared;
if (wcscmp(ours, theirs) != 0)
{
++mismatches;
}
// `%ls`:`FormatByteSize` 的 `%.2f %ls` 依赖"宽格式串里的 %ls = 宽字符串"。
// ⚠️ MSVC/UCRT 的宽 printf 里 `%s` 也表示**宽**字符串,与 MinGW ANSI(`%s`=窄)相反,
// 因此全仓禁止在宽格式串里写 `%s`(已有注释约束),这里顺带把 `%ls` 也钉住。
swprintf(ours, 128, L"%.2f %ls", value / 1024.0, L"MB");
_snwprintf(theirs, 128, L"%.2f %ls", value / 1024.0, L"MB");
++compared;
if (wcscmp(ours, theirs) != 0)
{
if (mismatches < 5)
{
std::printf(" [INFO] \"%%.2f %%ls\" ours=\"%ls\" ucrt=\"%ls\"\n", ours, theirs);
}
++mismatches;
}
swprintf(ours, 128, L"%lld", static_cast<long long>(iteration) * -7919ll);
_snwprintf(theirs, 128, L"%lld", static_cast<long long>(iteration) * -7919ll);
++compared;
if (wcscmp(ours, theirs) != 0)
{
++mismatches;
}
}
std::printf(" [INFO] compared %d format calls, %d mismatches\n", compared, mismatches);
JDI_CHECK_EQ(mismatches, 0);
}