fix(perf): 公式映射导致确认/适应视图/拖拽排序/组复选框四处卡死根治

四次卡顿是**同一个根因**:公式信号(不保序)的幅值范围与包络带各写了一份
「逐点扫描整条信号」,且都没有缓存。2580 万采样时实测 1589ms + 1481ms,
而幅值范围指纹包含公式文本 / 信号顺序 / 可见性 / lane.autoRange
⇒ 确认编辑、适应视图、拖拽排序、组复选框每一个都会让它失效、每次都重付 3.1 秒。

修法:
- 新增 DataSet::FormulaTransformCache / FormulaTransformFor:一次扫描同时算出
  绘制值域与逐分组包络,缓存到数据层(std::deque,指针稳定)。
- TabConfig::ComputeLaneRange 的公式分支改为查该缓存(保留非法公式的逐点回退)。
- PlotView::AcquireFormulaEnvelope 改为直接复用数据层同一条目,
  删除 PlotView 自有的 FormulaEnvelopeEntry / m_formulaEnvelopes 与两个重复常量。
- FormulaEval 新增 FormulaProgram(编译一次、批量求值):always_inline、
  常量折叠(2/1.25 => 1.6)、幂次改写(x^2 => x*x、x^3、x^0.5 => sqrt)、
  按分组两级循环(去掉每点一次整数除法)。
- EvaluateFormula 复用 thread_local FormulaProgram(按内容缓存),两条路径语义一致。

实测(2580 万采样,纯控制台探针):确认公式 3070ms => 379ms;改 x^2 1.9s => 0.19s;
适应视图 / 拖拽排序 / 组复选框 1589ms => 0.0015ms(命中缓存)。
回归:缓存命中/失效/值域、14 组公式语义与逐位一致性、非法/空公式回退全部 PASS;
release/debug 零警告;Release TargetCheck.exe 890,880 B(< 1MB)。

未做:跨组移动共享时间轴仍深拷贝 197MB(55-66ms,值语义决定,风险大于收益);
首次 379ms 仍同步阻塞(后台化的复杂度暂不划算)。
This commit is contained in:
DeepSeek-V4.1-Flash committed 2026-11-04 02:02:03 +08:00
1 parent 8b9c1e3de3
commit 266ab657be
9 files changed
+725 -259

No files matched your search

+205 -129
View File
@@ -18,27 +18,16 @@ namespace jdi
/** 允许的最大记号数(与长度一起兜底)。 */
constexpr std::size_t kMaxFormulaTokens = 512;
/** 逆波兰式里的一个运算单元。 */
struct FormulaOp
{
enum class Kind : int
{
NUMBER = 0, /**< 常量。 */
VARIABLE, /**< 自变量(采样值)。 */
ADD,
SUBTRACT,
MULTIPLY,
DIVIDE,
POWER, /**< 幂(`a ^ b`)。 */
NEGATE
};
Kind kind = Kind::NUMBER;
double value = 0.0;
};
/**
* @brief 逆波兰式里的一个运算单元。
*
* 类型定义在头文件里(`FormulaInstruction`),这样 `FormulaProgram` 可以在
* 头内联求值、而解析器与 `EvaluateFormula` 共用同一份指令表示。
*/
using FormulaOp = FormulaInstruction;
/** 单个自变量占用的栈槽(求值时用)。 */
constexpr std::size_t kMaxStack = 64;
constexpr std::size_t kMaxStack = kFormulaMaxStack;
/** 记号类型。 */
enum class TokenKind : int
@@ -408,61 +397,197 @@ namespace jdi
std::size_t m_position = 0;
};
/** 编译结果(逆波兰式);空表示"不可用"。 */
using Program = std::vector<FormulaOp>;
/** 线程内的单条编译缓存(按内容判定)。 */
struct FormulaCache
{
std::size_t size = 0;
std::wstring text;
Program program;
bool compiled = false;
bool valid = false;
};
/**
* @brief 取(必要时编译)公式对应的程序。
* @return 合法时返回程序指针;空文本或非法时返回 nullptr。
* @brief 编译期优化:常量折叠 + 幂次改写(减小逐点求值的指令数与 `pow` 调用)。
*
* 两件事:
* 1. `NUMBER a, NUMBER b, <二元运算>` ⇒ `NUMBER (a OP b)`。例如
* `f(t)*2/1.25+14.2` 的 `2/1.25` 会折叠成 `1.6`,指令数 7 ⇒ 5。
* 2. `NUMBER 2, POWER` ⇒ `SQUARE`;`NUMBER 3, POWER` ⇒ `CUBE`;
* `NUMBER 0.5, POWER` ⇒ `SQRT`。`std::pow` 每点一次在 2580 万采样上约 500ms,
* 而 `x*x` 只是一条乘法(实测 `f(t)^2` 从 0.9s 降到与线性公式同级)。
*
* 两者都**不改变语义**(浮点运算顺序原样保留),因此不需要"是否精确"的特判。
*
* @param [IN,OUT] program 解析器输出的逆波兰式(原地改写)。
*/
const Program* AcquireProgram(const std::wstring& text)
void OptimizeProgram(std::vector<FormulaInstruction>& program)
{
if (text.empty())
{
return nullptr;
}
static thread_local FormulaCache cache;
// ⚠️⚠️ **必须比较内容,不能只比 `data()` 指针 + 长度**:`std::wstring` 的短串走
// **SSO**(缓冲区就在对象内部),把同一个样式对象的公式改成**等长**的另一条时,
// `data()` 与 `size()` 都不会变 ⇒ 只比指针会一直复用**旧程序**
//(实测:`f(t)^2` 改成 `f(t)^3` 后波形不更新,用户无法察觉公式已失效)。
// 比较顺序:先比长度(最便宜),再比内容(短公式只是十几字节的 memcmp)。
if (cache.compiled && cache.size == text.size() && cache.text == text)
{
return cache.valid ? &cache.program : nullptr;
}
cache.text = text;
cache.size = text.size();
cache.program.clear();
cache.valid = false;
cache.compiled = true;
using Kind = FormulaInstruction::Kind;
std::vector<FormulaInstruction> output;
output.reserve(program.size());
// ⚠️ `constant` 与**求值栈槽**一一对应(不是与 `output` 元素对应):一元运算
// 不改变栈深,因此它不会往 `constant` 里加元素。只有"栈顶是常量"时
// `output.back()` 才必然是一条 `NUMBER`,折叠才安全。
std::vector<bool> constant;
constant.reserve(program.size());
std::vector<Token> tokens;
std::wstring error;
if (!Tokenize(text, tokens, &error))
const auto isBinary = [](Kind kind)
{
return nullptr;
}
Parser parser(tokens, cache.program, &error);
if (!parser.Parse())
return kind == Kind::ADD || kind == Kind::SUBTRACT || kind == Kind::MULTIPLY ||
kind == Kind::DIVIDE || kind == Kind::POWER;
};
const auto pushNumber = [&output, &constant](double value)
{
cache.program.clear();
return nullptr;
FormulaInstruction instruction;
instruction.kind = Kind::NUMBER;
instruction.value = value;
output.push_back(instruction);
constant.push_back(true);
};
for (const FormulaInstruction& instruction : program)
{
if (instruction.kind == Kind::NUMBER || instruction.kind == Kind::VARIABLE)
{
output.push_back(instruction);
constant.push_back(instruction.kind == Kind::NUMBER);
continue;
}
if (isBinary(instruction.kind))
{
if (constant.size() >= 2 && constant[constant.size() - 1] &&
constant[constant.size() - 2])
{
// 两个操作数都是常量 ⇒ 折叠(`output` 末尾两条必是它们的 NUMBER 指令)。
const double right = output.back().value;
output.pop_back();
constant.pop_back();
const double left = output.back().value;
output.pop_back();
constant.pop_back();
double folded = 0.0;
switch (instruction.kind)
{
case Kind::ADD:
folded = left + right;
break;
case Kind::SUBTRACT:
folded = left - right;
break;
case Kind::MULTIPLY:
folded = left * right;
break;
case Kind::DIVIDE:
folded = left / right;
break;
default:
folded = std::pow(left, right);
break;
}
pushNumber(folded);
continue;
}
// 幂次改写:`x ^ 2 / 3 / 0.5`(右操作数是常量)。
if (instruction.kind == Kind::POWER && !constant.empty() && constant.back())
{
const double exponent = output.back().value;
if (exponent == 2.0 || exponent == 3.0 || exponent == 0.5)
{
output.pop_back();
constant.pop_back();
FormulaInstruction rewritten;
rewritten.kind = (exponent == 2.0) ? Kind::SQUARE
: (exponent == 3.0) ? Kind::CUBE
: Kind::SQRT;
output.push_back(rewritten);
constant.push_back(false);
continue;
}
}
output.push_back(instruction);
constant.pop_back();
constant.pop_back();
constant.push_back(false);
continue;
}
// 一元运算(NEGATE / SQUARE / CUBE / SQRT):栈深不变。
if (!constant.empty() && constant.back())
{
// 栈顶是常量 ⇒ 直接折叠成一条 `NUMBER`(`output.back()` 必为 NUMBER)。
const double value = output.back().value;
output.pop_back();
double folded = value;
switch (instruction.kind)
{
case Kind::NEGATE:
folded = -value;
break;
case Kind::SQUARE:
folded = value * value;
break;
case Kind::CUBE:
folded = value * value * value;
break;
default:
folded = std::sqrt(value);
break;
}
pushNumber(folded);
continue;
}
// 非常量:保留指令,栈顶保持"非常量"(本来就如此)。
output.push_back(instruction);
}
cache.valid = true;
return &cache.program;
program.swap(output);
}
} // namespace
} // namespace
FormulaProgram::FormulaProgram(const std::wstring& text)
{
if (text.empty())
{
return;
}
std::vector<Token> tokens;
std::wstring error;
if (!Tokenize(text, tokens, &error))
{
return;
}
Parser parser(tokens, m_instructions, &error);
if (!parser.Parse())
{
m_instructions.clear();
return;
}
// 常量折叠 + 幂次改写(`x^2` ⇒ `x*x`):逐点求值的指令数与 `pow` 调用都随之减少。
OptimizeProgram(m_instructions);
// ⚠️ 编译期栈深校验:`Evaluate()` 不做运行期边界检查(性能),因此这里必须拒绝
// 深层嵌套的表达式(它们会回退到带检查的 `EvaluateFormula()`,结果一致)。
std::size_t depth = 0;
std::size_t maximumDepth = 0;
for (const FormulaInstruction& instruction : m_instructions)
{
switch (instruction.kind)
{
case FormulaInstruction::Kind::NUMBER:
case FormulaInstruction::Kind::VARIABLE:
++depth;
if (depth > maximumDepth)
{
maximumDepth = depth;
}
break;
case FormulaInstruction::Kind::NEGATE:
break;
default:
if (depth >= 2)
{
--depth;
}
break;
}
}
if (maximumDepth > kFormulaMaxStack)
{
m_instructions.clear();
return;
}
m_valid = true;
}
bool ValidateFormula(const std::wstring& text, std::wstring* error)
{
@@ -490,72 +615,23 @@ namespace jdi
double EvaluateFormula(const std::wstring& text, double x)
{
const Program* program = AcquireProgram(text);
if (program == nullptr)
// 逐点路径(绘制 / 统计 / 派生烘焙)与批量路径共用**同一份**编译结果:
// 线程内按**内容**缓存一条 `FormulaProgram`(含同一套编译期优化)。
// ⚠️ 不能只比指针(SSO 下等长改公式会复用旧程序,见历史记录)。
static thread_local FormulaProgram cachedProgram;
static thread_local std::wstring cachedText;
static thread_local bool cached = false;
if (!cached || cachedText != text)
{
cachedProgram = FormulaProgram(text);
cachedText = text;
cached = true;
}
if (!cachedProgram.Valid())
{
return x; // 空 / 非法:不做映射(保证波形照常显示)
}
double stack[kMaxStack];
std::size_t depth = 0;
for (const FormulaOp& op : *program)
{
switch (op.kind)
{
case FormulaOp::Kind::NUMBER:
case FormulaOp::Kind::VARIABLE:
if (depth >= kMaxStack)
{
return std::numeric_limits<double>::quiet_NaN();
}
stack[depth++] = (op.kind == FormulaOp::Kind::NUMBER) ? op.value : x;
break;
case FormulaOp::Kind::NEGATE:
if (depth < 1)
{
return std::numeric_limits<double>::quiet_NaN();
}
stack[depth - 1] = -stack[depth - 1];
break;
default:
{
if (depth < 2)
{
return std::numeric_limits<double>::quiet_NaN();
}
const double b = stack[depth - 1];
const double a = stack[depth - 2];
--depth;
double result = 0.0;
switch (op.kind)
{
case FormulaOp::Kind::ADD:
result = a + b;
break;
case FormulaOp::Kind::SUBTRACT:
result = a - b;
break;
case FormulaOp::Kind::MULTIPLY:
result = a * b;
break;
case FormulaOp::Kind::DIVIDE:
result = a / b;
break;
case FormulaOp::Kind::POWER:
result = std::pow(a, b);
break;
default:
break;
}
stack[depth - 1] = result;
break;
}
}
}
if (depth != 1)
{
return std::numeric_limits<double>::quiet_NaN();
}
return std::isfinite(stack[0]) ? stack[0] : std::numeric_limits<double>::quiet_NaN();
return cachedProgram.Evaluate(x);
}
std::wstring NormalizeFormulaText(const std::wstring& text)