Just curious: why is static more performant than let?

Continuing the discussion from Validate User Input against predefined "list":

In your last post in the original topic, you (@Yandros) commented that static is more performant than let. Why is that? What exactly is static's equivalent in C++ or Pascal/Delphi? Is it like a global non-compile time constant variable? Something like Pascal's var in the global scope or either of C++'s const without an initializer which is a compile time constant or a non-const variable in the global scope? What is const then? Is it a compile-time constant? Like C++11's constexpr?

static is a non-compile-time global variable.

This means that you can only use stack values (e.g. [&'static str; 5]) in static context, memory allocation is not possible. Also, you can only call const functions in static context.

A "pure" (read-only) static (that is, I'm not talking abouth things like thread_local! or lazy_static!) is guaranteed to be initialized at compile time (thus requiring a const expression), and then using it just requires dereferencing a constant address.

Example

#[inline(never)]
fn print_array (at_array: &'_ [i32; 8])
{
    println!("array = {:?}", at_array[..]);
}

fn foo ()
{
    let array: [i32; 8] = [1, 2, 3, 4, 5, 6, 7, 8];
    print_array(&array);
}

In this case, in non optimized code (an optimizer could theoretically const propagate this, making let use static storage too; I can't test this right now), since array is a local (variable), it lives in the stack and must thus be initialised at runtime, each time the function is called.

That's why, since the values of the array are always the same, it is better to make it live in the static storage (e.g., .rodata section for an ELF file) where it can be initialised at compile time (it shall live in the executable, and then be loaded into memory by the loader) by declaring the variable static.

fn foo ()
{
    static ARRAY: [i32; 8] = [1, 2, 3, 4, 5, 6, 7, 8];
    print_array(&ARRAY);
}

Modulo ARRAY visibility within the containing module, above is equivalent to:

static ARRAY: [i32; 8] = [1, 2, 3, 4, 5, 6, 7, 8];
fn foo ()
{
    print_array(&ARRAY);
}

To be honest, another option would be to declare the variable const and let the compiler choose between static or stack storage; I guess that's what I should have used in the mentioned post.


The equivalent to Rust's static in C is either a private global variable (i.e., declared (anywhere) with the static modifier) or a public global variable (i.e., declared outside any function body, without the static modifier).

(Note: once I have the setup (on the phone right now) I will edit this post with the different disassemblies of foo())

2 Likes

So I am a bit confused now.

I tried a simple Playground example, and I got an error when I tried to mutate a static. When I made it a static mut, I got errors saying mutating a static is unsafe and needs an unsafe block.

What is the difference between it and a const? What benefit does it bring to the language over a const constant? Why would you need two constructs doing similar things?

Mutable global state is unsafe, since in can cause data races. But you can use a immutable static variable with interior mutability.

1 Like

I am still confused, but the nuance of exactly what the mutability rules are is actually off-topic (sorry, I didn't realise when I asked my previous question). So I will get back to the topic by asking: do I understand correctly that static variables are more performant because you always know exactly where in memory they are located, as opposed to stack values, which you have to find by reading the base pointer and do some arithmetic on it before you get the correct address to access?

I'll start a new topic for the discussion of static vs const, if I may (meaning you have time to answer me and are willing -- else I will use my StackOverflow account for the first time :wink:)

There shouldn't be any performance difference between accessing a local or static variable. However, the static variable only needs to be initialized once. Local variables have to be pushed on the stack every time the containing function is called.

On most instruction set architectures (ISAs), accessing an item at a constant offset to the current stack frame pointer is just as fast as accessing an item at a fixed location in a read-only code or data segment. It's initialization, not access, where the performance difference occurs.

2 Likes

So static in Rust used inside a function does what static in C/++ does when you use it inside a function (make the variable's lifetime static, i.e. let it live for the duration of the program)? Used outside it is like declaring a global variable in C/++?

Well, mostly, in both cases, they are treated the same, it just happens to be different places where you put them. If you put it in the public namespace (In the top of your file, outside of any nesting) then it becomes part of the module/library. If you place it into a function, it is still treated the same as if it were outside of the function, it just so happens to be that its scope is the function body. To demonstrate similar effects, look at this (valid) code:

fn main() {
    trait Bar {}
    struct Foo;
    impl Foo {}
    impl Bar for Foo {}
    mod baz {}
    const X: f32 = 0.0;
    static Bar: usize = 0;
    static mut Bar2: usize = 0;
}

The only difference between putting things inside functions is that noone can access things inside of a function's scope. You can't do something like the following:

fn main() {
    pub fn xyz() {}
}
fn bar() {
    main::xyz();
}

Because you can't access things inside of a function's scope, even if they're declared as pub.

Thanks! So the only difference between declaring a static inside a function and outside the function, is that the one inside the function is only accessible from within the function, while the one outside is accessible from some wider scope?

Also, now that I understand that one :upside_down_face:, what exactly is the difference between a static and a const in Rust? Is a const a static which is forced to be initialized at compile time?

Essentially, yes.

A const variable is copied everywhere it's mentioned, it's evaluated at compile time, and then the value is copied.

const X: usize = bar();
const fn bar() -> usize { 32 }
fn main() {
    let z = X;
    let y = &X;
}

turns into:

fn main() {
    let z = 32;
    let y = &32;
}

(And bar stays if it is needed at runtime) While with a static, it is done like so:

static X: usize = bar();
const fn bar() //...
fn main() {
    let z = X;
    let y = &X;
}

Turns into:

static X = 32;
fn main() {
    let z = 32; //usize is `Copy`
    let y = &X;
}

So to recap:

  • statics are allocated in the final program and are actual points in memory.
  • consts are copied at compile time to everywhere they are referenced.
  • Both are evaluated at compile time. And both require const fns
1 Like

So, don't know how well versed you are in C, but static X: usize = 32; is then like const int X = 32; in C and const X: usize = 32; is like #define X 32 in C? I.e. a const gets text-replaced, albeit more intelligently than C's preprocessor, like C's macro #defines and static is putting the value into some known, read-only location in memory only once and references it everywhere you use it?

A big selling point of rust to me is the lack of the c preprocessor :persevere:.
What you mentioned is almost correct, just that something like this:

//This takes 10 minutes to complete
const fn foo() -> usize {
    for i in 0..10000 {/**/}
}
const X: usize = foo();
fn main() {
    let y = X;
    let z = X;
    let w = X;
}

Would not take 30 minutes to compile, just 10.

1 Like

C'mon, the C preprocessor is quite a good tool for including code from other files into your project and for conditional compilation. Just don't use it to define macros :grimacing:

But Rust has its own way of including files and disabling code, so it did take the good parts of the C preprocessor and incorporated it :slightly_smiling_face:

Not always: C's const int X = 32; within a function's body is Rust's let X: i32 = 32;

Rust's static X: usize = 32; is, in C:

  • const int X = 32; when the Rust declaration is pub (and thus outside a function for it to matter)

    • public global variable
  • static const int X = 32; when the Rust declaration is private (non-pub outside a function, or inside a function (where pub does not matter)

    • private global variable

If the translation is weird that's because C's static keyword weirdly conveys two meanings at the same time: global and private.

Oh and something I forgot about C/C++ and rust:

const X: usize = 0;

is equivalent to

constexpr int X = 0;

and as @Yandros pointed out, const in C/C++ is equivalent to let x. Same idea with functions:

const fn foo() -> usize;

is the same idea as:

constexpr int foo(); 
1 Like

I learnt a lot from reading this thread.

To be fair, rust also has macros, with which you can do some pretty crazy things; they're just explicitly marked instead of relying on case conventions, and generally a lot more sane to work with.

Yes, but I meant that C macros are inherently looking for trouble, because it is just being dumb text replaced before compilation, while Rust's macros are a little more "intelligent" IIUC. So, with Rust, there is no reason to discourage the use of macros, while in C++, the use of macros is discouraged in most cases (except in extremely memory-constrained embedded systems).