Enhancing Rust Performance Analysis: Building a Procedural Macro for Function Execution Benchmarking

Shobhit chaturvedi
4 min readDec 26, 2023

--

In the world of software development, performance analysis plays a critical role, particularly in resource-intensive applications. Rust, known for its focus on safety and performance, offers various tools for developers to optimise their code. This article introduces an advanced technique using Rust’s procedural macros to monitor function execution times, an invaluable asset for benchmarking and performance tuning.

Step 1: Setting Up the Macro Library

First, we’ll create a new library crate specifically for our attribute macro. This macro will enable easy measurement of function execution times.

Create a new library crate using Cargo:

cargo new function_benchmark_macro --lib

Configure the crate to be a procedural macro library. In the Cargo.toml file, set the proc-macro flag to true:

[lib]
proc-macro = true

Add necessary dependencies for macro creation:


[dependencies]
syn = {version = "*", features = ["full"]}
quote="*"
proc-macro2="*"

Step 2: Developing the Procedural Macro

Now, let’s dive into the core of our project: the procedural macro for benchmarking function execution times.

  1. Define the procedural macro auto_log:
#[proc_macro_attribute]
pub fn auto_log(_attrs: TokenStream, item: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(item as syn::ItemFn);

let fn_name = &input_fn.sig.ident;
let fn_block = &input_fn.block;

let expanded = quote! {
fn #fn_name() {
let start = std::time::Instant::now();
println!("Entering function: {}", stringify!(#fn_name));
#fn_block
println!("Exiting function: {} (took {} ms)", stringify!(#fn_name), start.elapsed().as_millis());
}
};

TokenStream::from(expanded)
}

This macro, when applied, wraps the function body with time tracking logic, capturing the duration of its execution.

Step 3: Testing the Macro

To verify our macro’s functionality, we’ll create a test project.

Create a test project

Create a new Cargo project:

cargo new test_macro

Add the path to the function_benchmark_macro in its Cargo.toml:

[dependencies]
function_benchmark_macro ={ path="../function_benchmark_macro"}

Implement a test function using the auto_log macro:

#[auto_log]
fn log_function() {
for num in 0..10 {
thread::sleep(time::Duration::from_secs(1));
println!("{}", num);
}
}

call function and check peromance time of log function from main

fn main() {
log_function();
}

Upon execution, the output will indicate the time taken by log_function.

output will be

Entering function: log_function
0
1
2
3
4
5
6
7
8
9
Exiting function: log_function (took 10038 ms)

Conclusion:

The auto_log procedural macro is a powerful tool for Rust developers, simplifying the process of performance benchmarking. By automating the time-tracking of function executions, developers can efficiently identify bottlenecks and optimize their code, especially in CPU or network-intensive operations. Additionally, this approach can be seamlessly integrated into unit tests without affecting production code.

This hands-on guide demonstrates the creation of a procedural macro in Rust, a technique that not only bolsters performance analysis but also exemplifies Rust’s extensibility and efficiency as a programming language.

some other application of procedural macros are

  1. Conditional compilation for function

You can create an attribute macro that enables conditional compilation of functions based on certain criteria, such as feature flags, environment variables, or custom logic.

Example: Feature-based Conditional Compilation

// In your macro crate

#[proc_macro_attribute]
pub fn conditional_compile(attrs: TokenStream, item: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(item as ItemFn);
let attr_args = parse_macro_input!(attrs as AttributeArgs);

// Logic to determine if the function should be included
let should_include = ...; // Determine based on attr_args

let fn_name = &input_fn.sig.ident;
let fn_block = &input_fn.block;

let expanded = if should_include {
quote! {
fn #fn_name() {
#fn_block
}
}
} else {
quote! {} // Exclude the function
};

TokenStream::from(expanded)
}

In this example, the macro decides whether to include the function based on the attributes provided.

2. Auto implementing Traits based attributes

You can use attribute-like macros to implement certain traits for structs or enums based on custom attributes.

Example: Auto-implement a Serialization Trait

// In your macro crate

#[proc_macro_attribute]
pub fn auto_serialize(_attrs: TokenStream, item: TokenStream) -> TokenStream {
let input_struct = parse_macro_input!(item as ItemStruct);

let struct_name = &input_struct.ident;

let expanded = quote! {
// Implement a serialization trait for the struct
impl Serialize for #struct_name {
fn serialize(&self) -> String {
// Serialization logic
}
}

#input_struct // Keep the original struct definition
};

TokenStream::from(expanded)
}

This macro automatically implements a custom Serialize trait for any struct it's applied to.

Appendix: Full Code Listings

here is full code files and toml file for macro crate and test_macro crate

Procedure Macro Crate (Cargo.toml and src/lib.rs)

function_benchmark_macro/Cargo.toml
name = "function_benchmark_macro"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
proc-macro = true

[dependencies]
syn = {version = "*", features = ["full"]}
quote="*"
proc-macro2="*"
function_benchmark_macro/src/lib.rs
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_attribute]
pub fn auto_log(_attrs: TokenStream, item: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(item as syn::ItemFn);

let fn_name = &input_fn.sig.ident;
let fn_block = &input_fn.block;

let expanded = quote! {
fn #fn_name() {
let start = std::time::Instant::now();
println!("Entering function: {}", stringify!(#fn_name));
#fn_block
println!("Exiting function: {} (took {} ms)", stringify!(#fn_name), start.elapsed().as_millis());
}
};

TokenStream::from(expanded)
}

Test Macro Crate (Cargo.toml and src/main.rs)

test_macro_crate/Cargo.toml
name = "test_macro_crate"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
function_benchmark_macro ={ path="../function_benchmark_macro"}
test_macro_crate/src/main.rs
use core::time;
use std::thread;

use function_benchmark_macro::auto_log;
#[auto_log]
fn log_function() {
for num in 0..10 {
thread::sleep(time::Duration::from_secs(1));
println!("{}", num);
}
}

fn main() {
log_function();
}

--

--