-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Description
Version Used: 5.0.0-2.25523.111 (b0f34d51), language version 14.0.
Steps to Reproduce:
public static class Test
{
public readonly ref struct S
{
public readonly ref int Reference;
}
public static ref int GetReference(S s)
{
return ref s.Reference;
}
// Cases below.
public static void DiscardParam(ref int r)
{
_ = r;
}
public static void DiscardMethod()
{
_ = GetReference(default);
}
public static void DiscardDirect()
{
_ = default(S).Reference;
}
public static void UnusedLocalParam(ref int r)
{
int unused = r;
}
public static void UnusedLocalMethod()
{
int unused = GetReference(default);
}
public static void UnusedLocalDirect()
{
int unused = default(S).Reference;
}
}Expected Behavior: The IL of all methods below Cases below. tries to load a value from the reference.
Actual Behavior:
- When optimization is on, methods
...Paramand...Methoddo not haveldind.i4. - When optimization is off, methods
DiscardParamandDiscardMethoddoes not haveldind.i4. - Regardless of optimization, methods
...Directalways hasldind.i4(desired).
Explanation: In certain of my libraries, I decide to trigger NullReferenceException (note 1) prior to doing anything to the reference. Consider the following code:
static void CopyBackwards(ref int src, ref int dst, int count)
{
if (count == 0)
return;
_ = src;
_ = dst;
for (int i = count; i != 0;)
{
--i;
Unsafe.Add(ref dst, i) = Unsafe.Add(ref src, i);
}
}It's important to trigger access on src, dst before trying anything, so that we get NullReferenceException appropriately. Otherwise (suppose we don't check for it), if count is very large, then the following bad things can happen:
- The first iteration succeeds, corrupting memory.
- The first iteration fails, but the exception thrown is
AccessViolationException.
Note 1: I know the recommended way is to check for null and throw ArgumentNullException. This bug report has nothing to do with best practices. Also, the context is actually in .NET Framework, which I understand might not be tracked in this repo. But the bug reproduces in .NET 10.0, so it should be relevant.
Analysis:
Prior to the introduction of Span<T> and more generally ref fields, the only possible ref that can be created by a purely C# program is always valid non-null. Therefore, eliminating the load was valid as far as C# is concerned.
However, the introduction of ref fields and the default initialization behavior means ref could point to null, so reading from it then discarding the result is not always a no-op. The side effect should be preserved.