【问题标题】:Can I stop .NET 4 performing tail-call elimination?我可以停止 .NET 4 执行尾调用消除吗?
【发布时间】:2012-07-14 04:37:54
【问题描述】:

我们正在将应用迁移到 .NET 4.0(从 3.5)。我们遇到的问题之一只能在非常特定的条件下重现:

  • 仅在发布版本中
  • 仅在启用优化和/或调试信息设置为 pdb-only 的情况下。

我的意思是,如果我禁用优化将调试信息设置为完整,问题就会消失。

有问题的代码在 .NET 3.5 上运行良好,在发布模式下启用了优化等,并且已经运行了很长时间。

我真的不想暗示 C# 编译器中存在错误,所以我的问题是我是否可以使用任何技术来追踪我们可能做错了什么导致不正确的优化?

我正在尝试将这个问题缩小到一个小测试用例,以便我可以在这里发布一些代码。

编辑:

我已将问题归结为以下几点:

我们在表单的构造函数中有这段代码:

public ConnectionForm()
{
    LocalControlUtil.Configure("ConnectionForm", "Username", usernameLabel);
    LocalControlUtil.Configure("ConnectionForm", "Password", passwordLabel);
    LocalControlUtil.Configure("ConnectionForm", "Domain", domainLabel);
    LocalControlUtil.Configure("ConnectionForm", "Cancel", cancelButton);
    LocalControlUtil.Configure("ConnectionForm", "OK", okButton);
}

这些调用是针对一些自定义本地化代码的。此表单的构造函数是从另一个程序集中调用的。 LocalControlUtil.Configure 方法调用 Assembly.GetCallingAssembly(),它为上述所有调用返回正确的值,除了最后一个

我可以重新排序上面的行,添加新行或删除当前行,每次都是最后一行不起作用。

我假设这是 JIT 将最后一个方法调用内联到调用构造函数的位置(在另一个程序集中)。在上面的构造函数中添加[MethodImpl(MethodImplOptions.NoInlining)] 可以解决问题。

有人知道为什么会这样吗?我觉得很奇怪,最后一行只能内联。这是 .NET 4.0 中的新行为吗?

编辑 2:

我现在已将其范围缩小到消除尾调用,我认为是由 new tail-call stuff in .NET 4 引起的。

在上面的代码中,构造函数中对LocalControlUtil.Configure的最后一次调用被消除并放入调用方法中,该方法在另一个程序集中。当方法调用Assembly.GetCallingAssembly 时,我们没有得到正确的程序集。

有什么方法可以阻止编译器(或 JIT 或其他任何行为)消除尾调用?

【问题讨论】:

  • 前几天我确实在 SO 上看到了一个类似的问题,它肯定看起来编译器中存在错误,因为它优化不正确。所以,这可能的,但据我了解,这种情况相当罕见。
  • 我记得在博客中读到过这个。不知何故,编译器导致某些对象被过早地处理掉。该示例是一个秒表方法,它在失败之前只运行了 1 个循环。很遗憾,我没有帖子的链接。
  • 你能告诉我们更多关于这个问题的细节吗?例如,当您更改优化设置时,浮点计算有多种不同的方式。线程计时、垃圾收集计时等可以通过多种方式进行更改。问题的本质是什么?

标签: c# .net optimization compiler-construction .net-4.0


【解决方案1】:

如果不是太长,我只是把它放在评论中,但你试过了吗:

public ConnectionForm()
{
  try
  {
    LocalControlUtil.Configure("ConnectionForm", "Username", usernameLabel);
    LocalControlUtil.Configure("ConnectionForm", "Password", passwordLabel);
    LocalControlUtil.Configure("ConnectionForm", "Domain", domainLabel);
    LocalControlUtil.Configure("ConnectionForm", "Cancel", cancelButton);
    LocalControlUtil.Configure("ConnectionForm", "OK", okButton);
  }
  catch
  {
    throw;
  }
}

引发的任何异常都被抛出这一事实使得这对于编写的代码而言几乎是一个空更改,但 try-catch 边界通常会阻止编译器和抖动部分的优化。

【讨论】:

  • 这确实有效,但这意味着我需要将这个 try-catch 放在任何调用 LocalControlUtil.Configure 的地方(很多地方)。真的,我希望有一个全球性的“不要消除尾声”解决方案。
【解决方案2】:

我认为您已经在寻找问题的正确轨道上......将问题缩小到最小的测试用例几乎总是一个很好的第一步。

接下来将比较为每个版本生成的 IL,例如 Reflector。阅读 IL 并非易事(除非您有丰富的经验),因此最大限度地减少您必须查看的代码量至关重要。

更新:我对此进行了更多思考,如果问题在 IL 级别无法辨别,则可能是 JIT(及时)级别,这明显更难调试。我从来不需要达到那个水平,所以我对问题的那一部分没有任何洞察力(如果涉及到那个)。

【讨论】:

    【解决方案3】:

    不,你不能。

    .NET 4.0 比 3.5 优化了更多的尾调用,这是一件好事。我们的代码非常愚蠢。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-09-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多