【问题标题】:In C#, does copying a member variable to a local stack variable improve performance?在 C# 中,将成员变量复制到本地堆栈变量会提高性能吗?
【发布时间】:2011-11-23 18:57:19
【问题描述】:

我经常编写将成员变量复制到本地堆栈变量的代码,我相信这样可以通过删除访问成员变量时必须发生的指针取消引用来提高性能。

这有效吗?

例如

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 亿次。我的观点是,每一点小事都有帮助,有时我想做的只是给编译器一些提示。

【问题讨论】:

  • 你分析过它吗?
  • 也许你通过这样做获得的时间是“丢失”的,因为它需要时间将指针复制到另一个内存位置(我们正在谈论微优化,所以我认为它们是合理的事情考虑)。
  • @mike,即使他已经对其进行了分析,解释为什么仍然是一个有用的工件。
  • 我对分析这个并不感兴趣。我试图了解这种微优化是否应该起作用。我实际上认为乔恩的答案是正确的 - 更具可读性。而且我同意微优化不是更具可读性。
  • 这不是微优化;这是一个纳米优化。您几乎肯定会遇到比这个问题大 数千数百万 倍的问题。你有一个满是黑莓荆棘的院子,你正试图通过用镊子调整单个草叶来改善草坪的外观。而是花时间清理荆棘。

标签: c# performance


【解决方案1】:

哪个更可读?这通常应该是你的主要激励因素。你甚至需要使用for 循环而不是foreach

由于mConstraintsreadonly,我可能希望 JIT 编译器为您执行此操作 - 但实际上,您在循环中做什么?这很重要的机会非常小。我几乎总是选择第二种方法只是为了可读性 - 我更喜欢foreach 在可能的情况下。 JIT 编译器是否优化这种情况在很大程度上取决于 JIT 本身 - 这可能因版本、体系结构、甚至方法的大小或其他因素而异。这里可能没有“确定的”答案,因为替代的 JIT 总是有可能以不同的方式进行优化。

如果您认为自己处于真正很重要的极端情况,您应该对它进行基准测试 - 彻底地使用尽可能真实的数据。 只有这样你才应该改变你的代码而不是最易读的形式。如果您“经常”编写这样的代码,那么您似乎不太可能帮自己任何忙。

即使可读性差异相对较小,我会说它仍然存在且显着 - 而我当然预计性能差异可以忽略不计。

【讨论】:

  • +1,即使你是唯一一个编写这些东西的人,在离开它几个月后回来尝试再次理解这些东西,哈哈打赌你不能,就像无法阅读自己的笔迹一样。
  • -1。这是一个逃避的答案。想知道 x 是否应该比 y 快是一个合理的问题。当差异非常小时,基准测试可能会很嘈杂。 OP 可能会问这个问题,因为他/她想要一个更好的关于计算机如何在后台工作的心智模型,而不是为了直接的实用价值。
  • @dsimcha:OP 要求给出明确的答案,我已经解释了为什么这是不可行的。
  • @Jon:我给了他最接近确定答案的可行方法,因为正如你所说,没有确定的答案。你给他讲了一些无关紧要的事情。这是编译器优化的问题,我解释了假设一个极其愚蠢的编译器会发生什么,以及哪些编译器优化可能会抵消这种影响。
  • @dsimcha:问题是,我觉得这个问题是基于一个有缺陷的前提:“有一个明确的答案会很有用”——我的回答基本上是说不,那是 没有 有用(即使它可能是确定的),除非你处于一个真正的极端情况,它实际上会产生影响——在这种情况下它应该是可衡量的。我不认为它是切线相关的 - 他说这是他“经常”编写的那种代码,基本上我是说停止这样做,以及为什么。
【解决方案2】:

如果编译器/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
    }
}

不同之处在于DoSomethingPossiblyFastermConstraints 位于堆栈中,并且访问只需要一层指针间接,因为它与堆栈指针的偏移量是固定的。在DoSomethingPossiblySlower 中,如果编译器错过了优化机会,则会出现额外的指针间接寻址。编译器必须从堆栈指针读取一个固定的偏移量才能访问this,然后从this 读取一个固定的偏移量才能获得mConstraints

有两种可能的优化可以抵消这个命中:

  1. 编译器可以完全按照您手动执行的操作并将mConstraints 缓存在堆栈上。

  2. 编译器可以将this 存储在一个寄存器中,这样它就不需要在每次循环迭代之前从堆栈中获取它,然后再取消引用它。这意味着从this 或堆栈中获取mConstraints 基本上是相同的操作:从已经在寄存器中的指针的固定偏移量的单个取消引用。

【讨论】:

  • 现在这更像是我所期待的答案。谢谢。
【解决方案3】:

你知道你会得到什么回应,对吧? “计时。”

可能没有明确的答案。首先,编译器可能会为您进行优化。其次,即使没有,汇编级别的间接寻址也可能不会明显变慢。第三,与循环迭代次数相比,它取决于制作本地副本的成本。然后还有缓存效果需要考虑。

我喜欢优化,但我肯定会说这是一个地方,等到你遇到问题,然后再试验。这是可以在需要时添加的可能优化,而不是需要预先计划以避免以后产生巨大连锁反应的那些优化之一。


编辑:(朝着明确的答案)

在发布模式下编译这两个函数并使用 IL Dasm 检查 IL 表明,在这两个地方,“PossiblyFaster”函数都使用局部变量,它少了一条指令
ldloc.0 vs
ldarg.0; ldfld class Constraint[] Manager::mConstraints

当然,这仍然是从机器代码中删除的一层——您不知道 JIT 编译器会为您做什么。但“PossiblyFaster”可能会稍微快一些。
但是,我仍然不建议您添加额外的变量,直到您确定这个函数是您系统中最昂贵的东西。

【讨论】:

  • 啊!我被斯基特了!仅仅因为他打字速度更快并且给出了更好的答案,我不知道他为什么会得到所有的选票;)
  • 应该对 SO rep 征税 :)
【解决方案4】:

我已经对此进行了分析,并提出了一些有趣的结果,这些结果可能仅对我的具体示例有效,但我认为在此值得一提。

最快的是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 的只读选项时,速度变慢了。

因此,我对此的总结是,在这种情况下可读确实也能提供性能。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-05-17
    • 2023-03-18
    • 2011-04-13
    • 2019-02-22
    • 2012-05-16
    • 2019-09-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多