Ownership and conditionally executed code

I am reading a rust book over the weekend and I have a question about the concept of ownership. The impression is that the property is used to statically determine where the resource can be freed. Suppose now that we have the following:

{ // 1 let x; // 2 { // 3 let y = Box::new(1); // 4 x = if flip_coin() {y} else {Box::new(2)} // 5 } // 6 } // 7 

I was surprised to see that the compiler accepts this program. By inserting println! and implementing the Drop sign for the value in the box, I saw that a field containing a value of 1 would be freed up on any line 6 or 7 depending on the returned flip_coin value. How does the compiler know when to free this box? Is it decided at runtime to use some information about the runtime (for example, a flag indicating whether this box is used)?

+6
source share
2 answers

After some research, I found out that Rust is currently adding a flag to each type that implements the Drop property, so that it knows whether the value has been discarded or not, which, of course, requires time-consuming execution. There were suggestions to avoid this cost using static drops or hot drops , but these solutions had problems with their semantics, namely that drops may occur in places you would not expect (for example, in the middle of a code block), especially if you're used to the RAII C ++ style. Currently, there is an opinion that the best compromise is another solution where flags are removed from types. Instead, the flags will be added to the stack, but only when the compiler cannot determine when to do Drop statically (having the same semantics as C ++), what happens when there are conditional movements like the example given in this question . For all other cases, there will be no time spent on execution. It seems that this proposal will not be implemented over time at 1.0.

Note that C ++ has similar runtime costs associated with unique_ptr . When the new Drop implemented, Rust will be strictly better than C ++ in this regard.

Hope this is the correct summary of the situation. The loan goes to u / dyoll1013, u / pcwalton, u / !! kibwen, u / Kimundi on reddit, and Chris Morgan here on SO.

+6
source

In non-optimized code, Rust uses dynamic checks, but it is likely that they will be eliminated in the optimized code.

I looked at the behavior of the following code:

 #[derive(Debug)] struct A { s: String } impl Drop for A { fn drop(&mut self) { println!("Dropping {:?}", &self); } } fn flip_coin() -> bool { false } #[allow(unused_variables)] pub fn test() { let x; { let y1 = A { s: "y1".to_string() }; let y2 = A { s: "y2".to_string() }; x = if flip_coin() { y1 } else { y2 }; println!("leaving inner scope"); } println!("leaving middle scope"); } 

According to your comment on another answer, a drop call for a line that is left alone occurs after "leaving the inner area" of println. This is similar to the mere expectation that the y regions expand to the end of their block.

If you look at the assembler language compiled without optimization, it seems that the if not only copies either y1 or y2 to x, but also zeroes out any variable provided as the source for the move. Here's the test:

 .LBB14_8: movb -437(%rbp), %al andb $1, %al movb %al, -177(%rbp) testb $1, -177(%rbp) jne .LBB14_11 jmp .LBB14_12 

Here is the "then" branch, which moves the string "y1" to x. Pay attention, especially to the memset call, which resets y1 after moving:

 .LBB14_11: xorl %esi, %esi movl $32, %eax movl %eax, %edx leaq -64(%rbp), %rcx movq -64(%rbp), %rdi movq %rdi, -176(%rbp) movq -56(%rbp), %rdi movq %rdi, -168(%rbp) movq -48(%rbp), %rdi movq %rdi, -160(%rbp) movq -40(%rbp), %rdi movq %rdi, -152(%rbp) movq %rcx, %rdi callq memset@PLT jmp .LBB14_13 

(This looks horrible until you realize that all these movq instructions just copy 32 bytes from %rbp-64 , which is y1, %rbp-176 , which is x, or at least temporary, which will eventually be x.) Note that it copies 32 bytes, not 24, which you expect for Vec (one pointer plus two clips). This is because Rust adds a hidden “fall flag” to the structure, indicating whether this value is real-time or not, after three visible fields.

And here is the else branch, doing the same for y2:

 .LBB14_12: xorl %esi, %esi movl $32, %eax movl %eax, %edx leaq -128(%rbp), %rcx movq -128(%rbp), %rdi movq %rdi, -176(%rbp) movq -120(%rbp), %rdi movq %rdi, -168(%rbp) movq -112(%rbp), %rdi movq %rdi, -160(%rbp) movq -104(%rbp), %rdi movq %rdi, -152(%rbp) movq %rcx, %rdi callq memset@PLT .LBB14_13: 

This is followed by the code to “leave the inner area” of println, which is painful to see, so I won’t include it here.

Then we call the procedure “glue_drop” for both y1 and y2. This seems to be a compiler-generated function that takes A, checks the String Vec drop flag, and if this set calls Invase a drop, followed by the drop procedure for the string it contains.

If I read this correctly, it's pretty smart: even if it's A, which has a drop method that we need to call first, Rust knows what it can use ... inhale ... the Vec drop flag inside String inside A as a flag which indicates whether A should be discarded.

Now that they are compiled with optimization, the inlining and flow analysis should recognize situations in which a fall will definitely happen (and omit the run-time check), or definitely won't (and omit the fall altogether). And I believe that I have heard about optimizations that duplicate the code following the then / else clause in both ways, and then specialize them. This will eliminate all checks during the execution of this code (but duplicate the println call!).

As the original poster indicates, there is an RFC proposal to move carry flags from values ​​and instead link them to a stack of slots holding values.

Thus, it is likely that optimized code may not have any checks at run time. However, I cannot force myself to read optimized code. Why don't you give it a try?

+4
source

Source: https://habr.com/ru/post/983545/


All Articles