【问题标题】:Why can't readonly field check be optimised out of a loop?为什么不能循环优化只读字段检查?
【发布时间】:2021-04-01 21:51:45
【问题描述】:

当我有一个只读变量时:

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();
    }
}

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

【问题讨论】:

  • 因为readonly 只是一个编译时规则。它实际上并没有保证任何东西。见重复。优化不得违反法律法规,无论多么不明智。
  • 另请注意,检查生成的 CIL 并没有真正显示任何内容。生成的本机代码才是最重要的,我很确定我已经看到了这种优化。

标签: c# compiler-optimization


【解决方案1】:

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

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 保证 this1 -- 即使可以在构造函数内调用此循环,并且memberVar 可以在循环结束后在构造函数中更改,不能在循环体中更改 -- DoStuff() 不是当前对象的构造函数,DoOtherStuff() 也不是。反射不计算在内,虽然可能可能使用反射来破坏不变量,但不允许这样做。线程确实很重要,请参阅脚注。

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

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


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

【讨论】:

    【解决方案2】:

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

    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;
    }
    

    工作小提琴:https://dotnetfiddle.net/W9UO3m

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

    【讨论】:

    • 这是一个经过深思熟虑的答案并且提出了一个有效的观点,但是:我还没有遇到过故意禁用优化以保持反射快乐的情况。还有其他例子吗?
    • @3Dave 这可能有点做作,但它确实给出了编译器无法优化它的一个很好的理由
    • 使用反射破坏不变量会导致未定义的行为。参见例如stackoverflow.com/a/19966051/103167
    【解决方案3】:

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

    为了更清楚的解释:

    注意

    readonly 关键字与 const 关键字不同。一个常量 字段只能在字段声明时进行初始化。一种 readonly 字段可以在字段声明中多次赋值 并在任何构造函数中。因此,只读字段可以有不同的 值取决于使用的构造函数。此外,虽然 const 字段是 编译时常量,只读字段可用于运行时 常量如下例所示:

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

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

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

    【讨论】:

    • 我不希望它像 const 那样被优化(const 将在 IL 中由它的值表示,整个 else 部分将是 noop,因为我们会在编译时知道该值) .我希望它被优化为仅在循环之外进行一次检查,至少如果我们不在构造函数或静态方法中,因为此时已经分配了值。
    • 循环内可以修改i,但不影响检查结果。
    • @defaultUsernameN 您不能基于此优化 IL,因为它在类的所有实例之间共享。您必须在编译时验证每个可能的实例化对于readonly 字段具有相同的值。通过使用readonly,您是在告诉编译器它可能具有不同的值。 (不过,if 上的有效点。)
    • 只读变量不共享,因为它不是静态的。当然,方法代码是共享的,但这只会使建议的优化也共享-有什么害处?但无论如何,另一个答案中有一点确实指向 readonly var 将被合法更改的情况 - 从另一个线程中的构造函数。编辑了这个问题,以确保它不像我期望的那样读取,就像它是一个 const 一样丢弃整个检查 - 感谢您指出这一点!
    猜你喜欢
    • 2014-08-28
    • 2018-11-01
    • 1970-01-01
    • 2013-03-01
    • 1970-01-01
    • 2017-12-08
    • 2012-03-20
    • 2020-09-09
    • 1970-01-01
    相关资源
    最近更新 更多