std::vector move assign patch
Marc Glisse
marc.glisse@inria.fr
Tue Apr 25 16:13:00 GMT 2017
On Tue, 25 Apr 2017, Jonathan Wakely wrote:
> On 25/04/17 17:23 +0200, Marc Glisse wrote:
>> On Tue, 25 Apr 2017, Jonathan Wakely wrote:
>>
>>> On 24/04/17 22:10 +0200, Marc Glisse wrote:
>>>> It seems that this patch had 2 consequences that may or may not have been
>>>> planned. Consider this example (from PR64601)
>>>>
>>>> #include <vector>
>>>> typedef std::vector<int> V;
>>>> void f(V&v,V&w){ V(std::move(w)).swap(v); }
>>>> void g(V&v,V&w){ v=std::move(w); }
>>>>
>>>> 1) We generate shorter code for f than for g, probably since the fix for
>>>> PR59738. g ends up zeroing v, copying w to v, and finally zeroing w, and
>>>> for weird reasons (and because we swap the members one by one) the
>>>> standard prevents us from assuming that v and w do not overlap in weird
>>>> ways so we cannot optimize as much as one might expect.
>>>
>>> f has an additional precondition (that the allocators of the vectors
>>> being swapped must propagate on swap or be equal) and so the swap code
>>> doesn't have to worry about non-equal allocators.
>>>
>>> g has to be able to cope with the case where the allocator doesn't
>>> propagate and isn't equal, and so is more complicated.
>>>
>>> However, the propagation trait is known at compile-time, and for the
>>> common case so is the equality condition, so it's unfortunate if that
>>> can't be simplified (I'm sure you've analysed it carefully already
>>> though!)
>>
>> The code isn't horrible. With f, we get:
>>
>> movq (%rsi), %r8
>> movq 8(%rsi), %rcx
>> movq $0, (%rsi)
>> movq $0, 8(%rsi)
>> movq 16(%rsi), %rdx
>> movq $0, 16(%rsi)
>> movq (%rdi), %rax
>> movq %rcx, 8(%rdi)
>> movq %r8, (%rdi)
>> movq %rdx, 16(%rdi)
>> testq %rax, %rax
>>
>> which seems quite optimal: read each pointer from w, write them to v, write
>> 0s in w, that's 9 memory operations, +1 to read the pointer from w and
>> possibly call delete on it.
Of course I should have mentioned that writing 0s is done between the read
and the other write, which differentiates it from g.
>> With g:
>>
>> movq $0, 8(%rdi)
>> movq (%rdi), %rax
>> movq $0, 16(%rdi)
>> movq $0, (%rdi)
>> movq (%rsi), %rdx
>> movq %rdx, (%rdi)
>> movq 8(%rsi), %rcx
>> movq $0, (%rsi)
>> movq 8(%rdi), %rdx
>> movq %rcx, 8(%rdi)
>> movq 16(%rsi), %rcx
>> movq %rdx, 8(%rsi)
>> movq 16(%rdi), %rdx
>> movq %rcx, 16(%rdi)
>> movq %rdx, 16(%rsi)
>> testq %rax, %rax
>>
>> That's only 5 more memory operations. If I tweak vector swapping to avoid
>> calling swap on each member (which drops type-based aliasing information,
>> that was the topic of PR64601)
>
> I didn't really understand the discussion in the PR. I find that's
> true of most TBAA discussions.
>
> std::swap(T& x, T& y) is hard to optimise because we don't know that
> the dynamic type of the thing at &x is the same type as T?
std::swap<int*> only knows that it is dealing with pointers to integers,
so _M_start from one vector might be in the same location as _M_finish
from some other vector...
When I access __x._M_start directly, I am accessing some part of a
_Vector_impl, and 2 _Vector_impl can only be the same or disjoint, they
cannot partially overlap.
>> void _M_swap_data(_Vector_impl& __x) _GLIBCXX_NOEXCEPT
>> {
>> pointer tmp;
>> #define MARC(x,y) tmp=x; x=y; y=tmp
>> MARC(_M_start, __x._M_start);
>> MARC(_M_finish, __x._M_finish);
>> MARC(_M_end_of_storage, __x._M_end_of_storage);
>> }
>>
>> this gets down to 13, which is kind of sensible
>> * 0 the elements of v -> 3 ops
>> * read the elements of w -> 3 ops
>> * write them to v -> 3 ops
>> * 0 the elements of w -> 3 ops
>> (+1 to get the pointer that we might call delete on)
>>
>> The first step of zeroing the elements of v is redundant
>> * if v and w don't alias, we are going to overwrite those 0s in step 3
>> without ever reading them
>> * if v and w are the same, we are going to write those 0s in step 4 anyway
>>
>> but that's hard for the optimizers to notice.
>>
>> I didn't try hard to find a nice C++ way to get an equivalent of g that
>> generates the optimal number of operations, but it would be a little ugly
>> to write in operator=
>> this->_M_impl._M_finish = x._M_impl._M_finish; x._M_impl._M_finish = 0;
>> same for _M_end_of_storage and _M_start, and remembering to use the
>> original this->_M_impl._M_start for delete.
>
> I'm not opposed to writing it out by hand. Operations on std::vector
> should be as fast as possible, and move-assignment should be cheap.
I am not sure it would make a noticable difference. I might do it at some
point in the future...
By the way, do we have a policy on writing if(p)delete p; vs directly
delete p; which does nothing for p==0? There are cases where the extra
shortcut is a nice optimization, others where it is redundant work. At
-Os, the middle-end could optimize if(p)free(p) to free(p) in the future.
--
Marc Glisse
More information about the Libstdc++
mailing list