5

I have a function of type

f: fn(x: SomeType, y: Arc<()>) -> ISupposeTheReturnTypeDoesNotMatter

when compiled (with or without optimization), will the y be optimized away?

The intention of the y is to limit the number of the running instances of f, if y is referenced too many times, the caller of f will not call f until the reference count of y becomes lower.

Edit: clarification on my intention

The intention is to keep the number of running http requests (represented by the above f) in control, the pseudo code looks like this:

let y = Arc::new(());
let all_jobs_to_be_done = vector_of_many_jobs;
loop {
    while y.strong_count() < some_predefined_limit {
        // we have some free slots, fill them up with instances of f,
        // f is passed with a clone of y,
        // so that the running of f would increase the ref count,
        // and the death of the worker thread would decrease the ref count
        let work = all_jobs_to_be_done.pop();
        let ticket = y.clone();
        spawn_work(move || {f(work, ticket)});
    }

    sleep_for_a_few_seconds();
}

The reason for this seemingly hacky work around is that I cannot find a library that meets my needs (consume a changing work queue with bounded amount of async (tokio) workers, and requeue the work if the job fails)

Incömplete
  • 853
  • 8
  • 20
  • Sounds like an interesting question, write a small test to investigate how it behaves – Simson Sep 02 '20 at 01:20
  • 4
    I prefer specification over observation. For debug build, the observation is that it won't be optimized away, but this could also mean that I didn't meet the requirement of the optimizer, perhaps for some other code arrangement, it will be optimized away. – Incömplete Sep 02 '20 at 01:23
  • The debug build will not optimize anything away unless you manually specify a higher level of optimization. – Herohtar Sep 02 '20 at 01:50
  • 1
    With all the refcount-limitation logic in the caller, why pass the `Arc` to the callee? As you say, it’s unused there... I don’t understand the purpose of keeping it in the function’s signature. – eggyal Sep 02 '20 at 02:55
  • @eggyal To take advantage of the move semantics and variable lifetime, see my edits. – Incömplete Sep 02 '20 at 12:07
  • Understood. @Acorn’s answer points out that side effects such as decrementing the ref count (which occurs when the `Arc` is dropped) necessitates that it be passed to the function. Of course, the optimiser could then determine that it may be dropped immediately upon entering the function rather than holding it until the function terminates, but if necessary you could ensure correct behaviour by explicitly invoking `std::mem::drop` at the desired point. – eggyal Sep 02 '20 at 12:29
  • In fact, you could achieve the same without passing the `Arc` to the function, but by dropping it within the spawned closure after the function returns. – eggyal Sep 02 '20 at 13:01

1 Answers1

6

Will Rust optimize away unused function arguments?

Yes, LLVM (the backend for rustc) is able to optimize away unused variables when removing them does not change program behavior, although nothing guarantees it will do it. rustc has some passes before LLVM too, but the same applies.

Knowing what exactly counts as program behavior is tricky business. However, multi-threaded primitives used in refcounting mechanics are usually the sort of thing that cannot be optimized away for good reason. Refer to the Rust reference for more information (other resources that might help are the nomicon, the different GitHub repos, the Rust fora, the C++11 memory model which Rust uses, etc.).

On the other hand, if you are asking about what are the semantics of the language when it encounters unused parameters, then no, Rust does not ignore them (and hopefully never will!).

will the y be optimized away?

No, it is a type with side effects. For instance, dropping it requires running non-trivial code.

The intention of the y is to limit the number of the running instances of f

Such an arrangement does not limit how many threads are running f since Arc is not a mutex and, even if it were some kind of mutex, you could construct as many independent ones as you wanted.

Acorn
  • 24,970
  • 5
  • 40
  • 69
  • Thanks for the clarification. The limitation is done manually, one the caller of f, there is something like this `if y.strong_count() > some_number then wait_until_ref_count_lowers() else f(x, y.clone())`, The limitation is not a hard limit, as long as it around a certain number, it's ok. – Incömplete Sep 02 '20 at 02:34
  • 1
    You're welcome! If the semaphore logic is outside, then there is no need to pass it to `f`. – Acorn Sep 02 '20 at 02:44
  • @Acorn FWIW while Arc does have side-effects, [the function could be inlined and arc-cloning in the parent removed entirely](https://godbolt.org/z/vcYv4T). If you remove the `-O` you can see the call to `Arc::clone` in the compiled output, but with `-O` the entire thing goes away. – Masklinn Sep 02 '20 at 05:43
  • 3
    @Masklinn The call to a function named `clone` is removed, but Arc's cloning (as a concept) is *not* removed, the `lock add` and `lock sub` instructions are present in the optimized output. In the context of the OP's question the argument was not optimized away, for reasons explained in this answer. – user4815162342 Sep 02 '20 at 07:15
  • @Masklinn It is not removed, you still have the branching and the locked ops as user4815162342 mentions. Those are the sort of things I was referring to in the answer with "*multi-threaded primitives used in refcounting mechanics*". What you are seeing as "removed" is just the consequence of inlining and other passes. – Acorn Sep 02 '20 at 09:32