Skip to content

Incorrect optimization: a faulty load could be eliminated when compiling C# to IL #81574

@GeeLaw

Description

@GeeLaw

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 ...Param and ...Method do not have ldind.i4.
  • When optimization is off, methods DiscardParam and DiscardMethod does not have ldind.i4.
  • Regardless of optimization, methods ...Direct always has ldind.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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions