局部变量与静态变量内存和性能
本文关键字:性能 内存 变量 静态 局部变量 | 更新日期: 2023-09-27 18:36:48
非静态方法调用中的静态变量存储在哪里? 即在 CalculateTotalStatic()
内部,我们正在递增静态成员MyStaticVariableHolder.Total
,并将变量i
与 for 循环中的MyStaticVariableHolder.TotalArray.Length
进行比较。
另一方面,在此方法的另一个版本中 CalculateTotalLocal()
,我们使用在方法中声明的局部变量来执行上述两个操作。
在CalculateTotalLocal
过程中,堆栈上将放置两个额外的变量,它们将它们的值保存在堆栈本身(localTotal
和localLength
)。在CalculateTotalStatic
的情况下会发生什么?它是否每次都访问堆中的静态变量?此外,CalculateTotalLocal
比 CalculateTotalStatic
快 10% .这种性能改进的正确原因是什么?
编辑 - 我想我不是很清楚 - 为此道歉。
我想说的是:
是否可以(根据 C# 规范)通过兼容的 C# 编译器/JIT 以与局部变量相同的方式优化静态变量访问?
class MyStaticVariableHolder
{
public static int[] TotalArray = new int[100];
public static int Total = 1;
}
class Trial
{
public void CalculateTotalStatic()
{
for (int i = 0; i < MyStaticVariableHolder.TotalArray.Length; i++)
{
MyStaticVariableHolder.Total += i;
}
}
public void CalculateTotalLocal()
{
int localTotal = MyStaticVariableHolder.Total;
int[] localTotalArray = MyStaticVariableHolder.TotalArray;
int localLength = MyStaticVariableHolder.TotalArray.Length;
for (int i = 0; i < localLength; i++)
{
localTotal += i;
}
MyStaticVariableHolder.Total = localTotal;
}
}
我也在查看此链接 - http://www.dotnetperls.com/local-variable-field-optimization 供参考,但我没有实现他们获得的那么多的性能改进。
静态变量存储在非静态方法调用中的什么位置?
我的意思是:从静态变量中获取值以对其执行一些计算。因此,将复制该值。副本制作到什么存储?
在 IL 级别,它被制作到评估堆栈。 运行时具有广泛的自由度,可以随心所欲地调整评估堆栈。 当前线程的堆栈或寄存器都可能。
我注意到在某些情况下可以复制到堆位置。
在 CalculateTotalStatic 的情况下会发生什么?它是否每次都访问堆中的静态变量?
等等,谁说他们首先在堆上?不需要将静态变量存储在垃圾回收堆上。他们的记忆不会被收集,那么他们为什么要在堆上呢? (变量引用的数组当然在堆上。
让我们重新措辞。
在 CalculateTotalStatic 的情况下会发生什么?它是否每次都对静态变量进行全新访问?
这个问题仍然无法回答。再次改写。
运行时是否需要每次都从变量中重新获取?
不。 允许运行时执行任何在单线程程序中不可见的优化。 (这有点夸大其词;它不会执行一些优化。我不打算列出它们是什么。内化这一事实。 在单线程程序中,除非移动,否则一切都是稳定的。 在多线程程序中,除非保持静止,否则一切都在移动。允许运行时假设前者,即使在后者为真的世界中也是如此。
此外,CalculateTotalLocal 比 CalculateTotalStatic 快 10%。这种性能改进的正确原因是什么?
我不知道。检查生成的代码,看看有什么区别。
是否可以(根据 C# 规范)通过兼容的 C# 编译器/JIT 以与局部变量相同的方式优化静态变量访问?
绝对是的。这完全属于"在单线程程序中不可见的任何优化"。
此外:运行时不仅允许按照它认为合适的方式对读取和写入进行重新排序。还允许不同的线程观察不一致的世界。如果一个线程观察正在读取和写入的变量的特定时间序列,则允许另一个线程观察完全不同的读取和写入交错。
此外:永远不要忘记,我所说的运行时也是指运行时所依赖的任何东西,比如 CPU。 请记住,允许 CPU 在他们认为合适的情况下对读取和写入进行重新排序。仅仅因为您正在查看一些 x86 代码,这些代码显然将内存中的位置读入寄存器,这与读取实际进入内存的时间完全无关。该内存位置可能已经在缓存中,并且主内存可能已经写入另一个线程,从而有效地将读取时间向后移动。
此外:挥发性不一定有帮助。对于那些相信他们可以正确预测程序的行为的人,该程序在强内存模型上只能对静态变量进行易失性访问,我鼓励你阅读 http://blog.coverity.com/2014/03/26/reordering-optimizations/,看看你是否可以正确地推断出允许的读写序列。请记住,这是在一个强大的记忆模型上;现在想想弱记忆模型的事情可能会变得多么困难!
当你放弃跨线程共享内存的标准模式和做法时,你就处于深水区。不要去那里。
这是关于框架类的线程安全的样板 MSDN 注释:
线程安全
此类型的任何公共静态(在 Visual Basic 中共享)成员都是线程安全的。不保证任何实例成员都是线程安全的。
一般来说,这是一堆马毛,不应该假设是准确的,它是复制/粘贴文档。 但是,您可以获得静态变量的一个保证,并且包含此简介的唯一原因是.NET内存模型确实承诺对此类变量的赋值可以对其他线程可见。
这意味着抖动无法优化从此类变量读取或写入。 基础负载或存储指令不能延迟,也不能重新排序。 这不是免费的。
局部变量没有这样的问题,抖动有一个硬保证,这个变量永远不会被另一个线程看到。 这允许抖动寄存变量,并将其存储在CPU寄存器中。 处理器上可用的最快内存类型。
请记住,心智模型不正确,"堆栈上将放置两个额外的变量"是不准确的。 实际上,当您在没有附加调试器的情况下运行程序的发布版本时,这两个变量都将存储在 CPU 寄存器中。 您获得的性能改进在这样的测试中是可以衡量的。
静态变量的存储通常从加载程序堆(AppDomain 实现详细信息)分配。 这不会影响程序的性能,抖动知道变量的确切地址,因为它是分配存储的地址。
除非您的static
字段声明为 volatile
,否则 .NET JIT 可以自由地执行与您所做的相同优化,并且每个主要版本都在这方面变得更好,这可以解释与过时的"Dot Net Perls"文章的区别。
我对静态版本 10% 差异的最佳猜测是 static int[] TotalArray = new int[100]
初始值设定项,它可以并且很可能确实会对MyStaticVariableHolder static
成员访问施加一些开销,以确保它只运行一次,并且在实际需要之前只运行一次。