#const #proc-macro #currying #up #procedural #value #speeding

macro const-currying

A procedural macro for speeding up your function using currying

5 releases

0.0.5 Apr 6, 2024
0.0.4 Apr 1, 2024
0.0.3 Mar 20, 2024
0.0.2 Mar 20, 2024
0.0.1 Mar 19, 2024

#166 in Procedural macros

Download history 344/week @ 2024-03-18 2151/week @ 2024-04-01 3847/week @ 2024-04-08 4058/week @ 2024-04-15 3719/week @ 2024-04-22

13,775 downloads per month

Apache-2.0 OR MIT

17KB
315 lines

Latest Version Documentation

Const-currying

A rust proc macro that help you improve your function's performance using curring techniques.

Motivation

Const generics is a feature of rust that allows you to pass const values as generic parameters. This feature is very useful to improve the performance of your code, by generating multiple versions of the same function at compile time. However, sometimes you have to call the function with a runtime-dependent value, even if it's likely a known const value, or a part of the calls are with const values. This macro helps you to generate multiple versions of the same function, with either const or runtime values, at compile time.

There is an excited crate partial_const, which already provides a similar feature. It's fully based on the wonderful type system of rust, however, it requires caller to specify the const value explicitly. Our crate provides a proc-macro based solution, which introduces no invasive changes to the caller's code.

Usage

You can specify the potential constant values of the function's arguments using maybe_const attribute.

use const_currying::const_currying;

#[const_currying]
fn f1(
    #[maybe_const(dispatch = x, consts = [0, 1])] x: i32,
    #[maybe_const(dispatch = y, consts = [true, false])] y: bool,
    z: &str,
) -> i32 {
    if y {
        x
    } else {
        -x
    }
}

There are two arguments x and y which are potentially passed as const values. The optional dispatch attribute specifies the suffix of the generated function name. As an example, we can see the full generated codes here.

#[allow(warnings)]
fn f1_orig(x: i32, y: bool, z: &str) -> (i32, String) {
    if y { (x, z.to_string()) } else { (-x, z.chars().rev().collect()) }
}
#[allow(warnings)]
fn f1_x<const x: i32>(y: bool, z: &str) -> (i32, String) {
    if y { (x, z.to_string()) } else { (-x, z.chars().rev().collect()) }
}
#[allow(warnings)]
fn f1_y<const y: bool>(x: i32, z: &str) -> (i32, String) {
    if y { (x, z.to_string()) } else { (-x, z.chars().rev().collect()) }
}
#[allow(warnings)]
fn f1_x_y<const x: i32, const y: bool>(z: &str) -> (i32, String) {
    if y { (x, z.to_string()) } else { (-x, z.chars().rev().collect()) }
}
#[inline(always)]
fn f1(x: i32, y: bool, z: &str) -> (i32, String) {
    match (x, y) {
        (1, false) => f1_x_y::<1, false>(z),
        (1, true) => f1_x_y::<1, true>(z),
        (0, false) => f1_x_y::<0, false>(z),
        (0, true) => f1_x_y::<0, true>(z),
        (x, false) => f1_y::<false>(x, z),
        (x, true) => f1_y::<true>(x, z),
        (1, y) => f1_x::<1>(y, z),
        (0, y) => f1_x::<0>(y, z),
        (x, y) => f1_orig(x, y, z),
    }
}

The original function f1 is renamed to f1_orig, and a powerset of two const arguments x and y are generated as different functions f1_x, f1_y and f1_x_y. Finally, the original function f1 is replaced by a dispatcher function, which calls the generated functions according to the runtime values of x and y.

Benifits

In most cases, the compiler optimization is trustworth enough to generate best codes for you. However, when your function is too complicated to inline, the compiler may not be able to get enough information to optimize the function.

The macro generates multiple versions of the function, and matches the consts argument explicitly in the dispatcher function. This enforces the compiler to generate the best codes for each const value. This is also why const-generics is introduced to rust, and the macro make it easier to use with a runtime-dependent value.

"No silver bullet" is a well-known principle in software engineering. The macro is not a silver bullet, and it's not always the best choice to use it. At least, it may heavily increase you binary size. You should always profile your code before and after using the macro, and make sure the performance is improved.

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option.

Dependencies

~1–1.6MB
~34K SLoC