REXX Language implementation
cREXX can profile an RXBIN program at three complementary levels:
These are development facilities. They are compiled into a dedicated VM build and are absent from a normal build. They are also separate run modes: one run collects timing data or instruction-sequence data, never both.
Configure an optimized profiling build from the repository root:
cmake -S . -B cmake-build-profile \
-DCMAKE_BUILD_TYPE=Release \
-DCREXX_VM_PROFILING=ON
cmake --build cmake-build-profile --config Release \
--target rxvm rxbvm rxtvm rxvme rxbvme rxseq rxas
Omit rxtvm from that command for MSVC or another compiler without GNU-style
labels-as-values support.
The profiling option is off by default. When it is off, the instrumentation hooks compile to no code: the normal VM has no profiling branches, state, or command-line options. A profiling build does contain the instrumentation even when a particular run does not request a profile, so use the normal Release VM for production execution and uninstrumented benchmark baselines.
Executable locations depend on the CMake generator and platform. The examples
below assume the profiling-build executables are installed or otherwise on
PATH.
All command-line VM variants expose the same profiling options:
| VM | Execution mode | Runtime library |
|---|---|---|
rxvm |
compiler-selected product VM | load required RXBIN modules explicitly |
rxbvm |
switch-dispatch VM | load required RXBIN modules explicitly |
rxtvm |
direct-threaded VM, where supported | load required RXBIN modules explicitly |
rxvme |
compiler-selected product VM | standard library embedded |
rxbvme |
switch-dispatch VM | standard library embedded |
Use the same VM variant when comparing profiles. Differences between VM modes are often exactly what the instruction and transition views reveal.
The repository contains profiling_demo.rxas, reproduced here. Its metadata gives the profiler human-readable callable names, and its worker procedure is called twice.
.globals=0
.meta "profiling_demo.main"="b" ".int" main() ""
.meta "profiling_demo.worker"="b" ".void" worker() ""
main() .locals=0
call worker()
call worker()
ret 0
worker() .locals=5
load r1, 0
load r2, 5
loop:
iadd r1, r1, 1
copy r3, r1
copy r4, r3
ilt r0, r1, r2
brt loop, r0
ret
Assemble it from the repository root:
rxas -o profiling_demo \
docs/books/crexx_programming_guide/examples/profiling_demo.rxas
This produces profiling_demo.rxbin. The same profiling commands work with
RXBIN files produced by rxc and rxas, by rxlink, or by the crexx
driver.
Run the example and write a human-readable report:
rxvm --profile-output profiling-demo.txt profiling_demo.rxbin
Write the same data as CSV by using a filename ending in .csv:
rxvm --profile-output profiling-demo.csv profiling_demo.rxbin
The extension check is case-insensitive. Any other filename selects the table
format. --profile-output enables timing profiling by itself; it does not need
to be combined with --profile.
For a table on standard error instead of a file, use:
rxvm --profile profiling_demo.rxbin
--profile=timing, --profile-output=profiling-demo.txt, and the equivalent
--sequence-...=... spellings are also accepted. Profiling options must occur
before the first binary filename. Program arguments still follow -a, for
example:
rxvme --profile-output application.csv application.rxbin -a first second
For a deterministic diagnostic census with all timing fields set to zero, use
--profile=counts. Put it before --profile-output when both are present:
rxvm --profile=counts --profile-output application-counts.csv application.rxbin
Counts-only output is intended for exact operation comparisons. It is still a profile-build diagnostic and is not product timing evidence.
The heading identifies the VM mode and the program result. A result of 0
normally means that the program completed successfully. The next lines state
the monotonic-clock calibration, hot-loop interrupt-poll count, invalid-event
count, counter-overflow state, and whether procedure and allocation tracking
were complete.
The report then contains complementary views of the run:
| Section | What it reports |
|---|---|
| Instructions | Count, effective handler placement, and entry-to-retire or entry-to-terminal time for each executed opcode. Rows are ordered by total time. |
| Transitions | Time from an instruction retiring to the next instruction entering. Kinds distinguish sequential flow, a taken branch, call entry, return exit, interrupt entry/resume, external entry, and termination. |
| Procedures and methods | Runtime calls and inclusive elapsed/body/self time for bytecode callables; call count and total observed time for native callables. |
| Call mechanics | The measured VM work entering and leaving each bytecode callable. |
| Runtime allocation and value/frame storage | Successful profiling-scope allocation requests, requested bytes, maximum request size, frame reuse, and active-frame high water. |
| Value operations | Common whole-value, typed-copy, move, clear, reset and destroy helper entries by structural payload shape, with logical payload bytes. Nested helper calls are intentionally separate rows. |
| Frame-entry phases | Fresh/reused counts and diagnostic time for local/global pointer relinking, argument-count reset, inherited/root context setup and final frame fields. |
| Branch sites | Canonical module/index executions, taken/fall-through direction, same-module backward targets and cross-module targets for RXAS branch-flow opcodes. |
| Call-path census | Dynamic call attempts by direct/dynamic/native/root/signal path, exact arity, callable kind, frame disposition, outcome, target, and site. |
| Return placement | The actual runtime RET_REG local-move/non-local-copy decision plus void, ignored, immediate, and terminal returns. |
| Dynamic selection | Method/factory selector attempts and their success/failure outcomes, kept separate from subsequent DCALL activity. |
| Call-window attribution | Effects-backed setup swaps and defensive argument copies, completed mapping-recovery swap sequences, and all remaining unclassified SWAP/COPY executions. |
| Signal-unwind restoration | Branch-unwind events, discarded bytecode frames, restored bytecode/native windows and slots, and restoration failures. |
The final Interrupt sub-phases section separates interrupt scans from the mechanics of entering, resuming, or terminating an interrupt path. These sub-phases overlap the interrupt transition rows and must not be added to them.
The instruction table’s handler column is inline or outline for the
handler that actually executed in this build. It is mixed when executions
attributed to one canonical public opcode used both placements, which can
happen when a process-private fused handler is selected for only some static
sites. This is effective runtime attribution, not merely a lookup of the
serialized opcode’s policy. In CSV, the same value occupies the existing
value column on instruction rows; schema version 5 and the column layout are
unchanged.
Callable identity comes from RXBIN META_FUNC metadata. The report includes
the fully qualified name and classifies each row as procedure, method,
factory, or native. Older binaries without function metadata still receive
a module/procedure fallback name, but do not have the richer signature data.
For bytecode callables the columns mean:
| Column | Meaning |
|---|---|
calls |
Number of runtime frame activations. |
complete |
Calls that returned normally. |
unwound |
Calls discarded by exceptional stack unwinding. |
total ns |
Elapsed call time, including measured entry, body, and exit spans. |
average ns |
Elapsed time divided by the number of elapsed samples. |
body ns |
Inclusive body time, including nested bytecode calls. |
self ns |
Time attributed only to that callable, excluding nested bytecode calls and observed native-call time. |
native child |
Observed native-call time removed from this bytecode caller’s self time. It is a child component of body time, not extra elapsed time. |
self % |
Self time as a percentage of inclusive body time. |
The call-mechanics table exposes the entry and exit components separately:
caller call-instruction entry
-> callee first-instruction entry entry overhead
-> callee return-instruction entry inclusive body
-> caller next-instruction entry exit overhead
The complete elapsed span runs from the first boundary to the last. External entry and terminal return use the nearest VM boundary available. These are observed VM spans, not an estimate of the benefit of inlining.
Native plugin calls are timed around the VM’s native-call boundary. A native
row therefore has calls, complete, total time, and average time, while
body, self, and bytecode entry/exit columns are shown as unavailable. Native
time remains visible in the calling instruction’s timing but is removed from
the bytecode caller’s self time. The native_child metric preserves that
removed amount explicitly for each bytecode caller.
Dynamic calls are attributed to the concrete procedure selected at runtime. An inlined procedure or method creates no runtime frame and consequently does not appear as a separate callable row; its work belongs to the containing procedure and its executed instructions.
The census is entirely dynamic. A call row is recorded at the VM boundary
where the opcode path, concrete target, actual argument count, caller site, and
activation result are authoritative. external_root,
signal_bytecode, and signal_native keep non-instruction
entry distinct. Failed or unresolved attempts have an explicit outcome and
none_failed frame disposition. Native calls use
no_child_native; successful bytecode and root/signal frames use
fresh or reused.
Argument mechanics are not inferred from opcode adjacency. At each successful
argument-bearing call, the profiler performs a backward slice of the executed
straight-line trace from the actual call window. It uses the NR-04
rxop_effects() overwrite/flow contract as the definition boundary.
Only reached SWAP_REG_REG instructions are setup swaps and only
reached whole-value COPY_REG_REG definitions are defensive
argument copies. Normal restoration reconstructs the setup permutation and
buffers subsequent swaps until their combined effect recovers the pre-call
mapping. Only a completed recovery sequence is credited; a mapping carried
into another call or to frame completion is not treated as a tracking failure.
Every other executed SWAP_REG_REG or COPY_REG_REG
remains explicitly unclassified.
RET_REG placement is measured inside its real VM branch. A true
local with unchanged base mapping is moved; an argument, global, linked/swapped
local, or other non-local source is copied. This internal copy decision is not
a COPY_REG_REG instruction and is therefore separate from
call-window copy counts. Exceptional unwind is also separate from ordinary
return placement.
All counters saturate. Non-zero census_tracking_unavailable,
attribution_degraded, restoration_failures, or an
overflowed/degraded status means the affected census
is incomplete. Zero-count categories remain in CSV so absence is not confused
with parser loss.
CSV is the best input for spreadsheets, scripts, and comparisons between runs. The current format identifies itself as schema version 5 and preserves the schema-4 24-column header:
section,name,value,id,count,total_ns,average_ns,min_ns,max_ns,percent,selected,entries,resumes,terminals,module,kind,completed,unwound,return_type,args,bytes,max_bytes,high_water,status
Columns that do not apply to a row are empty or zero. Interpret rows by their
section value:
section |
Row use |
|---|---|
summary |
Schema, VM mode, result, timer calibration, poll/error state, and overflow/tracking status. |
status |
Explicit completeness/degradation status for instructions, procedures, allocations, call census, frame entry, value operations and branch sites. |
instruction |
One row per executed opcode; value is its effective inline, outline, or mixed handler placement. |
transition |
One row per observed transition kind. |
interrupt |
Scan totals and per-signal selection/entry/resume/terminal data. |
procedure |
One or more metric rows per called procedure, method, factory, or native routine. |
allocation |
One row per allocation/value/frame counter, including byte and high-water fields where they apply. |
value_operation |
name is the helper/typed operation, value is the structural payload shape, and count, bytes, and max_bytes are the observed calls and logical payload bytes. |
frame_entry |
name is the entry phase, value is fresh or reused, id carries phase units, and the normal count/timing fields carry invocations and diagnostic time. |
branch |
name is the opcode, id is the canonical instruction index, count is executions, selected/entries/resumes/terminals are taken/fall-through/backward/cross-module counts, and module names the exact module. |
census |
Aggregate call path, exact arity, callable kind, frame disposition, and outcome rows; zero path/kind/disposition/outcome categories are retained. |
call |
One dynamic target/site/path/arity/kind/frame/outcome row. bytes, max_bytes, and high_water carry setup swaps, normal restoration swaps, and defensive argument copies for this section only. |
return |
Dynamic return-placement decisions. |
dynamic |
Method/factory selection attempts, successes, and failures. |
mechanics |
Global call-window setup/restoration/unclassified swap and defensive/unclassified copy counts. |
unwind |
Signal-unwind frame/window/slot/restoration counters. |
For a bytecode procedure row, value is one of elapsed,
inclusive_body, self, native_child, entry_overhead, or
exit_overhead. A native row uses native_total. Callable rows also carry
module, kind, completed, unwound, return_type, and args metadata.
Do not assume that one CSV row represents all metrics for a callable.
Schema-5 readers should accept schema 4 by treating the four new sections as unavailable, not as zero. Schema-4 readers may continue to consume the original sections because the header and existing row meanings did not change.
The allocation view counts successful allocation requests made while the
timing-profile run is active. bytes is the sum of requested capacities and
max_bytes is the largest single requested capacity. A successful realloc
is one request for its full new capacity, not a net-growth byte count. These
are request counters, not retained/live-heap measurements.
| Counter | Exact scope |
|---|---|
frame_blocks |
Fresh combined stack_frame blocks allocated when the selected procedure has no reusable block large enough. bytes includes the frame, pointer arrays, and inline local/a0 value storage. |
standalone_values |
Successful value_f() heap allocations. Each request also contributes one value_slots slot. |
attribute_value_blocks |
Fresh or replacement blocks containing object-attribute value structs. Their element counts contribute to value_slots. |
attribute_pointer_storage |
Successful allocations or capacity changes for attribute pointer arrays, unlinked-attribute pointer arrays, and attribute-buffer pointer arrays. |
string_buffers |
Successful heap buffer allocations/capacity changes made by VM string growth or alias-safe string concatenation. Inline small strings do not count. |
binary_buffers |
Successful heap buffer allocations/capacity changes made by VM binary growth or binary concatenation. |
value_slots |
Total value structs supplied by counted standalone, frame-local/a0, and attribute-value allocations. bytes is slots times sizeof(value); max_bytes is the largest counted value block. It deliberately overlaps the containing allocation byte totals. |
frame_activations |
Every activated bytecode frame, fresh or recycled. high_water is the maximum simultaneously active bytecode-frame count observed in the run. |
frame_reuses |
Activations satisfied from a procedure’s frame recycler rather than a fresh frame_blocks request. |
The scope excludes module loading, profiler/RXSEQ bookkeeping, plugin-private
and native-payload ownership, OS/TLS support, reference-lifetime payloads, and
temporary native conversion buffers. It is intentionally a focused VM
value/frame/storage view, not a replacement for a heap profiler. Allocation
rows report complete, overflowed, or degraded; the summary also carries
allocation_tracking_unavailable. A non-zero active-frame balance at report
time degrades frame_activations because its high water cannot then be treated
as a balanced run.
The timings are raw, instrumented monotonic wall times. The profiler calibrates the minimum positive interval between adjacent clock reads and reports how often calibration reads had a zero delta, but it does not subtract a guessed timer cost from short instructions.
For useful results:
Instruction, transition, procedure, and interrupt sections are overlapping views. Procedure body times also overlap their nested callees. Do not add the sections or all procedure rows together as if they partition elapsed time.
invalid events, counter overflow, procedure tracking=degraded, or
allocation tracking=degraded in the table heading—and their CSV summary or
allocation-row equivalents—mean that the affected view is incomplete.
Preserve those status fields whenever profiles are processed automatically.
Sequence mode finds frequently executed straight-line instruction patterns.
It records where sequential windows occurred during the run; the separate
rxseq program then decodes the original RXBIN modules, normalizes operands,
and clusters equivalent patterns.
Capture two-instruction windows from the example:
rxvm --sequence-count=2 \
--sequence-output profiling-demo.rxseq \
profiling_demo.rxbin
--sequence-count accepts only 2, 3, or 4, and it must be paired with
--sequence-output. The output is a compact binary execution profile, not a
text report.
Analyze it as a table:
rxseq profiling-demo.rxseq profiling_demo.rxbin
Or write candidate data as CSV:
rxseq profiling-demo.rxseq profiling_demo.rxbin \
--output profiling-demo-candidates.csv
As with the timing report, .csv is recognized case-insensitively; another
output extension selects the human-readable table.
A window continues only across an actual sequential fall-through transition in the same module and runtime frame. Bytecode calls and returns, taken branches, interrupt entry or resume, external frame entry, and program termination break the window. A native call that returns normally stays within its CALL instruction and can participate in a sequential window. A branch instruction can be the last instruction in a window, but a window never crosses a branch when it is taken.
Loops increase the dynamic count of a site instead of enlarging the .rxseq
file. The capture keeps one counter for each possible starting slot in the
loaded modules and writes only non-zero sites.
The .rxseq file deliberately stores module identity and counts, not a second
copy of the bytecode. Supply rxseq with the complete module set from the
profiled run. Argument order does not matter, but names, expanded content
hashes, and instruction sizes must match exactly, and no extra modules may be
added.
For example, a two-module run is captured and analyzed as follows:
rxvm --sequence-count=2 --sequence-output application.rxseq \
application.rxbin library.rxbin
rxseq application.rxseq application.rxbin library.rxbin
An rxvme or rxbvme run also records its embedded standard-library modules.
Analysis therefore needs the exact corresponding library RXBIN image. For the
simplest reproducible workflow, use rxvm/rxbvm with an explicit module set
or profile a single linked RXBIN image.
rxseq rejects a missing module, an extra module, a content-hash mismatch, or
an instruction-size mismatch. Keep the .rxseq file and its exact RXBIN inputs
together.
Operands are alpha-renamed by first occurrence across the whole window.
Registers become r1, r2, and so on. Every other encoded operand—such as a
literal, pool constant, label, or procedure reference—becomes c1, c2, and
so on. Reuse remains visible, so these two sites cluster even if their physical
register numbers differ:
IADD_REG_REG_REG(R17,R5,R9) | COPY_REG_REG(R5,R22)
-> IADD_REG_REG_REG(r1,r2,r3) | COPY_REG_REG(r2,r4)
The table heading reports the selected window length, number of clusters, number of static sites, and whether a counter overflowed. Each candidate row contains:
| Field | Meaning |
|---|---|
Count |
Sum of dynamic executions across all clustered sites. |
Sites |
Number of distinct starting sites with the normalized pattern. |
Modules |
Number of modules containing those sites. |
Args |
Number of distinct normalized register/constant symbols. |
Status |
candidate when the window decoded successfully. |
Pattern |
Normalized instruction and operand-reuse pattern. |
mapping / example |
One concrete operand mapping and module/start-slot location. |
CSV output uses these columns:
rank,count,sites,modules,symbols,status,pattern,mapping,example_module,example_start
Candidate extraction is a discovery aid, not an automatic bytecode rewrite.
Before combining instructions or adding an optimizer rule, separately review
control flow, liveness, aliasing, exceptions, interrupts, and observable VM
semantics. Treat overflow=yes as an incomplete count rather than silently
ranking the saturated data.
| Symptom | Resolution |
|---|---|
rxvm -h does not list profiling options |
Reconfigure and rebuild with -DCREXX_VM_PROFILING=ON; normal builds intentionally omit them. |
--profile and --sequence-count are separate run modes |
Run timing and sequence collection separately. |
| A sequence option says its partner is required | Supply both --sequence-count and --sequence-output. |
profiled module is missing or module content hash mismatch |
Analyze with every exact RXBIN module used by the captured run. |
| An expected procedure is absent | Check whether it was inlined and whether the RXBIN contains META_FUNC metadata. |
| Procedure tracking is degraded | Treat callable data as incomplete; instruction and transition data may still be useful. |