为什么只读字段检查不能在循环之外进行优化?

Why can't readonly field check be optimised out of a loop?

当我有一个只读变量时:

public readonly bool myVar = true;

然后用这样的代码检查它:

for(int i = 0; i != 10; i++)
{
    if(myVar)
        DoStuff();
    else
        DoOtherStuff();
}

查看发出的 IL,我可以看到在循环的每次迭代中都执行了检查。我希望上面的代码产生与此相同的 IL:

if (myVar)
{
    for(int i = 0; i != 10; i++)
    {
        DoStuff();
    }
}
else
{
    for(int i = 0; i != 10; i++)
    {
        DoOtherStuff();
    }
}

为什么不在循环外部优化检查,因为该字段是只读的并且不能在迭代之间更改?

因为它可以使用反射来改变:

using System;
using System.Reflection;

public class Program
{
    public static void Main()
    {
        var t = new Test();
        var field = typeof(Test).GetField("myVar", BindingFlags.Instance | BindingFlags.Public);

        Console.WriteLine(t.myVar); // prints True

        field.SetValue(t, false);
        Console.WriteLine(t.myVar); // prints False

        // Trying to use t.myVar = false or true; <-- does not compile
    }
}

public class Test
{
    public readonly bool myVar = true;
}

工作Fiddle:https://dotnetfiddle.net/W9UO3m

请注意,编译时代码优化器无法绝对确定地预测或检测此类反射代码是否存在,如果存在,它是否会运行。

一个readonly字段可以在代码的不同点(多个构造函数)被初始化为不同的值。它无法优化,因为多个构造函数允许相关字段的多个值。 现在,如果您只有一个构造函数,没有相关的分支,因此对于该字段只有一个可能的值,我希望优化器提供更多。但是,这种分析是 C++ 的编译时间比 C# 长得多的原因之一,而且这类任务通常不会委托给运行时。

为了更清楚的解释:

Note

The readonly keyword is different from the const keyword. A const field can only be initialized at the declaration of the field. A readonly field can be assigned multiple times in the field declaration and in any constructor. Therefore, readonly fields can have different values depending on the constructor used. Also, while a const field is a compile-time constant, the readonly field can be used for run-time constants as in the following example:

https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/readonly

对于您的具体情况,我建议使用 const 而不是 readonly,尽管我没有在生成的 IL 中寻找差异。

老实说,我想知道它是否真的有所作为。任何执行分支预测并具有中等体面缓存的 CPU 在任何一种情况下都可能产生相同的性能。 (请注意,我还没有测试;这只是一种怀疑。)

您提出的优化实际上是两个单独的更简单转换的组合。首先是将成员访问拉到循环之外。来自

for(int i = 0; i != 10; i++)
{
    var localVar = this.memberVar;
    if(localVar)
        DoStuff();
    else
        DoOtherStuff();
}

var localVar = this.memberVar;
for(int i = 0; i != 10; i++)
{
    if(localVar)
        DoStuff();
    else
        DoOtherStuff();
}

第二个是将循环条件与if条件互换。来自

var localVar = this.memberVar;
for(int i = 0; i != 10; i++)
{
    if(localVar)
        DoStuff();
    else
        DoOtherStuff();
}

var localVar = this.memberVar;
if (localVar) {
    for(int i = 0; i != 10; i++)
        DoStuff();
}
else {
    for(int i = 0; i != 10; i++)
        DoOtherStuff();
}

第一个受到readonly的影响。为此,编译器必须证明 memberVar 不能在循环内改变,并且 readonly 保证这个 1 —— 即使这个循环可以在内部调用一个构造函数,并且 memberVar 的值可以在循环结束后在构造函数中更改,它不能在循环体中更改 -- DoStuff() 不是当前对象的构造函数,也不是 DoOtherStuff()。反射不算数,虽然 可能 使用反射来破坏不变量,但不允许这样做。线程确实很重要,请参阅脚注。

第二个是简单的转换,但编译器更难做出决定,因为很难预测它是否真的会提高性能。自然可以分开看,自己对代码做第一次改造,看生成什么代码。

也许更重要的考虑是,在 .NET 中,优化过程发生在 MSIL 和机器代码之间,而不是在将 C# 编译为 IL 期间。因此,您无法通过查看 MSIL 来了解正在进行哪些优化!


1 还是这样? .NET 内存模型比例如在 C++ 模型中,任何数据竞争都会很快导致未定义的行为,除非对象被定义为 volatile/atomic。如果此循环在从对象构造函数派生的工作线程中运行,并且在派生线程之后,构造函数继续(我将称之为“下半场”)以更改 readonly 成员,会怎样?内存模型是否需要工作线程看到该更改?如果 DoStuff() 和构造函数的后半部分强制内存栅栏,例如访问 volatile 的其他成员或获取锁怎么办?所以readonly只允许在单线程环境下优化.