1

In this code

fn do_something_under_lock(
    is_locked: &AtomicBool,
) {
    loop {
        let lock_acquired = is_locked
            .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
            .is_ok();

        if lock_acquired {
            // Do something
            // ...

            is_locked.store(false, Ordering::Release);
            break;
        }
    }
}

will moving let outside of loop, like

fn do_something_under_lock(
    is_locked: &AtomicBool,
) {
    let mut lock_acquired;
    loop {
        lock_acquired = is_locked
            .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
            .is_ok();

        if lock_acquired {
            // Do something
            // ...

            is_locked.store(false, Ordering::Release);
            break;
        }
    }
}

give some performance benefit, will it change when Drop is called(according to https://doc.rust-lang.org/rust-by-example/trait/drop.html#drop

The Drop trait only has one method: drop, which is called automatically when an object goes out of scope. The main use of the Drop trait is to free the resources that the implementor instance owns.

and at first glance, scope of lock_acquired changed ), or will compiler produce same code anyway? What is the best way to check compiler output?

I expect compiler to understand that developer intent is the same and produce the same binary

Ivan Psov
  • 11
  • 4

1 Answers1

5

In theory the first code allocates space on the stack for a new boolean each time and removes it at the end of the loop body. The second code allocates a new boolean once and removes it at the end of the function.

However, the compiler will almost certainly compile these to the same instructions, using the same location on the stack if it uses the stack, but more likely using a register for the boolean.

The first code is also easier for a human to read, since it is clear that the value of the boolean is only set for the duration of the loop body and does not affect subsequent iterations.

Of more concern for performance, this code will spin in a tight loop while another thread has the lock, which may affect the speed at which the other thread gets to unlock. I'd recommend using existing crates that provide locking (std::sync or parking_lot).

Jonathan Giddy
  • 1,435
  • 10
  • 6
  • 1
    The compiler will always reuse the stack storage, but it is not easier for it to reason about the first than the second. – Chayim Friedman Jul 03 '23 at 05:57
  • I feel like it's important to note that values get dropped, not variables. So yes, the final value assigned to `lock_acquired` would (if it weren't `Copy`) get dropped later, but all values in-between would get dropped as they get re-assigned. – isaactfa Jul 03 '23 at 08:09
  • I think local variables on the stack are always allocated when entering the function, regardless of where they are declared in the source code. I don't think the first code "allocates space on the stack for a new boolean each time", not even "in theory". It's just not how it works. – Sven Marnach Jul 03 '23 at 09:46
  • @SvenMarnach One could argue that it does allocate space each time in theory, but I agree that this is not even present as an optimization in the compiler, that's just how MIR is built. – Chayim Friedman Jul 04 '23 at 16:47
  • @ChayimFriedman It's also how even old, non-optimizing C compilers back in the nineties have alays done it – the stack frame is created when entering the function, and it's not touched during its execution (except for other function calls). In old versions of C, and in languages like Pascal or Modula 2, all variables needed to be declared at or before the beginning of the function body. Later, C and C++ allowed introducing variables in the middle of a function, but that only caused a logical reduction of the scope. The emitted code was still identical to having all variables declared up front. – Sven Marnach Jul 04 '23 at 19:31