Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove helper method frames from Monitors #113242

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

davidwrighton
Copy link
Member

@davidwrighton davidwrighton commented Mar 7, 2025

  • Convert to the FCALL fast path/QCALL slow path approach
  • Make EnterHelperResult/LeaveHelperAction into enum class so that they can safely be silently marshaled between native and managed
  • Move the lockTaken flag handling to managed code so we can share more helpers
  • Benchmarking indicates that this makes uncontended locks somewhere around 10% faster. I believe this is primarily due to removing a small amount of handling around error checking and lock flag usage which isn't always needed.

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
- Convert to the FCALL fast path/QCALL slow path approach
- Make EnterHelperResult/LeaveHelperAction into enum class so that they can safely be silently marshaled between native and managed
- Move the lockTaken flag handling to managed code so we can share more helpers
Copy link
Contributor

Tagging subscribers to this area: @mangod9
See info in area-owners.md if you want to be subscribed.

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
@davidwrighton
Copy link
Member Author

@EgorBot -amd -arm -windows_intel

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Numerics;

BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);

public class Tests
{
    private obj _lockObj = new object();
    private int _field = 0;

    [Benchmark(Baseline = true)]
    public string Lock_OnLockObj()
    {
        _field++;
        lock(_lockObj)
        {
            _field++;
        }
    }

    [Benchmark]
    public bool LockOnThis_ThisKnownToBeNonNull()
    {
        _field++;
        lock(this)
        {
            _field++;
        }
    }

    [Benchmark]
    public bool LockOnThis_ThisKnownToBeNonNull()
    {
        lock(this)
        {
            _field++;
        }
    }
}

@davidwrighton davidwrighton marked this pull request as ready for review March 7, 2025 18:19
@Copilot Copilot bot review requested due to automatic review settings March 7, 2025 18:19
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

PR Overview

This PR refactors the Monitor implementation to remove helper method frames by introducing a fast path/slow path approach for lock acquisition and release. Key changes include:

  • Introducing new fast path methods (TryEnter_FastPath and TryEnter_FastPath_WithTimeout) and enumerated types (EnterHelperResult, LeaveHelperAction) for better handling of lock acquisition.
  • Replacing the ReliableEnter/Exit methods with new logic for handling both immediate and timeout-based lock acquisition.
  • Migrating lockTaken flag handling to managed code and updating related methods (Enter, TryEnter, and Exit) accordingly.

Reviewed Changes

File Description
src/coreclr/System.Private.CoreLib/src/System/Threading/Monitor.CoreCLR.cs Refactors lock acquisition and release by adding fast path/slow path methods and replacing old helper methods, while updating argument validation and exception handling

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

TryEnter(obj, millisecondsTimeout, ref lockTaken);
return lockTaken;
ArgumentNullException.ThrowIfNull(obj);

Copy link
Preview

Copilot AI Mar 7, 2025

Choose a reason for hiding this comment

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

The TryEnter(object, int) method does not explicitly validate that millisecondsTimeout is not less than -1. To maintain the documented contract, consider adding a check to throw ArgumentException for invalid timeout values.

Suggested change
if (millisecondsTimeout < -1)
{
throw new ArgumentException("Timeout must be greater than or equal to -1.", nameof(millisecondsTimeout));
}

Copilot is powered by AI, so mistakes are possible. Review output carefully before use.

Provide additional feedback

Please help us improve GitHub Copilot by sharing more details about this comment.

}

private static void TryEnter_Timeout_WithLockTaken(object obj, int timeout, ref bool lockTaken)
{
Copy link
Preview

Copilot AI Mar 7, 2025

Choose a reason for hiding this comment

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

The TryEnter_Timeout_WithLockTaken method does not handle cases where timeout is less than -1. It is recommended to add an explicit check and throw an ArgumentException when timeout is invalid.

Suggested change
{
{
if (timeout < -1)
{
throw new ArgumentException("Timeout must be greater than or equal to -1.", nameof(timeout));
}

Copilot is powered by AI, so mistakes are possible. Review output carefully before use.

Provide additional feedback

Please help us improve GitHub Copilot by sharing more details about this comment.

@davidwrighton
Copy link
Member Author

Sigh. Looks like something is busted. I'd like to finish looking into that before anyone reviews this.

return;
}

Exit_Slowpath(obj, exitBehavior);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can Exit_FastPath do anything when returning exitBehavior != LeaveHelperAction.None (note NOT EQUAL) that would break the subsequent fall-through to Exit_Slowpath (e.g. we did the fast-path exit, but it needs some other LeaveHelperAction... but we've done the Exit already)?

(same question on line 154)

Copy link
Member Author

Choose a reason for hiding this comment

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

No.

if (exitBehavior == LeaveHelperAction.None)
return;

Exit_Slowpath(obj, exitBehavior);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can Exit_FastPath do anything when returning exitBehavior != LeaveHelperAction.None (note NOT EQUAL) that would break the subsequent fall-through to Exit_Slowpath (e.g. we did the fast-path exit, but it needs some other LeaveHelperAction... but we've done the Exit already)?

(same question on line 174)

Copy link
Member Author

Choose a reason for hiding this comment

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

No.

}
}

if (TryEnter_Slowpath(obj, timeout))
Copy link
Contributor

Choose a reason for hiding this comment

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

Will this result in up to two timeout periods elapsing if the fast-path tried and returned anything other than Entered or Contention?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, the timeout input to the fast path is only used to check against 0 (which has special meaning).

return AwareLock::EnterHelperResult::Contention;
}

result = obj->EnterObjMonitorHelperSpin(pCurThread);
Copy link
Contributor

Choose a reason for hiding this comment

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

Should a timeOut of non-zero be handled, should it be passed down to/honored by the EnterObjMonitorHelperSpin?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, this is the fast set of spinning that waits for fractions of a millisecond or so, if you're willing to wait at all, we run the same amount of spinning for all possible timeouts.

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
…ectly. Also hard-code the exact values for the Enter/Leave results on the native side so that the values are well defined instead of implicit.
{
QCALL_CONTRACT;

GCX_COOP();
Copy link
Member

Choose a reason for hiding this comment

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

QCalls need BEGIN_QCALL and END_QCALL if exceptions will be thrown.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
Comment on lines +55 to +61
private enum LeaveHelperAction {
None = 0,
Signal = 1,
Yield = 2,
Contention = 3,
Error = 4,
};
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
private enum LeaveHelperAction {
None = 0,
Signal = 1,
Yield = 2,
Contention = 3,
Error = 4,
};
private enum LeaveHelperAction
{
None = 0,
Signal = 1,
Yield = 2,
Contention = 3,
Error = 4,
}

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants