【问题标题】:Is there a workaround to the C# 28-time inline limit?C# 28 次内联限制是否有解决方法?
【发布时间】:2011-04-27 07:48:56
【问题描述】:

我正在使用 Red Gate 的 Performance Profiler 优化物理模拟程序。处理碰撞检测的代码中有大约 52 项以下小检查,在两种情况下处理 3 维 26 个方向的单元格。

CollisionPrimitiveList cell = innerGrid[cellIndex + 1];
if (cell.Count > 0)
    contactsMade += collideWithCell(obj, cell, data, ref attemptedContacts);

cell = innerGrid[cellIndex + grid.XExtent];
if (cell.Count > 0)
    contactsMade += collideWithCell(obj, cell, data, ref attemptedContacts);

cell = innerGrid[cellIndex + grid.XzLayerSize];
if (cell.Count > 0)
    contactsMade += collideWithCell(obj, cell, data, ref attemptedContacts);

作为程序的一个极其紧凑的循环,所有这些都必须使用相同的方法,但我突然发现,在我将区域从二维扩展到三维之后(从计数增加到 52 次检查) 16),突然 cell.Count 不再被内联,即使它是一个简单的吸气剂。 public int Count { get { return count; } } 这造成了巨大的性能损失,我花了相当长的时间才发现,当 cell.Count 在方法中出现 28 次或更少时,它每次都被内联,但是一旦 cell.Count 在方法中出现 29 次或更多,它一次都没有内联(尽管绝大多数调用来自代码中很少执行的最坏情况部分。)

回到我的问题,有人知道绕过这个限制吗?我认为简单的解决方案只是将计数字段设置为内部而不是私有,但我想要一个比这更好的解决方案,或者至少只是更好地了解情况。我希望在 Microsoft 的编写高性能托管应用程序页面 http://msdn.microsoft.com/en-us/library/ms973858.aspx 上会提到这类事情,但遗憾的是它不是(可能是因为 28 个计数限制是多么随意?)

我正在使用 .NET 4.0。

编辑:看起来我误解了我的小测试。我发现内联失败不是因为方法本身被调用了 28 次以上,而是因为按照某种标准,它们应该内联的方法“太长”。这仍然让我感到困惑,因为我看不出如何合理地不内联一个简单的 getter(并且内联它们的性能要好得多,因为我的分析器清楚地向我展示了),但显然 CLI JIT 编译器拒绝内联任何东西只是因为该方法已经很大(玩弄细微的变化告诉我这个限制是1500的代码大小(来自idasm),超过它没有进行内联,即使在我的getter的情况下,一些测试表明没有添加额外的代码要内联的开销)。

谢谢。

【问题讨论】:

  • 如果你真的关心性能,你不会使用getter-setter。他们只是碍事。
  • 有趣。听起来像是寄存器优化——也许反汇编 IL 会给出一些提示?
  • Ron,我想这可以满足我的需要,但我也喜欢通过不允许写访问来获得 getter 为我提供的保护(Count 实际上是链接中的节点数在这里列出,如果不小心改变了,事情可能会很糟糕)。 Jeremy,我可能会尝试查看 IL,但我的印象是它无济于事,因为内联仅由 CLR JIT 编译器完成。 (感谢我的分析器,我只能讲述内联)
  • 阅读 blogs.msdn.com/b/davidnotario/archive/2004/11/01/250398.aspx 给了我这样的想法,即这种方法正在达到 O(n^2) 与 O(n) 的优化算法选择。来自该站点:“如果您找到解决方案的时间有限,一个好的方法可能是为小 Ns 做最佳解决方案,并为较大的 Ns 回退到非最优但快速的求解器。”我建议尝试将该方法拆分为 2 个子方法并连续调用它们 - 这可能会使您受到 JIT 的限制。
  • @Alk:将属性 getter 公开,并将下划线成员设为内部。那么你就不允许外部用户搞砸了。

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


【解决方案1】:

我没有对此进行测试,但似乎一种可能的解决方法是让多个属性都返回相同的东西。可以想象,每个属性可以得到 28 个内联。

请注意,内联方法的次数很可能取决于该方法的本机代码的大小(请参阅http://blogs.msdn.com/b/vancem/archive/2008/08/19/to-inline-or-not-to-inline-that-is-the-question.aspx),数字 28 特定于该属性。一个简单的属性可能比一个更复杂的方法被内联的次数更多。

【讨论】:

  • 虽然我不能说你有答案,但我尝试了你关于返回相同内容的多个属性的想法,但很惊讶它实际上并没有导致内联发生,这让我意识到这是父方法的大小,我粗略估计的调用次数,这实际上是罪魁祸首。
【解决方案2】:

直截了当,这并不能解释为什么 28 是神奇的数字,但我很好奇如果你将所有候选 CollisionListPrimitive 实例整理到一个数组中,然后在其中调用你的“if count > 0”块会发生什么数组的循环?

cell.Count 调用是否再次内联?

例如

CollisionPrimitiveList[] cells = new CollisionPrimitiveList {
    innerGrid[cellIndex + 1],
    innerGrid[cellIndex + grid.XExtent],
    innerGrid[cellIndex + grid.XzLayerSize]
    // and all the rest
};

// Loop over cells - for demo only. Use for loop or LINQ'ify if faster
foreach (CollisionPrimitiveList cell in cells) 
{
    if (cell.Count > 0)
        contactsMade += collideWithCell(obj, cell, data, ref attemptedContacts);  
}

我知道性能是问题所在,构建数组并循环遍历它会产生开销,但如果 cell.Count 再次内联,整体性能是否会更好/足够好?

【讨论】:

  • 鉴于我刚刚发现它是父方法代码大小,这无疑会减少到足以导致内联再次发生。 OTOH,这仅适用于部分方法,因为许多单元格在测试之前必须进行范围检查。
【解决方案3】:

我猜测(尽管绝不是肯定的)这可能与提到的enregistration 问题有关——CLR 可能为每个 if 语句分配一个新变量,并且这些变量超过了共 64 个变量。你觉得可能是这样吗?

【讨论】:

  • 我试图通过在可能导致这些假定的额外变量分配的单元格中添加任意 if 语句来测试这个假设,但通过我的测试,这几乎没有什么区别,而是内联是否发生与代码大小有关。我敢肯定,真正不同的是原生大小,但 ildasm 报告的大小的阈值始终在 1500 或 1501 左右。
  • @Akh:您的语句没有完全优化,对吧?他们有什么副作用吗? (尝试有条件地增加一个全局变量。)
  • 它们没有被优化出来,我的预测能力如何? --他们正在增加一个变量(contactsMade)并与 cell.Count 进行比较。
  • @Akh:哦哈哈好吧,那我不知道……不过这很有趣!
猜你喜欢
  • 2020-08-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-08-16
相关资源
最近更新 更多