Repository navigation
Look up the variable of a property once, not with every evaluation - #368
Conversation
A property expression in a rule and every field of the core output line looked up the property by its name in the module's map with each evaluation, copying the name each time. A property expression now keeps the variable it resolved when it was compiled, and a field of the core output line keeps it from the first step that finds it; modules never replace the variable of a property, and variables cannot be redeclared. `core.output` still accepts names that do not exist yet, as before.
With the variable kept, the five evaluate methods of PropertyExpression were copies of those of VariableExpression. PropertyExpression now derives from it and only adds the module and the property name, which telemetry layouts use; its constructor looks the property up once instead of twice. A rule condition of the wrong type now reports "variable is not a boolean" for a property too. Telemetry takes the kept variable instead of looking it up again, and the comment on the variable of an output field says when it is set.
…is never replaced A rule like `when core.millis then ...` reported "variable is not a boolean" since the rework. VariableExpression now asks a virtual kind() for the word in its five messages, which a PropertyExpression answers with "property", as before the rework. The properties map of a module says that a module only adds to it, since compiled property expressions, core.output fields, telemetry frames and enabled_property keep the variable they found once, and the first cast in to_field says it must stay before the VariableExpression cast, which would match a property too. Module::get_property returns a const reference like get_enabled_property, so the callers that remain per step do not copy the shared pointer. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WgKVNXMAzmF5Pgiy4mqXRP
falkoschindler
left a comment
There was a problem hiding this comment.
@JensOgorek Thanks, the core of this holds up: I traced every write to a module's properties map and to Global::variables, and nothing replaces a variable under an existing name, #365 included (it uses emplace), so neither a compiled property expression nor a cached core.output field can go stale. The review turned up four small things, all fixed in 03d82c6 on the branch; the PR description has the two bullets that changed.
Fixed in 03d82c6
- The invariant was stated only at its users. The "never replaced" rule stood in comments at
PropertyExpressionandoutput_element_t, not at thepropertiesmap itself in module.h. A comment above the map now names the rule and the four places that rely on it (compiled property expressions,core.outputfields, telemetry frames,enabled_property). Enforcing it with a private map and anadd_propertythat throws would touch about 30 module constructors and is left out. - A property of the wrong type said "variable". The five
evaluate_*methods ofVariableExpressionreported "variable is not a boolean" for a property too, e.g. forwhen core.millis then ..., where the condition type is not checked at compile time and the error is echoed every loop. A protected virtualkind()gives "variable" or "property", so the messages read as before the rework. - The cast order in
to_fieldis load-bearing. Since aPropertyExpressionis aVariableExpressionnow, the generic branch in telemetry.cpp would match a property too and then fail to find it inGlobal::variables. A comment says thePropertyExpressioncast must stay first. get_propertyreturned the shared pointer by value. It now returnsconst Variable_ptr &likeget_enabled_property; the callers that need a copy (PropertyExpression, thecore.outputcache,wheels.cpp) still take it once.
Retest of
|
Motivation
A rule that reads a property looked the property up by its name in the module's map every time it was evaluated, and
core.outputdid the same for each of its fields in every loop. A name longer than 15 characters, whichstd::stringcannot hold without allocating, also cost a heap allocation per lookup. On a bench ESP32, 500 property reads in rules took 5 ms more per loop with short names and 19 ms more with a 16-character name than with this PR, that is 10 and 38 µs per read. A field ofcore.outputwith a 16-character name took 25 µs longer; with short names the difference is below the 1 ms resolution of the measurement. Long names are common: six of the 44 fields of F30's core line have one, likewheels.linear_speed_from_ticks, and so do most mirrors of aBusTelemetry(#365), such asp2t.delta_2.angle_left, which arm interlocks read in every loop.Implementation
VariableExpressionthat keeps the variable it resolves when the code is compiled, plus the module and property name for telemetry layouts.Proxyfor unknown or declared properties,SerialBusfor clock offsets,BusTelemetrymirrors in Mirror values of serial bus peers in BusTelemetry modules #365). Declaring an existing name assigns to its variable. So the variable of a property never changes; telemetry frames and theenabledproperty already rely on this.core.outputkeeps its variable from the first loop that finds it. Until then it is looked up in every loop as before, so the output may still name a variable or property that appears later.Module::get_propertytakes the name by reference, searches the map once instead of twice and returns a reference to the variable, likeget_enabled_property.property is not a boolean, a variablevariable is not a boolean, as before.Cost: 8 B of heap per property in compiled code and per output field; 1 792 B less flash.
Progress
Bench test 1: property reads in rules
One ESP32 board, built with ESP-IDF 5.3.1, main
ec5d268against this PRbaab50c. After each reset, the console received:thas 1900 characters, and 10 rules with 20 assignmentss = teach copy it 200 times per loop. That is a code path this PR does not touch, and it keeps the loop just under its 10 ms period, so that any further work stretches the loop. Then followed 50 rules with 10 property reads each, all of them evaluated and false:or the same with ten times
core.last_message_age < 0, a name of 16 characters. The loop time is 20 s ofcore.millisdivided by the loops counted meanwhile. A loop that overruns waits for the next 1 ms tick, so the loop time has a resolution of 1 ms, 2 µs per read; 10 ms means the work fits into the period. Each run was done twice with the same result.ec5d268baab50ccore.last_message_agecore.last_message_age, no string assignmentsWith this PR the heap holds 8 B more per read (32 104 instead of 36 048 B free with the 500 short reads).
The rework
eac45c9, which makes the property expression a variable expression, gave the same 10, 15 and 10 ms for the string assignments alone, with the 500 reads ofcore.last_message_ageadded, and for those reads alone.With the review fixes
03d82c6the three runs gave 10, 15 and 10 ms again, andwhen core.millis then …reportsproperty is not a booleanin every loop,when x then …with anint xreportsvariable is not a boolean.Bench test 2: fields of core.output
Same board and the same 200 string assignments, with the console at 921600 baud (
core.set_baudrate(921600)), so that the line does not wait for the UART.core.outputnamed 200 fields cycling throughi1.level,i2.level, …i4.change, or 80 timescore.last_message_age(100 would exceed the 2 KB console line). The runs with this PR were done twice with the same result.ec5d268baab50ccore.last_message_ageWith short names the time goes into formatting the values; the lookup is below the resolution.
core.output("core.millis later")beforeint later = 7behaves the same on both builds:error in module "core": unknown variable "later"until the declaration, then the value, 7 and afterlater = 88.※