我经常编写将成员变量复制到本地堆栈变量的代码,相信它会通过删除每次访问成员变量时必须发生的指针取消引用来提高性能。
这有效吗?
例如
public class Manager {
private readonly Constraint[] mConstraints;
public void DoSomethingPossiblyFaster()
{
var constraints = mConstraints;
for (var i = 0; i < constraints.Length; i++)
{
var constraint = constraints[i];
// Do something with it
}
}
public void DoSomethingPossiblySlower()
{
for (var i = 0; i < mConstraints.Length; i++)
{
var constraint = mConstraints[i];
// Do something with it
}
}
}
我的想法是DoSomethingPossiblyFaster实际上比DoSomethingPossiblySlower更快。
我知道这几乎是微观优化,但要有明确的答案会很有用。
修改 只是为此添加一点背景。我们的应用程序必须处理来自电信网络的大量数据,对于我们的一些服务器,这种方法可能每天被称为大约10亿次。我的观点是每一点都有帮助,有时我想做的就是给编译器一些提示。
答案 0 :(得分:16)
哪个可读?这通常应该是您的主要激励因素。您是否甚至需要使用for
循环代替foreach
?
由于mConstraints
是readonly
我可能期望JIT编译器为您执行此操作 - 但实际上,您在循环中做了什么?这种重要性的可能性非常小。为了便于阅读,我几乎总是选择第二种方法 - 我希望尽可能foreach
。 JIT编译器是否优化了这种情况将在很大程度上取决于JIT本身 - 它可能因版本,体系结构,甚至方法的大小或其他因素而异。这里可以 没有“明确的”答案,因为替代JIT总是有可能以不同的方式进行优化。
如果您认为自己处于一个真正重要的角落,那么您应该对其进行基准测试 - 尽可能使用尽可能真实的数据。 只有这样才能将代码从最易读的形式中删除。如果你“经常”编写这样的代码,你似乎不太可能给自己带来任何好处。
即使可读性差异相对较小,我也会说它仍然存在且显着 - 而我肯定期望性能差异可以忽略不计。
答案 1 :(得分:4)
如果编译器/ JIT尚未执行此操作或类似的优化(这是一个很大的if),那么DoSomethingPossiblyFaster
应该比DoSomethingPossiblySlower
更快。解释原因的最佳方法是将C#代码粗略地翻译为直接C。
当调用非静态成员函数时,会将一个隐藏指向this
的指针传递给该函数。您将大致有以下内容,忽略虚函数调度,因为它与问题无关(或者为了简单而等效地使Manager
密封):
struct Manager {
Constraint* mConstraints;
int mLength;
}
void DoSomethingPossiblyFaster(Manager* this) {
Constraint* constraints = this->mConstraints;
int length = this->mLength;
for (int i = 0; i < length; i++)
{
Constraint constraint = constraints[i];
// Do something with it
}
}
void DoSomethingPossiblySlower()
{
for (int i = 0; i < this->mLength; i++)
{
Constraint constraint = (this->mConstraints)[i];
// Do something with it
}
}
区别在于DoSomethingPossiblyFaster
,mConstraints
存在于堆栈上,访问只需要一层指针间接,因为它位于堆栈指针的固定偏移处。在DoSomethingPossiblySlower
中,如果编译器错过了优化机会,则会有一个额外的指针间接。编译器必须从堆栈指针读取固定偏移量才能访问this
,然后从this
读取固定偏移量以获得mConstraints
。
有两种可能的优化可以抵消这种打击:
编译器可以完全按照您手动执行的操作并在堆栈上缓存mConstraints
。
编译器可以将this
存储在寄存器中,这样在解除引用之前,它不需要在每次循环迭代时从堆栈中获取它。这意味着从mConstraints
或从堆栈中取出this
基本上是相同的操作:从已经在寄存器中的指针中单个取消引用固定偏移量。
答案 2 :(得分:3)
你知道你会得到的回应,对吗? “时间吧。”
可能没有确定的答案。首先,编译器可能会为您进行优化。其次,即使不是这样,在装配级别的间接寻址可能也不会明显变慢。第三,与循环迭代次数相比,它取决于制作本地副本的成本。然后有缓存效果需要考虑。
我喜欢优化,但这个地方我肯定会说等到你遇到问题然后再做实验。这是一种可能的优化,可以在需要时添加,而不是需要预先计划的优化之一,以避免以后产生巨大的连锁反应。
编辑:(朝着明确的答案)
在发布模式下编译两个函数并用IL Dasm检查IL表明在两个地方“PossiblyFaster”函数使用局部变量,它只有少一个指令
ldloc.0
vs
ldarg.0; ldfld class Constraint[] Manager::mConstraints
当然,这仍然是从机器代码中删除的一个级别 - 您不知道JIT编译器将为您做什么。但“可能更快”可能会略微加快 但是,在您确定此功能是系统中最昂贵的功能之前,我仍然不建议添加额外的变量。
答案 3 :(得分:1)
我已经对此进行了分析并提出了一些有趣的结果,这些结果可能仅对我的具体示例有效,但我认为值得注意的是这里。
最快的是X86发布模式。它在7.1秒内运行我的测试的一次迭代,而等效的X64代码需要8.6秒。这运行了5次迭代,每次迭代处理循环1920万次。
循环的最快方法是:
foreach (var constraint in mConstraints)
{
... do stuff ...
}
第二快的方法,让我大吃一惊的是以下
for (var i = 0; i < mConstraints.Length; i++)
{
var constraint = mConstraints[i];
... do stuff ...
}
我想这是因为mConstraints存储在循环的寄存器中。
当我删除mConstraints的readonly选项时,这个速度变慢了。
所以,我的总结是在这种情况下可读确实也提供了性能。