Skip to content

Commit 41e02e5

Browse files
Remove class constraint from Interlocked.{Compare}Exchange (#104558)
* Remove class constraint from Interlocked.{Compare}Exchange Today `Interlocked.CompareExchange<T>` and `Interlocked.Exchange<T>` support only reference type `T`s. Now that we have corresponding {Compare}Exchange methods that support types of size 1, 2, 4, and 8, we can remove the constraint and support any `T` that's either a reference type, a primitive type, or an enum type, making the generic overload more useful and avoiding consumers needing to choose less-than-ideal types just because of the need for atomicity with Interlocked.{Compare}Exchange. --------- Co-authored-by: Michal Strehovský <[email protected]>
1 parent 6aa2862 commit 41e02e5

File tree

62 files changed

+973
-579
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+973
-579
lines changed

src/coreclr/System.Private.CoreLib/src/System/Threading/Interlocked.CoreCLR.cs

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -102,21 +102,6 @@ public static long Exchange(ref long location1, long value)
102102
[return: NotNullIfNotNull(nameof(location1))]
103103
[MethodImpl(MethodImplOptions.InternalCall)]
104104
private static extern object? ExchangeObject([NotNullIfNotNull(nameof(value))] ref object? location1, object? value);
105-
106-
// The below whole method reduces to a single call to Exchange(ref object, object) but
107-
// the JIT thinks that it will generate more native code than it actually does.
108-
109-
/// <summary>Sets a variable of the specified type <typeparamref name="T"/> to a specified value and returns the original value, as an atomic operation.</summary>
110-
/// <param name="location1">The variable to set to the specified value.</param>
111-
/// <param name="value">The value to which the <paramref name="location1"/> parameter is set.</param>
112-
/// <returns>The original value of <paramref name="location1"/>.</returns>
113-
/// <exception cref="NullReferenceException">The address of location1 is a null pointer.</exception>
114-
/// <typeparam name="T">The type to be used for <paramref name="location1"/> and <paramref name="value"/>. This type must be a reference type.</typeparam>
115-
[Intrinsic]
116-
[return: NotNullIfNotNull(nameof(location1))]
117-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
118-
public static T Exchange<T>([NotNullIfNotNull(nameof(value))] ref T location1, T value) where T : class? =>
119-
Unsafe.As<T>(Exchange(ref Unsafe.As<T, object?>(ref location1), value));
120105
#endregion
121106

122107
#region CompareExchange
@@ -183,29 +168,6 @@ public static long CompareExchange(ref long location1, long value, long comparan
183168
[MethodImpl(MethodImplOptions.InternalCall)]
184169
[return: NotNullIfNotNull(nameof(location1))]
185170
private static extern object? CompareExchangeObject(ref object? location1, object? value, object? comparand);
186-
187-
// Note that getILIntrinsicImplementationForInterlocked() in vm\jitinterface.cpp replaces
188-
// the body of the following method with the following IL:
189-
// ldarg.0
190-
// ldarg.1
191-
// ldarg.2
192-
// call System.Threading.Interlocked::CompareExchange(ref Object, Object, Object)
193-
// ret
194-
// The workaround is no longer strictly necessary now that we have Unsafe.As but it does
195-
// have the advantage of being less sensitive to JIT's inliner decisions.
196-
197-
/// <summary>Compares two instances of the specified reference type <typeparamref name="T"/> for reference equality and, if they are equal, replaces the first one.</summary>
198-
/// <param name="location1">The destination, whose value is compared by reference with <paramref name="comparand"/> and possibly replaced.</param>
199-
/// <param name="value">The value that replaces the destination value if the comparison by reference results in equality.</param>
200-
/// <param name="comparand">The object that is compared by reference to the value at <paramref name="location1"/>.</param>
201-
/// <returns>The original value in <paramref name="location1"/>.</returns>
202-
/// <exception cref="NullReferenceException">The address of <paramref name="location1"/> is a null pointer.</exception>
203-
/// <typeparam name="T">The type to be used for <paramref name="location1"/>, <paramref name="value"/>, and <paramref name="comparand"/>. This type must be a reference type.</typeparam>
204-
[Intrinsic]
205-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
206-
[return: NotNullIfNotNull(nameof(location1))]
207-
public static T CompareExchange<T>(ref T location1, T value, T comparand) where T : class? =>
208-
Unsafe.As<T>(CompareExchange(ref Unsafe.As<T, object?>(ref location1), value, comparand));
209171
#endregion
210172

211173
#region Add

src/coreclr/jit/importercalls.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4130,11 +4130,7 @@ GenTree* Compiler::impIntrinsic(CORINFO_CLASS_HANDLE clsHnd,
41304130
case NI_System_Threading_Interlocked_Exchange:
41314131
case NI_System_Threading_Interlocked_ExchangeAdd:
41324132
{
4133-
assert(callType != TYP_STRUCT);
4134-
assert(sig->numArgs == 2);
4135-
41364133
var_types retType = JITtype2varType(sig->retType);
4137-
assert((genTypeSize(retType) >= 4) || (ni == NI_System_Threading_Interlocked_Exchange));
41384134

41394135
if (genTypeSize(retType) > TARGET_POINTER_SIZE)
41404136
{
@@ -4159,6 +4155,10 @@ GenTree* Compiler::impIntrinsic(CORINFO_CLASS_HANDLE clsHnd,
41594155
break;
41604156
}
41614157

4158+
assert(callType != TYP_STRUCT);
4159+
assert(sig->numArgs == 2);
4160+
assert((genTypeSize(retType) >= 4) || (ni == NI_System_Threading_Interlocked_Exchange));
4161+
41624162
GenTree* op2 = impPopStack().val;
41634163
GenTree* op1 = impPopStack().val;
41644164

src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Interlocked.cs

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,6 @@ public static long CompareExchange(ref long location1, long value, long comparan
3636
#endif
3737
}
3838

39-
[Intrinsic]
40-
[return: NotNullIfNotNull(nameof(location1))]
41-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
42-
public static T CompareExchange<T>(ref T location1, T value, T comparand) where T : class?
43-
{
44-
return Unsafe.As<T>(CompareExchange(ref Unsafe.As<T, object?>(ref location1), value, comparand));
45-
}
46-
4739
[Intrinsic]
4840
[MethodImpl(MethodImplOptions.AggressiveInlining)]
4941
[return: NotNullIfNotNull(nameof(location1))]
@@ -92,16 +84,6 @@ public static long Exchange(ref long location1, long value)
9284
#endif
9385
}
9486

95-
[Intrinsic]
96-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
97-
[return: NotNullIfNotNull(nameof(location1))]
98-
public static T Exchange<T>([NotNullIfNotNull(nameof(value))] ref T location1, T value) where T : class?
99-
{
100-
if (Unsafe.IsNullRef(ref location1))
101-
ThrowHelper.ThrowNullReferenceException();
102-
return Unsafe.As<T>(RuntimeImports.InterlockedExchange(ref Unsafe.As<T, object?>(ref location1), value));
103-
}
104-
10587
[Intrinsic]
10688
[MethodImpl(MethodImplOptions.AggressiveInlining)]
10789
[return: NotNullIfNotNull(nameof(location1))]

src/coreclr/tools/Common/TypeSystem/IL/NativeAotILProvider.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,6 @@ private static MethodIL TryGetIntrinsicMethodIL(MethodDesc method)
4646

4747
switch (owningType.Name)
4848
{
49-
case "Interlocked":
50-
{
51-
if (owningType.Namespace == "System.Threading")
52-
return InterlockedIntrinsics.EmitIL(method);
53-
}
54-
break;
5549
case "Unsafe":
5650
{
5751
if (owningType.Namespace == "System.Runtime.CompilerServices")
@@ -108,6 +102,12 @@ private static MethodIL TryGetPerInstantiationIntrinsicMethodIL(MethodDesc metho
108102

109103
switch (owningType.Name)
110104
{
105+
case "Interlocked":
106+
{
107+
if (owningType.Namespace == "System.Threading")
108+
return InterlockedIntrinsics.EmitIL(method);
109+
}
110+
break;
111111
case "Activator":
112112
{
113113
TypeSystemContext context = owningType.Context;

src/coreclr/tools/Common/TypeSystem/IL/Stubs/InterlockedIntrinsics.cs

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public static MethodIL EmitIL(
1919
MethodDesc method)
2020
{
2121
Debug.Assert(((MetadataType)method.OwningType).Name == "Interlocked");
22+
Debug.Assert(!method.IsGenericMethodDefinition);
2223

2324
if (method.HasInstantiation && method.Name == "CompareExchange")
2425
{
@@ -30,22 +31,45 @@ public static MethodIL EmitIL(
3031
if (compilationModuleGroup.ContainsType(method.OwningType))
3132
#endif // READYTORUN
3233
{
33-
TypeDesc objectType = method.Context.GetWellKnownType(WellKnownType.Object);
34-
MethodDesc compareExchangeObject = method.OwningType.GetKnownMethod("CompareExchange",
35-
new MethodSignature(
36-
MethodSignatureFlags.Static,
37-
genericParameterCount: 0,
38-
returnType: objectType,
39-
parameters: new TypeDesc[] { objectType.MakeByRefType(), objectType, objectType }));
40-
41-
ILEmitter emit = new ILEmitter();
42-
ILCodeStream codeStream = emit.NewCodeStream();
43-
codeStream.EmitLdArg(0);
44-
codeStream.EmitLdArg(1);
45-
codeStream.EmitLdArg(2);
46-
codeStream.Emit(ILOpcode.call, emit.NewToken(compareExchangeObject));
47-
codeStream.Emit(ILOpcode.ret);
48-
return emit.Link(method);
34+
// Rewrite the generic Interlocked.CompareExchange<T> to be a call to one of the non-generic overloads.
35+
TypeDesc ceArgType = null;
36+
37+
TypeDesc tType = method.Instantiation[0];
38+
if (!tType.IsValueType)
39+
{
40+
ceArgType = method.Context.GetWellKnownType(WellKnownType.Object);
41+
}
42+
else if ((tType.IsPrimitive || tType.IsEnum) && (tType.UnderlyingType.Category is not (TypeFlags.Single or TypeFlags.Double)))
43+
{
44+
int size = tType.GetElementSize().AsInt;
45+
Debug.Assert(size is 1 or 2 or 4 or 8);
46+
ceArgType = size switch
47+
{
48+
1 => method.Context.GetWellKnownType(WellKnownType.Byte),
49+
2 => method.Context.GetWellKnownType(WellKnownType.UInt16),
50+
4 => method.Context.GetWellKnownType(WellKnownType.Int32),
51+
_ => method.Context.GetWellKnownType(WellKnownType.Int64),
52+
};
53+
}
54+
55+
if (ceArgType is not null)
56+
{
57+
MethodDesc compareExchangeNonGeneric = method.OwningType.GetKnownMethod("CompareExchange",
58+
new MethodSignature(
59+
MethodSignatureFlags.Static,
60+
genericParameterCount: 0,
61+
returnType: ceArgType,
62+
parameters: [ceArgType.MakeByRefType(), ceArgType, ceArgType]));
63+
64+
ILEmitter emit = new ILEmitter();
65+
ILCodeStream codeStream = emit.NewCodeStream();
66+
codeStream.EmitLdArg(0);
67+
codeStream.EmitLdArg(1);
68+
codeStream.EmitLdArg(2);
69+
codeStream.Emit(ILOpcode.call, emit.NewToken(compareExchangeNonGeneric));
70+
codeStream.Emit(ILOpcode.ret);
71+
return emit.Link(method);
72+
}
4973
}
5074
}
5175

src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,6 @@ private MethodIL TryGetIntrinsicMethodIL(MethodDesc method)
8585
return UnsafeIntrinsics.EmitIL(method);
8686
}
8787

88-
if (mdType.Name == "Interlocked" && mdType.Namespace == "System.Threading")
89-
{
90-
return InterlockedIntrinsics.EmitIL(_compilationModuleGroup, method);
91-
}
92-
9388
return null;
9489
}
9590

@@ -114,6 +109,11 @@ private MethodIL TryGetPerInstantiationIntrinsicMethodIL(MethodDesc method)
114109
return TryGetIntrinsicMethodILForActivator(method);
115110
}
116111

112+
if (mdType.Name == "Interlocked" && mdType.Namespace == "System.Threading")
113+
{
114+
return InterlockedIntrinsics.EmitIL(_compilationModuleGroup, method);
115+
}
116+
117117
return null;
118118
}
119119

src/coreclr/vm/corelib.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,10 @@ DEFINE_METHOD(MEMORY_MARSHAL, GET_ARRAY_DATA_REFERENCE_MDARRAY, GetArrayDa
694694
DEFINE_CLASS(INTERLOCKED, Threading, Interlocked)
695695
DEFINE_METHOD(INTERLOCKED, COMPARE_EXCHANGE_T, CompareExchange, GM_RefT_T_T_RetT)
696696
DEFINE_METHOD(INTERLOCKED, COMPARE_EXCHANGE_OBJECT,CompareExchange, SM_RefObject_Object_Object_RetObject)
697+
DEFINE_METHOD(INTERLOCKED, COMPARE_EXCHANGE_BYTE, CompareExchange, SM_RefByte_Byte_Byte_RetByte)
698+
DEFINE_METHOD(INTERLOCKED, COMPARE_EXCHANGE_USHRT, CompareExchange, SM_RefUShrt_UShrt_UShrt_RetUShrt)
699+
DEFINE_METHOD(INTERLOCKED, COMPARE_EXCHANGE_INT, CompareExchange, SM_RefInt_Int_Int_RetInt)
700+
DEFINE_METHOD(INTERLOCKED, COMPARE_EXCHANGE_LONG, CompareExchange, SM_RefLong_Long_Long_RetLong)
697701

698702
DEFINE_CLASS(RAW_DATA, CompilerServices, RawData)
699703
DEFINE_FIELD(RAW_DATA, DATA, Data)

src/coreclr/vm/jitinterface.cpp

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7154,28 +7154,79 @@ bool getILIntrinsicImplementationForInterlocked(MethodDesc * ftn,
71547154
if (ftn->GetMemberDef() != CoreLibBinder::GetMethod(METHOD__INTERLOCKED__COMPARE_EXCHANGE_T)->GetMemberDef())
71557155
return false;
71567156

7157-
// Get MethodDesc for non-generic System.Threading.Interlocked.CompareExchange()
7158-
MethodDesc* cmpxchgObject = CoreLibBinder::GetMethod(METHOD__INTERLOCKED__COMPARE_EXCHANGE_OBJECT);
7159-
7160-
// Setup up the body of the method
7161-
static BYTE il[] = {
7162-
CEE_LDARG_0,
7163-
CEE_LDARG_1,
7164-
CEE_LDARG_2,
7165-
CEE_CALL,0,0,0,0,
7166-
CEE_RET
7167-
};
7168-
7169-
// Get the token for non-generic System.Threading.Interlocked.CompareExchange(), and patch [target]
7170-
mdMethodDef cmpxchgObjectToken = cmpxchgObject->GetMemberDef();
7171-
il[4] = (BYTE)((int)cmpxchgObjectToken >> 0);
7172-
il[5] = (BYTE)((int)cmpxchgObjectToken >> 8);
7173-
il[6] = (BYTE)((int)cmpxchgObjectToken >> 16);
7174-
il[7] = (BYTE)((int)cmpxchgObjectToken >> 24);
7157+
// Determine the type of the generic T method parameter
7158+
_ASSERTE(ftn->HasMethodInstantiation());
7159+
_ASSERTE(ftn->GetNumGenericMethodArgs() == 1);
7160+
TypeHandle typeHandle = ftn->GetMethodInstantiation()[0];
7161+
7162+
// Setup up the body of the CompareExchange methods; the method token will be patched on first use.
7163+
static BYTE il[5][9] =
7164+
{
7165+
{ CEE_LDARG_0, CEE_LDARG_1, CEE_LDARG_2, CEE_CALL, 0, 0, 0, 0, CEE_RET }, // object
7166+
{ CEE_LDARG_0, CEE_LDARG_1, CEE_LDARG_2, CEE_CALL, 0, 0, 0, 0, CEE_RET }, // byte
7167+
{ CEE_LDARG_0, CEE_LDARG_1, CEE_LDARG_2, CEE_CALL, 0, 0, 0, 0, CEE_RET }, // ushort
7168+
{ CEE_LDARG_0, CEE_LDARG_1, CEE_LDARG_2, CEE_CALL, 0, 0, 0, 0, CEE_RET }, // int
7169+
{ CEE_LDARG_0, CEE_LDARG_1, CEE_LDARG_2, CEE_CALL, 0, 0, 0, 0, CEE_RET }, // long
7170+
};
7171+
7172+
// Based on the generic method parameter, determine which overload of CompareExchange
7173+
// to delegate to, or if we can't handle the type at all.
7174+
int ilIndex;
7175+
MethodDesc* cmpxchgMethod;
7176+
if (!typeHandle.IsValueType())
7177+
{
7178+
ilIndex = 0;
7179+
cmpxchgMethod = CoreLibBinder::GetMethod(METHOD__INTERLOCKED__COMPARE_EXCHANGE_OBJECT);
7180+
}
7181+
else
7182+
{
7183+
CorElementType elementType = typeHandle.GetVerifierCorElementType();
7184+
if (!CorTypeInfo::IsPrimitiveType(elementType) ||
7185+
elementType == ELEMENT_TYPE_R4 ||
7186+
elementType == ELEMENT_TYPE_R8)
7187+
{
7188+
return false;
7189+
}
7190+
else
7191+
{
7192+
switch (typeHandle.GetSize())
7193+
{
7194+
case 1:
7195+
ilIndex = 1;
7196+
cmpxchgMethod = CoreLibBinder::GetMethod(METHOD__INTERLOCKED__COMPARE_EXCHANGE_BYTE);
7197+
break;
7198+
7199+
case 2:
7200+
ilIndex = 2;
7201+
cmpxchgMethod = CoreLibBinder::GetMethod(METHOD__INTERLOCKED__COMPARE_EXCHANGE_USHRT);
7202+
break;
7203+
7204+
case 4:
7205+
ilIndex = 3;
7206+
cmpxchgMethod = CoreLibBinder::GetMethod(METHOD__INTERLOCKED__COMPARE_EXCHANGE_INT);
7207+
break;
7208+
7209+
case 8:
7210+
ilIndex = 4;
7211+
cmpxchgMethod = CoreLibBinder::GetMethod(METHOD__INTERLOCKED__COMPARE_EXCHANGE_LONG);
7212+
break;
7213+
7214+
default:
7215+
_ASSERT(!"Unexpected primitive type size");
7216+
return false;
7217+
}
7218+
}
7219+
}
7220+
7221+
mdMethodDef cmpxchgToken = cmpxchgMethod->GetMemberDef();
7222+
il[ilIndex][4] = (BYTE)((int)cmpxchgToken >> 0);
7223+
il[ilIndex][5] = (BYTE)((int)cmpxchgToken >> 8);
7224+
il[ilIndex][6] = (BYTE)((int)cmpxchgToken >> 16);
7225+
il[ilIndex][7] = (BYTE)((int)cmpxchgToken >> 24);
71757226

71767227
// Initialize methInfo
7177-
methInfo->ILCode = const_cast<BYTE*>(il);
7178-
methInfo->ILCodeSize = sizeof(il);
7228+
methInfo->ILCode = const_cast<BYTE*>(il[ilIndex]);
7229+
methInfo->ILCodeSize = sizeof(il[ilIndex]);
71797230
methInfo->maxStack = 3;
71807231
methInfo->EHcount = 0;
71817232
methInfo->options = (CorInfoOptions)0;

src/coreclr/vm/metasig.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,8 @@ DEFINE_METASIG_T(SM(RefDec_RetVoid, r(g(DECIMAL)), v))
586586

587587
DEFINE_METASIG(GM(RefT_T_T_RetT, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, r(M(0)) M(0) M(0), M(0)))
588588
DEFINE_METASIG(SM(RefObject_Object_Object_RetObject, r(j) j j, j))
589+
DEFINE_METASIG(SM(RefByte_Byte_Byte_RetByte, r(b) b b, b))
590+
DEFINE_METASIG(SM(RefUShrt_UShrt_UShrt_RetUShrt, r(H) H H, H))
589591

590592
DEFINE_METASIG_T(SM(RefCleanupWorkListElement_RetVoid, r(C(CLEANUP_WORK_LIST_ELEMENT)), v))
591593
DEFINE_METASIG_T(SM(RefCleanupWorkListElement_SafeHandle_RetIntPtr, r(C(CLEANUP_WORK_LIST_ELEMENT)) C(SAFE_HANDLE), I))

src/libraries/Common/src/System/Net/StreamBuffer.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -292,15 +292,15 @@ private sealed class ResettableValueTaskSource : IValueTaskSource
292292

293293
private ManualResetValueTaskSourceCore<bool> _waitSource; // mutable struct, do not make this readonly
294294
private CancellationTokenRegistration _waitSourceCancellation;
295-
private int _hasWaiter;
295+
private bool _hasWaiter;
296296

297297
ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _waitSource.GetStatus(token);
298298

299299
void IValueTaskSource.OnCompleted(Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) => _waitSource.OnCompleted(continuation, state, token, flags);
300300

301301
void IValueTaskSource.GetResult(short token)
302302
{
303-
Debug.Assert(_hasWaiter == 0);
303+
Debug.Assert(!_hasWaiter);
304304

305305
// Clean up the registration. This will wait for any in-flight cancellation to complete.
306306
_waitSourceCancellation.Dispose();
@@ -312,7 +312,7 @@ void IValueTaskSource.GetResult(short token)
312312

313313
public void SignalWaiter()
314314
{
315-
if (Interlocked.Exchange(ref _hasWaiter, 0) == 1)
315+
if (Interlocked.Exchange(ref _hasWaiter, false))
316316
{
317317
_waitSource.SetResult(true);
318318
}
@@ -322,21 +322,21 @@ private void CancelWaiter(CancellationToken cancellationToken)
322322
{
323323
Debug.Assert(cancellationToken.IsCancellationRequested);
324324

325-
if (Interlocked.Exchange(ref _hasWaiter, 0) == 1)
325+
if (Interlocked.Exchange(ref _hasWaiter, false))
326326
{
327327
_waitSource.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(new OperationCanceledException(cancellationToken)));
328328
}
329329
}
330330

331331
public void Reset()
332332
{
333-
if (_hasWaiter != 0)
333+
if (_hasWaiter)
334334
{
335335
throw new InvalidOperationException("Concurrent use is not supported");
336336
}
337337

338338
_waitSource.Reset();
339-
Volatile.Write(ref _hasWaiter, 1);
339+
Volatile.Write(ref _hasWaiter, true);
340340
}
341341

342342
public void Wait()

0 commit comments

Comments
 (0)