Skip to content

Commit a0cff99

Browse files
committed
pd-vm: perf: shared DecodedInstructionData
1 parent 5422aa0 commit a0cff99

7 files changed

Lines changed: 164 additions & 114 deletions

File tree

pd-edge/README.md

Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ This crate now ships two binaries with different scopes:
2222
- [Active Control-Plane RPC](#active-control-plane-rpc)
2323
- [Example](#example)
2424
- [HTTP Proxy Performance Framework](#http-proxy-performance-framework)
25-
- [Latest Snapshot (2026-03-07, Local Windows x86_64 Dev Machine)](#latest-snapshot-2026-03-07-local-windows-x8664-dev-machine)
25+
- [Latest Snapshot (2026-03-11, Local Windows x86_64 Dev Machine)](#latest-snapshot-2026-03-11-local-windows-x8664-dev-machine)
2626
- [ABI Source of Truth](#abi-source-of-truth)
2727
- [Release Artifacts](#release-artifacts)
2828
- [Docker](#docker)
@@ -207,7 +207,7 @@ Run the built-in framework to benchmark these scenarios:
207207
- proxy with a compute-only program (no host calls; baseline workload)
208208
- proxy with additive async HTTP host calls on top of the same workload (`get_method/get_path/get_header/get_body` + `set_header/set_body`) and explicit termination (no upstream target)
209209

210-
Detailed report with charts: [`docs/HTTP_PROXY_PERF_REPORT_2026-03-07.md`](docs/HTTP_PROXY_PERF_REPORT_2026-03-07.md)
210+
Detailed report with charts: [`docs/HTTP_PROXY_PERF_REPORT_2026-03-11.md`](docs/HTTP_PROXY_PERF_REPORT_2026-03-11.md)
211211

212212
Build the proxy binary once before running benchmarks so the framework does not accidentally run a stale executable:
213213

@@ -243,7 +243,7 @@ cargo run -p pd-edge --example http_proxy_perf_framework --release -- \
243243
--requests 3000 \
244244
--warmup-requests 300 \
245245
--concurrency 64 \
246-
--fuel-latency-fuels "1,8,64,512,4096,50000" \
246+
--fuel-latency-fuels "1,2,4,8,10,16,32,64,512,4096,50000" \
247247
--fuel-latency-check-intervals "1,4,16,64" \
248248
--json-out target/http_proxy_fuel_sweep_async.json
249249

@@ -254,7 +254,7 @@ cargo run -p pd-edge --example http_proxy_perf_framework --release -- \
254254
--requests 3000 \
255255
--warmup-requests 300 \
256256
--concurrency 64 \
257-
--fuel-latency-fuels "1,8,64,512,4096,50000" \
257+
--fuel-latency-fuels "1,2,4,8,10,16,32,64,512,4096,50000" \
258258
--fuel-latency-check-intervals "1,4,16,64" \
259259
--json-out target/http_proxy_fuel_sweep_threading.json
260260
```
@@ -265,80 +265,80 @@ Second harness (VM-only microbenchmark in `pd-vm`):
265265
cargo test -p pd-vm --release --test jit_tests perf_cooperative_fuel_configuration_impacts_latency -- --ignored --nocapture
266266
```
267267

268-
### Latest Snapshot (2026-03-07, Local Windows x86_64 Dev Machine)
268+
### Latest Snapshot (2026-03-11, Local Windows x86_64 Dev Machine)
269269

270270
Harness A: HTTP end-to-end (`pd-edge/examples/http_proxy_perf_framework.rs`)
271271

272272
| Scenario | Async (RPS / p50 / p95 / p99 ms) | Threading (RPS / p50 / p95 / p99 ms) |
273273
|---|---:|---:|
274-
| `raw_no_program` | `80,753 / 1.485 / 2.612 / 3.195` | `84,774 / 1.414 / 2.544 / 3.207` |
275-
| `no_host_calls_program` | `31,214 / 3.918 / 6.972 / 8.831` | `26,598 / 3.812 / 10.454 / 30.124` |
276-
| `host_calls_terminate` | `25,383 / 4.768 / 8.925 / 11.742` | `27,021 / 4.172 / 9.454 / 14.503` |
274+
| `raw_no_program` | `98,941 / 1.233 / 2.032 / 2.523` | `104,751 / 1.144 / 1.983 / 2.433` |
275+
| `no_host_calls_program` | `43,846 / 2.801 / 4.866 / 5.945` | `42,757 / 2.613 / 6.357 / 9.942` |
276+
| `host_calls_terminate` | `42,250 / 2.937 / 5.002 / 6.171` | `41,393 / 2.749 / 6.115 / 9.715` |
277277

278278
Fuel sweep (fixed interval `1`, scenario `no_host_calls_program`):
279279

280280
| Fuel | Async (p50 / p95 / p99 ms / RPS) | Threading (p50 / p95 / p99 ms / RPS) |
281281
|---:|---:|---:|
282-
| `1` | `error` | `error` |
283-
| `8` | `error` | `error` |
284-
| `64` | `2.942 / 6.614 / 9.705 / 18,565` | `3.717 / 5.890 / 8.271 / 16,682` |
285-
| `512` | `1.845 / 3.838 / 5.393 / 30,841` | `1.953 / 6.535 / 11.394 / 24,239` |
286-
| `4096` | `1.575 / 3.193 / 4.811 / 36,327` | `1.577 / 9.448 / 15.266 / 24,069` |
287-
| `50000` | `1.663 / 3.420 / 4.755 / 34,399` | `2.275 / 6.642 / 12.567 / 22,095` |
282+
| `1` | `105.298 / 138.863 / 146.916 / 600` | `76.729 / 124.234 / 135.187 / 715` |
283+
| `8` | `9.134 / 11.424 / 27.760 / 6,688` | `9.254 / 15.927 / 27.765 / 5,827` |
284+
| `64` | `2.054 / 3.890 / 5.488 / 27,917` | `2.156 / 3.773 / 4.252 / 27,694` |
285+
| `512` | `1.376 / 2.521 / 3.460 / 42,025` | `1.501 / 2.678 / 3.070 / 39,060` |
286+
| `4096` | `1.172 / 3.052 / 4.685 / 44,031` | `1.319 / 3.478 / 4.366 / 40,453` |
287+
| `50000` | `1.423 / 2.424 / 2.990 / 42,500` | `1.073 / 3.328 / 6.703 / 44,639` |
288288

289289
Fuel-check-interval sweep (fixed fuel `50000`, scenario `no_host_calls_program`):
290290

291291
| Interval | Async (p50 / p95 / p99 ms / RPS) | Threading (p50 / p95 / p99 ms / RPS) |
292292
|---:|---:|---:|
293-
| `1` | `1.668 / 3.005 / 4.271 / 35,320` | `2.095 / 5.502 / 11.535 / 24,528` |
294-
| `4` | `1.682 / 2.890 / 3.966 / 35,947` | `1.695 / 5.410 / 10.466 / 28,842` |
295-
| `16` | `1.711 / 3.021 / 4.313 / 34,910` | `1.531 / 4.711 / 8.795 / 32,238` |
296-
| `64` | `1.808 / 3.670 / 5.534 / 32,247` | `1.356 / 4.129 / 9.361 / 35,537` |
293+
| `1` | `1.491 / 2.738 / 3.577 / 39,865` | `1.048 / 3.239 / 6.844 / 45,973` |
294+
| `4` | `1.391 / 2.585 / 3.354 / 42,300` | `1.077 / 3.207 / 5.883 / 45,722` |
295+
| `16` | `1.350 / 2.438 / 3.047 / 43,372` | `1.077 / 3.184 / 6.435 / 45,500` |
296+
| `64` | `1.324 / 2.328 / 2.987 / 45,168` | `1.044 / 3.111 / 6.276 / 46,496` |
297297

298298
Harness B: VM microbenchmark (`pd-vm/tests/jit/perf_tests.rs::perf_cooperative_fuel_configuration_impacts_latency`)
299299

300-
Baseline (`fuel disabled`): median latency `42,667 us`
300+
Baseline (`fuel disabled`): median latency `16,487 us`
301301

302302
Fuel sweep (`fixed_check_interval=1`):
303303

304304
| Fuel | Median Latency (us) | Median Yields | Slowdown vs Baseline |
305305
|---:|---:|---:|---:|
306-
| `1` | `76,903` | `4,100,147` | `1.80x` |
307-
| `2` | `59,548` | `2,050,073` | `1.40x` |
308-
| `4` | `49,541` | `1,025,036` | `1.16x` |
309-
| `8` | `46,577` | `512,518` | `1.09x` |
310-
| `16` | `43,663` | `256,259` | `1.02x` |
311-
| `32` | `43,189` | `128,129` | `1.01x` |
312-
| `64` | `42,624` | `64,064` | `1.00x` |
313-
| `128` | `42,370` | `32,032` | `0.99x` |
314-
| `256` | `41,822` | `16,016` | `0.98x` |
315-
| `512` | `42,189` | `8,008` | `0.99x` |
316-
| `1024` | `42,078` | `4,004` | `0.99x` |
317-
| `2048` | `47,115` | `2,002` | `1.10x` |
318-
| `4096` | `57,562` | `1,001` | `1.35x` |
319-
| `8192` | `56,234` | `500` | `1.32x` |
306+
| `1` | `66,886` | `4,100,091` | `4.06x` |
307+
| `2` | `48,050` | `2,050,045` | `2.91x` |
308+
| `4` | `40,954` | `1,025,022` | `2.48x` |
309+
| `8` | `37,086` | `512,511` | `2.25x` |
310+
| `16` | `35,881` | `256,255` | `2.18x` |
311+
| `32` | `34,548` | `128,127` | `2.10x` |
312+
| `64` | `33,860` | `64,063` | `2.05x` |
313+
| `128` | `33,601` | `32,031` | `2.04x` |
314+
| `256` | `33,025` | `16,015` | `2.00x` |
315+
| `512` | `33,544` | `8,007` | `2.03x` |
316+
| `1024` | `33,002` | `4,003` | `2.00x` |
317+
| `2048` | `33,222` | `2,001` | `2.02x` |
318+
| `4096` | `32,882` | `1,000` | `1.99x` |
319+
| `8192` | `32,980` | `500` | `2.00x` |
320320

321321
Fuel-check-interval sweep (`fixed_fuel=4096`):
322322

323323
| Interval | Median Latency (us) | Median Yields | Slowdown vs Baseline |
324324
|---:|---:|---:|---:|
325-
| `1` | `50,944` | `1,001` | `1.19x` |
326-
| `2` | `53,379` | `1,001` | `1.25x` |
327-
| `4` | `55,211` | `1,001` | `1.29x` |
328-
| `8` | `60,637` | `1,001` | `1.42x` |
329-
| `16` | `58,917` | `1,001` | `1.38x` |
330-
| `32` | `52,209` | `1,001` | `1.22x` |
331-
| `64` | `53,674` | `1,000` | `1.26x` |
332-
| `128` | `55,537` | `1,000` | `1.30x` |
333-
| `256` | `51,369` | `1,000` | `1.20x` |
325+
| `1` | `33,223` | `1,000` | `2.02x` |
326+
| `2` | `36,420` | `1,000` | `2.21x` |
327+
| `4` | `32,622` | `1,000` | `1.98x` |
328+
| `8` | `32,898` | `1,000` | `2.00x` |
329+
| `16` | `33,880` | `1,000` | `2.05x` |
330+
| `32` | `33,361` | `1,000` | `2.02x` |
331+
| `64` | `32,521` | `1,000` | `1.97x` |
332+
| `128` | `32,741` | `1,000` | `1.99x` |
333+
| `256` | `32,604` | `1,000` | `1.98x` |
334334

335335
Artifacts written by these runs:
336336

337337
- `target/http_proxy_perf_mode_async.json`
338338
- `target/http_proxy_perf_mode_threading.json`
339339
- `target/http_proxy_fuel_sweep_async.json`
340340
- `target/http_proxy_fuel_sweep_threading.json`
341-
- `target/pd_vm_perf_cooperative_fuel_2026-03-07.txt`
341+
- `target/pd_vm_perf_cooperative_fuel_2026-03-11.txt`
342342

343343
## ABI Source of Truth
344344

pd-vm/examples/mini_bench.rs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ fn benchmark_runtime(config: &BenchConfig) -> Result<(), String> {
347347
&aes_compiled.program,
348348
&aes_expected,
349349
config.run_trials,
350+
false,
350351
)?;
351352
}
352353
Err(err) => {
@@ -362,6 +363,7 @@ fn benchmark_runtime(config: &BenchConfig) -> Result<(), String> {
362363
&hot_loop.program,
363364
&hot_expected,
364365
config.run_trials,
366+
true,
365367
)?;
366368
println!();
367369
Ok(())
@@ -372,6 +374,7 @@ fn benchmark_runtime_workload(
372374
program: &Program,
373375
expected_stack: &[Value],
374376
trials: usize,
377+
include_aot: bool,
375378
) -> Result<(), String> {
376379
let interpreter =
377380
measure_runtime_mode(program, expected_stack, PerfExecMode::Interpreter, trials)?;
@@ -393,14 +396,21 @@ fn benchmark_runtime_workload(
393396
average_duration(&jit.samples).as_micros(),
394397
);
395398

396-
let aot = measure_runtime_mode(program, expected_stack, PerfExecMode::Aot, trials)?;
397-
println!(
398-
" {:<16} mode={:<12} median_us={:<10} avg_us={:<10}",
399-
label,
400-
aot.mode.label(),
401-
aot.median.as_micros(),
402-
average_duration(&aot.samples).as_micros(),
403-
);
399+
if include_aot {
400+
let aot = measure_runtime_mode(program, expected_stack, PerfExecMode::Aot, trials)?;
401+
println!(
402+
" {:<16} mode={:<12} median_us={:<10} avg_us={:<10}",
403+
label,
404+
aot.mode.label(),
405+
aot.median.as_micros(),
406+
average_duration(&aot.samples).as_micros(),
407+
);
408+
} else {
409+
println!(
410+
" {:<16} mode=aot skipped known_aot_abort_on_current_head",
411+
label
412+
);
413+
}
404414
} else {
405415
println!(
406416
" {:<16} mode=jit/aot unsupported on this target",

pd-vm/src/bytecode.rs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::collections::{HashMap, hash_map};
22
use std::fmt;
33
use std::hash::{BuildHasherDefault, Hash, Hasher};
4-
use std::sync::Arc;
4+
use std::sync::{Arc, OnceLock};
55

66
pub type SharedString = Arc<String>;
77
pub type SharedArray = Arc<Vec<Value>>;
@@ -388,6 +388,54 @@ pub struct HostImport {
388388
pub return_type: ValueType,
389389
}
390390

391+
#[derive(Debug)]
392+
pub(crate) struct DecodedInstructionData {
393+
pub(crate) ldc_values: Box<[Option<Value>]>,
394+
pub(crate) jump_targets: Box<[Option<usize>]>,
395+
pub(crate) local_indices: Box<[Option<u8>]>,
396+
}
397+
398+
impl DecodedInstructionData {
399+
fn build(program: &Program) -> Self {
400+
let mut ldc_values = vec![None; program.code.len()];
401+
let mut jump_targets = vec![None; program.code.len()];
402+
let mut local_indices = vec![None; program.code.len()];
403+
let mut ip = 0usize;
404+
while ip < program.code.len() {
405+
let opcode = match OpCode::try_from(program.code[ip]) {
406+
Ok(opcode) => opcode,
407+
Err(_) => break,
408+
};
409+
match opcode {
410+
OpCode::Ldc => {
411+
if let Some(raw_index) = read_u32_at(&program.code, ip + 1)
412+
&& let Some(value) = program.constants.get(raw_index as usize)
413+
{
414+
ldc_values[ip] = Some(value.clone());
415+
}
416+
}
417+
OpCode::Br | OpCode::Brfalse => {
418+
if let Some(target) = read_u32_at(&program.code, ip + 1) {
419+
jump_targets[ip] = Some(target as usize);
420+
}
421+
}
422+
OpCode::Ldloc | OpCode::Stloc => {
423+
if let Some(index) = program.code.get(ip + 1).copied() {
424+
local_indices[ip] = Some(index);
425+
}
426+
}
427+
_ => {}
428+
}
429+
ip = ip.saturating_add(1 + opcode.operand_len());
430+
}
431+
Self {
432+
ldc_values: ldc_values.into_boxed_slice(),
433+
jump_targets: jump_targets.into_boxed_slice(),
434+
local_indices: local_indices.into_boxed_slice(),
435+
}
436+
}
437+
}
438+
391439
#[derive(Clone, Debug)]
392440
pub struct Program {
393441
pub constants: Vec<Value>,
@@ -396,6 +444,7 @@ pub struct Program {
396444
pub imports: Vec<HostImport>,
397445
pub debug: Option<crate::debug_info::DebugInfo>,
398446
pub type_map: Option<TypeMap>,
447+
decoded_instruction_data_cache: Arc<OnceLock<Arc<DecodedInstructionData>>>,
399448
}
400449

401450
impl Program {
@@ -408,6 +457,7 @@ impl Program {
408457
imports: Vec::new(),
409458
debug: None,
410459
type_map: None,
460+
decoded_instruction_data_cache: Arc::new(OnceLock::new()),
411461
}
412462
}
413463

@@ -424,6 +474,7 @@ impl Program {
424474
imports: Vec::new(),
425475
debug,
426476
type_map: None,
477+
decoded_instruction_data_cache: Arc::new(OnceLock::new()),
427478
}
428479
}
429480

@@ -441,6 +492,7 @@ impl Program {
441492
imports,
442493
debug,
443494
type_map: None,
495+
decoded_instruction_data_cache: Arc::new(OnceLock::new()),
444496
}
445497
}
446498

@@ -453,6 +505,18 @@ impl Program {
453505
self.type_map = Some(type_map);
454506
self
455507
}
508+
509+
pub(crate) fn shared_decoded_instruction_data(&self) -> Arc<DecodedInstructionData> {
510+
Arc::clone(
511+
self.decoded_instruction_data_cache
512+
.get_or_init(|| Arc::new(DecodedInstructionData::build(self))),
513+
)
514+
}
515+
}
516+
517+
fn read_u32_at(code: &[u8], offset: usize) -> Option<u32> {
518+
let bytes = code.get(offset..offset + 4)?;
519+
Some(u32::from_le_bytes(bytes.try_into().ok()?))
456520
}
457521

458522
fn infer_local_count_from_code(code: &[u8]) -> usize {

pd-vm/src/vm/jit/aot.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -182,14 +182,15 @@ impl Vm {
182182
{
183183
let bundle = decode_aot_bundle(bytes)?;
184184
let fuel_check_interval = bundle.fuel_check_interval;
185-
let mut vm = Vm::new(crate::Program {
186-
constants: bundle.constants,
187-
code: vec![AOT_PLACEHOLDER_OPCODE; bundle.code_len],
188-
local_count: bundle.local_count,
189-
imports: bundle.imports,
190-
debug: None,
191-
type_map: None,
192-
});
185+
let mut vm = Vm::new(
186+
crate::Program::with_imports_and_debug(
187+
bundle.constants,
188+
vec![AOT_PLACEHOLDER_OPCODE; bundle.code_len],
189+
bundle.imports,
190+
None,
191+
)
192+
.with_local_count(bundle.local_count),
193+
);
193194
vm.native_only_aot = true;
194195
vm.native_aot_interrupt_check_interval = Some(fuel_check_interval);
195196
vm.native_aot_interrupt_mode = bundle.interrupt_mode.map(aot_vm_interrupt_mode);

pd-vm/src/vm/mod.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ pub use self::host::{
1818
StaticHostFunction,
1919
};
2020
use self::host::{HostCallExecOutcome, VmHostFunction, WaitingHostOp};
21-
use self::superinstructions::{DecodedInstructionData, build_decoded_instruction_data};
2221
pub use crate::bytecode::{HostImport, OpCode, Program, Value, ValueType};
2322
use crate::bytecode::{StableHasher, hash_value};
2423
pub use store::Store;
@@ -210,7 +209,7 @@ pub struct Vm {
210209
stack: Vec<Value>,
211210
locals: Vec<Value>,
212211
operand_type_hints: Option<Box<[PackedOperandTypes]>>,
213-
decoded_instruction_data: DecodedInstructionData,
212+
decoded_instruction_data: Arc<crate::bytecode::DecodedInstructionData>,
214213
host_functions: Vec<VmHostFunction>,
215214
host_function_symbols: HashMap<String, u16>,
216215
builtin_overrides: HashMap<u16, u16>,
@@ -338,7 +337,7 @@ impl Vm {
338337
let program_constants_len = program.constants.len();
339338
let local_count = program.local_count;
340339
let operand_type_hints = build_operand_type_hints(program.as_ref());
341-
let decoded_instruction_data = build_decoded_instruction_data(program.as_ref());
340+
let decoded_instruction_data = program.shared_decoded_instruction_data();
342341
let epoch_handle = EpochHandle::default();
343342
let epoch_counter_ptr = epoch_handle.as_ptr() as usize;
344343
Self {

0 commit comments

Comments
 (0)