|
| 1 | +- Feature Name: `option-replace-with` |
| 2 | +- Start Date: 2018-06-28 |
| 3 | +- RFC PR: (leave this empty) |
| 4 | +- Rust Issue: (leave this empty) |
| 5 | + |
| 6 | +# Summary |
| 7 | +[summary]: #summary |
| 8 | + |
| 9 | +This RFC proposes the addition of `Option::replace_with` to compliment `Option::replace` ([RFC #2296](https://github.com/rust-lang/rfcs/pull/2296)) and `Option::take` methods. It replaces the actual value in the option with the value returned from a closure given as a parameter, while the old value is passed into the closure. |
| 10 | + |
| 11 | +# Motivation |
| 12 | +[motivation]: #motivation |
| 13 | + |
| 14 | +`Option::replace_with` helps to clarify the intent and also [improves the performance of a naive `Option::take` + assignment implementation](https://barrielle.cedeela.fr/research_page/dropping-drops.html). |
| 15 | + |
| 16 | +# Guide-level explanation |
| 17 | +[guide-level-explanation]: #guide-level-explanation |
| 18 | + |
| 19 | +`Option::replace_with` is a replacement for the following code: |
| 20 | + |
| 21 | +```rust |
| 22 | +let mut some_option: Option<i32> = Some(123); |
| 23 | + |
| 24 | +some_option = consume_option_i32_and_produce_option_i32(some_option.take()); |
| 25 | +``` |
| 26 | + |
| 27 | +With `Option::replace_with` you will write: |
| 28 | + |
| 29 | +```rust |
| 30 | +let mut some_option: Option<i32> = Some(123); |
| 31 | + |
| 32 | +some_option.replace_with(|old_value| consume_option_i32_and_produce_option_i32(old_value)); |
| 33 | + |
| 34 | +// OR |
| 35 | + |
| 36 | +some_option.replace_with(consume_option_i32_and_produce_option_i32); |
| 37 | +``` |
| 38 | + |
| 39 | +While the first implementation works fine, it generates suboptimal code due to unnecessary "allocation" and "deallocation" of `None` value. The naive implementation is about 10% slower than the optimal solution: |
| 40 | + |
| 41 | +```rust |
| 42 | +let mut some_option: Option<i32> = Some(123); |
| 43 | + |
| 44 | +let old_value = unsafe { mem::uninitialized() }; |
| 45 | +mem::swap(&mut some_option, old_value); |
| 46 | +let mut new_value = consume_option_i32_and_produce_option_i32(old_value); |
| 47 | +mem::swap(&mut some_option, &mut new_value); |
| 48 | +mem::forget(new_value); |
| 49 | +``` |
| 50 | + |
| 51 | +`Option::replace_with` can implement the trick and reach the maximum performance. |
| 52 | + |
| 53 | +# Reference-level explanation |
| 54 | +[reference-level-explanation]: #reference-level-explanation |
| 55 | + |
| 56 | +This method will be added to the `core::option::Option` type implementation: |
| 57 | + |
| 58 | +```rust |
| 59 | +use core::mem; |
| 60 | + |
| 61 | +impl<T> Option<T> { |
| 62 | + // ... |
| 63 | + |
| 64 | + #[inline] |
| 65 | + fn replace_with<F>(&mut self, f: F) |
| 66 | + where |
| 67 | + F: FnOnce(Option<T>) -> Option<T>, |
| 68 | + { |
| 69 | + let mut old_value = unsafe { mem::uninitialized() }; |
| 70 | + mem::swap(self, &mut old_value); |
| 71 | + let mut new_value = f(old_value); |
| 72 | + mem::swap(self, &mut new_value); |
| 73 | + // After two swaps (`old_value` -> `self` -> `new_value`), `new_value` |
| 74 | + // holds an `uninitialized` value, so we just forget about it. |
| 75 | + mem::forget(new_value); |
| 76 | + } |
| 77 | +} |
| 78 | +``` |
| 79 | + |
| 80 | +Here is a benchmark: [link](https://github.com/frol/rust-benchmark-option-replace_with-rfc). |
| 81 | + |
| 82 | +Here is a generated assembly code comparison in Compiler Explorer: [link](https://godbolt.org/g/6Cukig) (naive implementation is on the left, and optimized implementation is on the right). |
| 83 | + |
| 84 | +# Drawbacks |
| 85 | +[drawbacks]: #drawbacks |
| 86 | + |
| 87 | +There will be no need in this method if the compiler can optimize the cases when it is clear that the variable holds `None`, i.e. `Option::take` and simple assignment would not produce unnecessary `moveq 0` and `drop_in_place` call. |
| 88 | + |
| 89 | +# Rationale and alternatives |
| 90 | +[alternatives]: #alternatives |
| 91 | + |
| 92 | +The rationale for proposing `Option::replace_with` is that it is the simplest way to boost the performance for the use-case. |
| 93 | + |
| 94 | +The alternative is to teach Rust compiler or LLVM to optimize the use-case expressed with a simple assignment. |
| 95 | + |
| 96 | +# Prior art |
| 97 | +[prior-art]: #prior-art |
| 98 | + |
| 99 | +[The performance issue and the workaround were initially discovered](https://barrielle.cedeela.fr/research_page/dropping-drops.html) during the digging into [Completely Unscientific Benchmark](https://www.reddit.com/r/rust/comments/8jbjku/naive_benchmark_treap_implementation_of_c_rust/). |
| 100 | + |
| 101 | +Naive searching through Rust codebase revealed only a single case where currently a simple assignment is used: [`src/librustdoc/passes/collapse_docs.rs`](https://github.com/rust-lang/rust/blob/e3bf634e060bc2f8665878288bcea02008ca346e/src/librustdoc/passes/collapse_docs.rs#L52-L81). |
| 102 | + |
| 103 | +# Unresolved questions |
| 104 | +[unresolved]: #unresolved-questions |
| 105 | + |
| 106 | +- Should `Option::replace_with` be introduced or LLVM/Rustc should implement a general optimization which will cover this use-case as well as many others? |
0 commit comments