Skip to content

Commit abba399

Browse files
authored
perf: only do exit check for instructions that can exit (#2126)
- update `create_tco_handler` macro to `create_handler` which now automatically sets `exit_code` based on whether the execute impl returns `Result::Err`. it acts as a simple wrapper for execute impls that don't return a `Result` - only do exit checks for executors that can exit in tco mode i.e. for execute impls that return `Result` - feature gate all non-tco functions with `#[cfg(not(feature = "tco"))]` [benchmark comparison](https://github.com/axiom-crypto/openvm-reth-benchmark/actions/runs/17590682650#summary-49971529404) Towards INT-4921
1 parent 2e6631f commit abba399

File tree

46 files changed

+700
-480
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+700
-480
lines changed

crates/toolchain/tests/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ num-bigint.workspace = true
3838
default = ["parallel"]
3939
parallel = ["openvm-circuit/parallel"]
4040
cuda = [ "openvm-rv32im-circuit/cuda" ]
41+
tco = ["openvm-circuit/tco"]
4142
touchemall = [
4243
"cuda",
4344
"openvm-circuit/touchemall",

crates/vm/derive/src/common.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use quote::{format_ident, quote};
2+
use syn::{Ident, ItemFn};
3+
4+
/// Extract the first two generic type parameters (F and CTX) from function generics
5+
pub fn extract_f_and_ctx_types(generics: &syn::Generics) -> (syn::Ident, syn::Ident) {
6+
let mut type_params = generics.params.iter().filter_map(|param| {
7+
if let syn::GenericParam::Type(type_param) = param {
8+
Some(&type_param.ident)
9+
} else {
10+
None
11+
}
12+
});
13+
14+
let f_type = type_params
15+
.next()
16+
.expect("Function must have at least one type parameter (F)")
17+
.clone();
18+
let ctx_type = type_params
19+
.next()
20+
.expect("Function must have at least two type parameters (F and CTX)")
21+
.clone();
22+
23+
(f_type, ctx_type)
24+
}
25+
26+
/// Build a list of generic arguments for function calls
27+
pub fn build_generic_args(generics: &syn::Generics) -> Vec<proc_macro2::TokenStream> {
28+
generics
29+
.params
30+
.iter()
31+
.map(|param| match param {
32+
syn::GenericParam::Type(type_param) => {
33+
let ident = &type_param.ident;
34+
quote! { #ident }
35+
}
36+
syn::GenericParam::Lifetime(lifetime) => {
37+
let lifetime = &lifetime.lifetime;
38+
quote! { #lifetime }
39+
}
40+
syn::GenericParam::Const(const_param) => {
41+
let ident = &const_param.ident;
42+
quote! { #ident }
43+
}
44+
})
45+
.collect()
46+
}
47+
48+
/// Generate handler name from function name:
49+
/// If original ends with `_impl`, replace with `_handler`, else append `_handler` suffix.
50+
pub fn handler_name_from_fn(fn_name: &Ident) -> Ident {
51+
let new_name_str = fn_name
52+
.to_string()
53+
.strip_suffix("_impl")
54+
.map(|base| format!("{base}_handler"))
55+
.unwrap_or_else(|| format!("{fn_name}_handler"));
56+
format_ident!("{}", new_name_str)
57+
}
58+
59+
/// Check if function returns Result type
60+
pub fn returns_result_type(input_fn: &ItemFn) -> bool {
61+
match &input_fn.sig.output {
62+
syn::ReturnType::Type(_, ty) => {
63+
matches!(**ty, syn::Type::Path(ref path) if path.path.segments.last().is_some_and(|seg| seg.ident == "Result"))
64+
}
65+
_ => false,
66+
}
67+
}

crates/vm/derive/src/lib.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ use syn::{
99
GenericParam, Ident, Meta, Token,
1010
};
1111

12+
mod common;
13+
#[cfg(not(feature = "tco"))]
14+
mod nontco;
1215
#[cfg(feature = "tco")]
1316
mod tco;
1417

@@ -48,7 +51,7 @@ pub fn preflight_executor_derive(input: TokenStream) -> TokenStream {
4851
_ => panic!("Only unnamed fields are supported"),
4952
};
5053
// Use full path ::openvm_circuit... so it can be used either within or outside the vm
51-
// crate.
54+
// crate. Assume F is already generic of the field.
5255
let where_clause = new_generics.make_where_clause();
5356
where_clause.predicates.push(
5457
syn::parse_quote! { #inner_ty: ::openvm_circuit::arch::PreflightExecutor<#field_ty_generic, RA> },
@@ -85,7 +88,7 @@ pub fn preflight_executor_derive(input: TokenStream) -> TokenStream {
8588
})
8689
.collect::<Vec<_>>();
8790
// Use full path ::openvm_circuit... so it can be used either within or outside the vm
88-
// crate. Assume F is already generic of the field.
91+
// crate.
8992
let (execute_arms, get_opcode_name_arms, where_predicates): (Vec<_>, Vec<_>, Vec<_>) =
9093
multiunzip(variants.iter().map(|(variant_name, field)| {
9194
let field_ty = &field.ty;
@@ -183,6 +186,7 @@ pub fn executor_derive(input: TokenStream) -> TokenStream {
183186
fn pre_compute_size(&self) -> usize {
184187
self.0.pre_compute_size()
185188
}
189+
#[cfg(not(feature = "tco"))]
186190
#[inline(always)]
187191
fn pre_compute<Ctx>(
188192
&self,
@@ -281,6 +285,7 @@ pub fn executor_derive(input: TokenStream) -> TokenStream {
281285
}
282286
}
283287

288+
#[cfg(not(feature = "tco"))]
284289
#[inline(always)]
285290
fn pre_compute<Ctx>(
286291
&self,
@@ -325,7 +330,7 @@ pub fn metered_executor_derive(input: TokenStream) -> TokenStream {
325330
_ => panic!("Only unnamed fields are supported"),
326331
};
327332
// Use full path ::openvm_circuit... so it can be used either within or outside the vm
328-
// crate. Assume F is already generic of the field.
333+
// crate.
329334
let mut new_generics = generics.clone();
330335
let where_clause = new_generics.make_where_clause();
331336
where_clause
@@ -357,6 +362,7 @@ pub fn metered_executor_derive(input: TokenStream) -> TokenStream {
357362
fn metered_pre_compute_size(&self) -> usize {
358363
self.0.metered_pre_compute_size()
359364
}
365+
#[cfg(not(feature = "tco"))]
360366
#[inline(always)]
361367
fn metered_pre_compute<Ctx>(
362368
&self,
@@ -457,6 +463,7 @@ pub fn metered_executor_derive(input: TokenStream) -> TokenStream {
457463
}
458464
}
459465

466+
#[cfg(not(feature = "tco"))]
460467
#[inline(always)]
461468
fn metered_pre_compute<Ctx>(
462469
&self,
@@ -832,13 +839,13 @@ fn parse_executor_type(
832839
/// unless the execute_impl returns an error. This is done for performance to skip an exit code
833840
/// check.
834841
#[proc_macro_attribute]
835-
pub fn create_tco_handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
842+
pub fn create_handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
836843
#[cfg(feature = "tco")]
837844
{
838845
tco::tco_impl(item)
839846
}
840847
#[cfg(not(feature = "tco"))]
841848
{
842-
item
849+
nontco::nontco_impl(item)
843850
}
844851
}

crates/vm/derive/src/nontco.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
use proc_macro::TokenStream;
2+
use quote::quote;
3+
use syn::{parse_macro_input, ItemFn};
4+
5+
use crate::common::{
6+
build_generic_args, extract_f_and_ctx_types, handler_name_from_fn, returns_result_type,
7+
};
8+
9+
/// Implementation of the non-TCO handler generation logic.
10+
/// This is called from the proc macro attribute in lib.rs.
11+
pub fn nontco_impl(item: TokenStream) -> TokenStream {
12+
// Parse the input function
13+
let input_fn = parse_macro_input!(item as ItemFn);
14+
15+
// Extract information from the function
16+
let fn_name = &input_fn.sig.ident;
17+
let generics = &input_fn.sig.generics;
18+
let where_clause = &generics.where_clause;
19+
20+
// Check if function returns Result
21+
let returns_result = returns_result_type(&input_fn);
22+
23+
// Extract the first two generic type parameters (F and CTX)
24+
let (f_type, ctx_type) = extract_f_and_ctx_types(generics);
25+
26+
// Derive new function name:
27+
// If original ends with `_impl`, replace with `_handler`, else append suffix.
28+
let handler_name = handler_name_from_fn(fn_name);
29+
30+
// Build the function call with all the generics
31+
let generic_args = build_generic_args(generics);
32+
let execute_call = if generic_args.is_empty() {
33+
quote! { #fn_name(pre_compute, instret, pc, arg, exec_state) }
34+
} else {
35+
quote! { #fn_name::<#(#generic_args),*>(pre_compute, instret, pc, arg, exec_state) }
36+
};
37+
38+
// Generate the execute and exit check code based on return type
39+
let handler_body = if returns_result {
40+
quote! {
41+
// Call original impl and wire errors into exit_code.
42+
let __ret = { #execute_call };
43+
if let ::core::result::Result::Err(e) = __ret {
44+
exec_state.set_instret_and_pc(*instret, *pc);
45+
exec_state.exit_code = ::core::result::Result::Err(e);
46+
return;
47+
}
48+
}
49+
} else {
50+
quote! {
51+
#execute_call;
52+
}
53+
};
54+
55+
// Generate the non-TCO handler function
56+
let handler_fn = quote! {
57+
#[inline(always)]
58+
unsafe fn #handler_name #generics (
59+
pre_compute: &[u8],
60+
instret: &mut u64,
61+
pc: &mut u32,
62+
arg: u64,
63+
exec_state: &mut ::openvm_circuit::arch::VmExecState<
64+
#f_type,
65+
::openvm_circuit::system::memory::online::GuestMemory,
66+
#ctx_type,
67+
>,
68+
)
69+
#where_clause
70+
{
71+
#handler_body
72+
}
73+
};
74+
75+
// Return both the original function and the new handler
76+
let output = quote! {
77+
#input_fn
78+
79+
#handler_fn
80+
};
81+
82+
TokenStream::from(output)
83+
}

crates/vm/derive/src/tco.rs

Lines changed: 27 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
use proc_macro::TokenStream;
2-
use quote::{format_ident, quote};
2+
use quote::quote;
33
use syn::{parse_macro_input, ItemFn};
44

5+
use crate::common::{
6+
build_generic_args, extract_f_and_ctx_types, handler_name_from_fn, returns_result_type,
7+
};
8+
59
/// Implementation of the TCO handler generation logic.
610
/// This is called from the proc macro attribute in lib.rs.
711
pub fn tco_impl(item: TokenStream) -> TokenStream {
@@ -13,16 +17,15 @@ pub fn tco_impl(item: TokenStream) -> TokenStream {
1317
let generics = &input_fn.sig.generics;
1418
let where_clause = &generics.where_clause;
1519

20+
// Check if function returns Result
21+
let returns_result = returns_result_type(&input_fn);
22+
1623
// Extract the first two generic type parameters (F and CTX)
1724
let (f_type, ctx_type) = extract_f_and_ctx_types(generics);
25+
1826
// Derive new function name:
19-
// If original ends with `_impl`, replace with `_tco_handler`, else append suffix.
20-
let new_name_str = fn_name
21-
.to_string()
22-
.strip_suffix("_impl")
23-
.map(|base| format!("{base}_tco_handler"))
24-
.unwrap_or_else(|| format!("{fn_name}_tco_handler"));
25-
let handler_name = format_ident!("{}", new_name_str);
27+
// If original ends with `_impl`, replace with `_handler`, else append suffix.
28+
let handler_name = handler_name_from_fn(fn_name);
2629

2730
// Build the generic parameters for the handler, preserving all original generics
2831
let handler_generics = generics.clone();
@@ -35,6 +38,21 @@ pub fn tco_impl(item: TokenStream) -> TokenStream {
3538
quote! { #fn_name::<#(#generic_args),*>(pre_compute, &mut instret, &mut pc, arg, exec_state) }
3639
};
3740

41+
// Generate the execute and exit check code based on return type
42+
let execute_stmt = if returns_result {
43+
quote! {
44+
// Call original impl and wire errors into exit_code.
45+
let __ret = { #execute_call };
46+
if let ::core::result::Result::Err(e) = __ret {
47+
exec_state.set_instret_and_pc(instret, pc);
48+
exec_state.exit_code = ::core::result::Result::Err(e);
49+
return;
50+
}
51+
}
52+
} else {
53+
quote! { #execute_call; }
54+
};
55+
3856
// Generate the TCO handler function
3957
let handler_fn = quote! {
4058
#[inline(never)]
@@ -54,12 +72,8 @@ pub fn tco_impl(item: TokenStream) -> TokenStream {
5472
use ::openvm_circuit::arch::ExecutionError;
5573

5674
let pre_compute = interpreter.get_pre_compute(pc);
57-
#execute_call;
75+
#execute_stmt
5876

59-
if ::core::intrinsics::unlikely(exec_state.exit_code.is_err()) {
60-
exec_state.set_instret_and_pc(instret, pc);
61-
return;
62-
}
6377
if ::core::intrinsics::unlikely(#ctx_type::should_suspend(instret, pc, arg, exec_state)) {
6478
exec_state.set_instret_and_pc(instret, pc);
6579
return;
@@ -89,45 +103,3 @@ pub fn tco_impl(item: TokenStream) -> TokenStream {
89103

90104
TokenStream::from(output)
91105
}
92-
93-
fn extract_f_and_ctx_types(generics: &syn::Generics) -> (syn::Ident, syn::Ident) {
94-
let mut type_params = generics.params.iter().filter_map(|param| {
95-
if let syn::GenericParam::Type(type_param) = param {
96-
Some(&type_param.ident)
97-
} else {
98-
None
99-
}
100-
});
101-
102-
let f_type = type_params
103-
.next()
104-
.expect("Function must have at least one type parameter (F)")
105-
.clone();
106-
let ctx_type = type_params
107-
.next()
108-
.expect("Function must have at least two type parameters (F and CTX)")
109-
.clone();
110-
111-
(f_type, ctx_type)
112-
}
113-
114-
fn build_generic_args(generics: &syn::Generics) -> Vec<proc_macro2::TokenStream> {
115-
generics
116-
.params
117-
.iter()
118-
.map(|param| match param {
119-
syn::GenericParam::Type(type_param) => {
120-
let ident = &type_param.ident;
121-
quote! { #ident }
122-
}
123-
syn::GenericParam::Lifetime(lifetime) => {
124-
let lifetime = &lifetime.lifetime;
125-
quote! { #lifetime }
126-
}
127-
syn::GenericParam::Const(const_param) => {
128-
let ident = &const_param.ident;
129-
quote! { #ident }
130-
}
131-
})
132-
.collect()
133-
}

crates/vm/src/arch/execution.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ pub type Handler<F, CTX> = unsafe fn(
129129
pub trait Executor<F> {
130130
fn pre_compute_size(&self) -> usize;
131131

132+
#[cfg(not(feature = "tco"))]
132133
fn pre_compute<Ctx>(
133134
&self,
134135
pc: u32,
@@ -160,6 +161,7 @@ pub trait Executor<F> {
160161
pub trait MeteredExecutor<F> {
161162
fn metered_pre_compute_size(&self) -> usize;
162163

164+
#[cfg(not(feature = "tco"))]
163165
fn metered_pre_compute<Ctx>(
164166
&self,
165167
air_idx: usize,

0 commit comments

Comments
 (0)