Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Avoid using long for disposed flag#13069

Merged
daxian-dbw merged 5 commits intoPowerShell:masterfrom
xtqqczze:no-long-disposed
Jul 10, 2020
Merged

Avoid using long for disposed flag#13069
daxian-dbw merged 5 commits intoPowerShell:masterfrom
xtqqczze:no-long-disposed

Conversation

@xtqqczze
Copy link
Contributor

@xtqqczze xtqqczze commented Jun 30, 2020

PR Summary

Avoid unecessary Int64 for disposed flag in CimAsyncOperation.cs

PR Context

The original intent appears to be to allow use of Interlocked.Read(Int64). However this is implemented in the BCL with Interlocked.CompareExchange(ref location, 0, 0). We can use that pattern with an Int32.

https://stackoverflow.com/a/24893231

PR Checklist

@ghost ghost assigned daxian-dbw Jun 30, 2020
@xtqqczze xtqqczze marked this pull request as ready for review June 30, 2020 23:03
@iSazonov
Copy link
Collaborator

iSazonov commented Jul 1, 2020

Since most of systems is x64 I don't value in the change.

@iSazonov iSazonov requested a review from PaulHigin July 1, 2020 05:21
@xtqqczze
Copy link
Contributor Author

xtqqczze commented Jul 1, 2020

Since most of systems is x64 I don't value in the change.

surely there is value for x86 and arm32?

get
{
return Interlocked.Read(ref this._disposed) == 1;
return Interlocked.CompareExchange(ref this._disposed, 0, 0) == 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure why it is so important to do atomic read on each get. But this is old code and I'd rather not make logic changes. It seems like _disposed should be tagged 'volatile' if it is that sensitive.

Anyway, what I don't like about this is that most of the time _disposed is '0' and so presumably each get will result in a write operation. It would be better to test for the final value of '1', which only happens once, and avoid multiple writes.

return Interlocked.CompareExchange(ref this._disposed, 1, 1) == 1;

Copy link
Collaborator

@vexx32 vexx32 Jul 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PaulHigin that code will overwrite the value with 1 on the first read.

EDIT: never mind, misunderstood what was happening here. Would recommend naming the arguments, though in this kind of case there's not a lot to be gleaned without reading over documentation.

Copy link
Contributor

@PaulHigin PaulHigin Jul 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replacement only occurs if the value and comparison value are equal. The _disposed value is '0' and the comparison value is '1'.

But this illustrates a serious downside to this change. It is difficult to read and reason about, and so difficult to maintain. There is not a large upside with this change, so I feel it should not be taken.

/cc @daxian-dbw

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interlocked APIs are never fun to reason about. An additional comment should be added, I feel, and arguments named for clarity, but the change itself is sound IMO.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PaulHigin I don't think the atomic read using CompareExchange is necessary, it is just being used to ensure a full fence. volatile does not protect against a read after write so it is no use here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not just using Interlocked APIs, but using them in an unusual way. This makes the code hard to read and maintain. I agree that the atomic read is probably not necessary, but I also see no great harm in it. It is old code and there is no real upside in changing it. Therefore I won't approve the change and instead suggest the code be left as is.

/cc @daxian-dbw

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a think about this and we can eliminate the dummy CompareExchange by using an explicit memory barrier (see 7fc1f28). This no longer relies upon side effects from CompareExchange.

@ghost ghost added the Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept label Jul 2, 2020
@iSazonov
Copy link
Collaborator

iSazonov commented Jul 2, 2020

surely there is value for x86 and arm32?

This code is not in a hot way. We won’t get any winnings.

@ghost ghost removed the Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept label Jul 2, 2020
get
{
return Interlocked.CompareExchange(ref this._disposed, 0, 0) == 1;
Interlocked.MemoryBarrier();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method has a different purpose.

Copy link
Contributor Author

@xtqqczze xtqqczze Jul 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but since _disposed is now Int32 atomicity is guaranteed and we don't need this method.

However, a side effect of CompareExchange is to provide a memory barrier, for clarity's sake we can use an explicit barrier rather then rely on the side effect.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xtqqczze it might be a good idea to add a comment there to explain what that actually is being used to ensure if you can. I checked the documentation and the description for that is... obtuse at best, though I think I have an OK understanding of what it's doing.

Copy link
Collaborator

@iSazonov iSazonov Jul 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but since _disposed is now Int32 atomicity is guaranteed and we don't need this method.

If Int32 is atomicity we could remove all Interlocked.* in the code and use volatile.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but since _disposed is now Int32 atomicity is guaranteed and we don't need this method.

If Int32 is atomicity we could remove all Interlocked.* in the code and use volatile.

Atomicity is guaranteed for a single read operation. CompareExchange is needed for atomic read-modify-write.

I don't think replacing Interlocked.* with volatile would be a good idea.

Copy link
Member

@daxian-dbw daxian-dbw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @PaulHigin that there doesn't seem to be a reason to use atomic read/write for keeping track of the disposed information. A bool field is usually for this purpose and works well, an example.

Like @PaulHigin said, this is old code, and this PR is not fixing a bug or a noticeable perf issue, so I also prefer to leave the code as is, with one exception -- the change at line 55 is correcting a mistake, so I would like to keep it as the only change in this PR. @xtqqczze thoughts?

get
{
return Interlocked.Read(ref this._disposed) == 1;
// Ensure the read of _disposed doesn't move up before the write in the Dispose method.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A memory barrier only guarantees the memory access order in the current thread, but Disposed method is very possible to be called in a different thread, so it doesn't seem useful to add the call to Interlocked.MemoryBarrier().

Copy link
Contributor Author

@xtqqczze xtqqczze Jul 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@daxian-dbw Yes it would seem we would need Interlocked.MemoryBarrierProcessWide instead, but according to documentation this is a very expensive operation.

The most correct method would be to use a dummy CompareExchange operation like in 6d1e962 but @PaulHigin had concerns this would be using the Interlocked API in an unusual way.

It would be great if we could do return Interlocked.Read(ref this._disposed) == 1; but alas this is not defined for an Int32. I am considering opening a new issue at dotnet/runtime to add this API to the BCL.

Copy link
Member

@daxian-dbw daxian-dbw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with the current change too, with 1 comment.

@daxian-dbw daxian-dbw merged commit e41a28c into PowerShell:master Jul 10, 2020
@daxian-dbw daxian-dbw added the CL-CodeCleanup Indicates that a PR should be marked as a Code Cleanup change in the Change Log label Jul 10, 2020
@daxian-dbw daxian-dbw added this to the 7.1.0-preview.6 milestone Jul 10, 2020
@xtqqczze xtqqczze deleted the no-long-disposed branch July 11, 2020 00:52
@ghost
Copy link

ghost commented Aug 17, 2020

🎉v7.1.0-preview.6 has been released which incorporates this pull request.:tada:

Handy links:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CL-CodeCleanup Indicates that a PR should be marked as a Code Cleanup change in the Change Log

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants