有没有绕过C# 28次内联限制的方法?

32

我正在使用Red Gate的性能分析器优化物理模拟程序。代码中的一部分涉及碰撞检测,其中包含大约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);
作为程序中一个非常紧密的循环,所有这些代码都必须在同一个方法中,但是我发现,在我将区域从二维扩展到三维后(将检查次数从16增加到52),突然间,即使它只是一个简单的getter,cell.Count也不再被内联。 public int Count { get { return count; } } 这导致了巨大的性能损失,并且花费了我相当长的时间才发现,当cell.Count出现在方法中不超过28次时,每次都会被内联,但一旦出现在方法中29次或更多次时,它就再也没有被内联过(尽管大部分调用来自极限情况下的代码部分,这些部分很少被执行)。
所以回到我的问题,有人有任何想法来避免这个限制吗?我认为简单的解决方案就是将count字段设置为internal而不是private,但我想要更好的解决方案,或者至少对这种情况有更好的理解。我希望微软的编写高性能托管应用程序页面上http://msdn.microsoft.com/en-us/library/ms973858.aspx提到了这种情况,但可悲的是它没有(可能是因为28次的限制有多么随意)。
我使用的是.NET 4.0。
编辑:看起来我误解了我的小测试。我发现内联失败并不是由于方法本身被调用了28次以上,而是因为它们应该内联到的方法“太长”了,按照某些标准。这仍然让我感到困惑,因为我不明白一个简单的getter如何会被合理地拒绝内联(并且我的分析器清楚地告诉我,在它们内联之后性能显着提高),但显然CLI JIT编译器拒绝内联任何东西,只是因为方法已经很大了(通过微小变化进行的测试表明,这个限制是1500行代码大小(从idasm中),超过它就不再内联,即使在我的getter情况下也是如此,一些测试表明,它们内联时不会增加任何额外的代码开销)。
谢谢。

5
如果您真的关心性能,那么您不会使用getter-setter。它们只会妨碍。 - Ron
Ron,我想这对我的需求来说已经足够了,但我也喜欢getter提供的保护,因为它不允许写入访问(这里Count实际上是链表中节点的计数,如果意外更改可能会导致非常糟糕的结果)。Jeremy,我可以尝试查看IL,但我认为它没有帮助,因为内联只由CLR JIT编译器完成。(我只能通过分析器告诉内联情况) - Akh
@Russ 这是个有趣的想法,但遗憾的是我没有注意到任何区别。 - Akh
1
@Alk:将属性的getter设置为public,底层成员设置为internal。这样就不会让外部用户搞砸它了。 - Brian
显示剩余7条评论
3个回答

4

1
虽然我不能说你的答案是正确的,但我尝试了你提出的多个返回相同内容的属性的想法,并惊讶地发现它实际上并没有导致内联,这让我意识到实际上是父方法的大小,大约与我所调用的次数相近,才是罪魁祸首。 - Akh

1
首先,这并没有解释为什么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 是再次内联的话,整体性能是否仍然更好/足够好?


考虑到我刚刚发现的关于它是父方法代码大小的问题,这无疑会将其减少足够以再次引入内联。但另一方面,这只能针对部分方法起作用,因为许多单元格在被测试之前必须进行范围检查。 - Akh

0

我猜(虽然不确定)这可能与提到的enregistration问题有关--可能是CLR为每个if语句分配了一个新变量,而这些变量超过了64个。你觉得这可能是原因吗?


我尝试通过在可能导致这些额外变量分配的单元格中添加任意if语句来测试这个假设,但是通过我的测试,这几乎没有什么区别,而是否进行内联处理与代码大小有关。我确信是本地大小造成了真正的差异,但ildasm报告的大小始终在1500或1501左右。 - Akh
@Akh:你的语句并没有完全被优化掉,对吧?它们有一些副作用吗?(尝试有条件地增加一个全局变量。) - user541686
它们没有被优化掉,那我有多可预测呢?-- 它们正在递增一个变量(contactsMade)并与cell.Count进行比较。 - Akh
@Akh:哦哈哈,好的,那我也不知道了...不过还是挺有趣的! - user541686

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接