请注意,此问题仅与性能有关。让我们跳过设计准则、哲学、兼容性、可移植性以及任何与纯性能无关的内容。非常感谢。
现在开始提问。我一直认为,因为C#getters/ssetters实际上是伪装的方法,所以读取公共字段的速度一定比调用getter更快。
所以为了确保我做了一个测试(下面的代码)。但是,如果您从Visual Studio内部运行该测试,则该测试只会产生预期结果(即字段比getter快34%)。
一旦你从命令行运行它,它显示的时间几乎相同。。。
唯一的解释可能是CLR进行了额外的优化(如果我错了,请纠正我)。
我不相信在实际应用中,如果这些特性以更复杂的方式使用,它们会以同样的方式进行优化。
请帮我证明或反驳这样一种观点,即在现实生活中,属性比字段慢。
问题是-我应该如何修改测试类以使CLR改变行为,从而使公共字段的性能超过getter。或者告诉我,任何没有内部逻辑的属性都将执行与字段相同的操作(至少在getter上)
编辑:我只是在谈论x64版本
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace PropertyVsField
{
class Program
{
static int LEN = 20000000;
static void Main(string[] args)
{
List<A> a = new List<A>(LEN);
List<B> b = new List<B>(LEN);
Random r = new Random(DateTime.Now.Millisecond);
for (int i = 0; i < LEN; i++)
{
double p = r.NextDouble();
a.Add(new A() { P = p });
b.Add(new B() { P = p });
}
Stopwatch sw = new Stopwatch();
double d = 0.0;
sw.Restart();
for (int i = 0; i < LEN; i++)
{
d += a[i].P;
}
sw.Stop();
Console.WriteLine("auto getter. {0}. {1}.", sw.ElapsedTicks, d);
sw.Restart();
for (int i = 0; i < LEN; i++)
{
d += b[i].P;
}
sw.Stop();
Console.WriteLine(" field. {0}. {1}.", sw.ElapsedTicks, d);
Console.ReadLine();
}
}
class A
{
public double P { get; set; }
}
class B
{
public double P;
}
}
正如其他人已经提到的,getter是内联的。
如果你想避免内联,你必须使用
-
将自动属性替换为手动属性:
class A { private double p; public double P { get { return p; } set { p = value; } } }
-
并告诉编译器不要内联getter(如果你喜欢的话,也可以同时内联两者):
[MethodImpl(MethodImplOptions.NoInlining)] get { return p; }
请注意,第一个更改不会对性能产生影响,而第二个更改显示了明显的方法调用开销:
手动属性:
auto getter. 519005. 10000971,0237547.
field. 514235. 20001942,0475098.
getter没有内联:
auto getter. 785997. 10000476,0385552.
field. 531552. 20000952,077111.
看看属性与字段——为什么重要?(Jonathan Aneja)MSDN上VB团队成员的博客文章。他概述了性质与字段的争论,并解释了琐碎的性质如下:
我听说过一个关于使用字段而不是属性的论点是"字段更快",但对于琐碎的属性true,因为CLR的实时(JIT)编译器将内联属性访问并生成与访问字段。
JIT将内联其内部度量确定的内联速度更快的任何方法(而不仅仅是getter)。假设一个标准属性是return _Property;
,那么它在任何情况下都将被内联。
您看到不同行为的原因是,在附加了调试器的调试模式下,JIT会受到严重的阻碍,以确保任何堆栈位置都与您期望的代码相匹配。
你也忘记了表现的第一条规则,测试胜过思考。例如,尽管快速排序在渐近上比插入排序快,但对于极小的输入,插入排序实际上更快。
唯一的解释可能是CLR进行了额外的优化(如果我错了,请更正我)。
是的,它被称为内联。它是在编译器中完成的(机器代码级别,即JIT)。由于getter/setter是琐碎的(即非常简单的代码),因此方法调用被销毁,getter/sette被写在周围的代码中。
这在调试模式下不会为了支持调试而发生(即在getter或setter中设置断点的能力)。
在visualstudio中,没有办法在调试器中做到这一点。编译发行版,在没有附加调试器的情况下运行,您将获得完整的优化。
我不相信在实际应用中,如果这些特性以更复杂的方式使用,它们会以同样的方式进行优化。
这个世界充满了错误的幻想。它们将被优化,因为它们仍然是琐碎的(即简单的代码,所以它们是内联的)。
在阅读了您的所有文章后,我决定使用以下代码进行基准测试:
[TestMethod]
public void TestFieldVsProperty()
{
const int COUNT = 0x7fffffff;
A a1 = new A();
A a2 = new A();
B b1 = new B();
B b2 = new B();
C c1 = new C();
C c2 = new C();
D d1 = new D();
D d2 = new D();
Stopwatch sw = new Stopwatch();
long t1, t2, t3, t4;
sw.Restart();
for (int i = COUNT - 1; i >= 0; i--)
{
a1.P = a2.P;
}
sw.Stop();
t1 = sw.ElapsedTicks;
sw.Restart();
for (int i = COUNT - 1; i >= 0; i--)
{
b1.P = b2.P;
}
sw.Stop();
t2 = sw.ElapsedTicks;
sw.Restart();
for (int i = COUNT - 1; i >= 0; i--)
{
c1.P = c2.P;
}
sw.Stop();
t3 = sw.ElapsedTicks;
sw.Restart();
for (int i = COUNT - 1; i >= 0; i--)
{
d1.P = d2.P;
}
sw.Stop();
t4 = sw.ElapsedTicks;
long max = Math.Max(Math.Max(t1, t2), Math.Max(t3, t4));
Console.WriteLine($"auto: {t1}, {max * 100d / t1:00.00}%.");
Console.WriteLine($"field: {t2}, {max * 100d / t2:00.00}%.");
Console.WriteLine($"manual: {t3}, {max * 100d / t3:00.00}%.");
Console.WriteLine($"no inlining: {t4}, {max * 100d / t4:00.00}%.");
}
class A
{
public double P { get; set; }
}
class B
{
public double P;
}
class C
{
private double p;
public double P
{
get => p;
set => p = value;
}
}
class D
{
public double P
{
[MethodImpl(MethodImplOptions.NoInlining)]
get;
[MethodImpl(MethodImplOptions.NoInlining)]
set;
}
}
当在调试模式下测试时,我得到了这样的结果:
auto: 35142496, 100.78%.
field: 10451823, 338.87%.
manual: 35183121, 100.67%.
no inlining: 35417844, 100.00%.
但当切换到释放模式时,结果与以前不同。
auto: 2161291, 873.91%.
field: 2886444, 654.36%.
manual: 2252287, 838.60%.
no inlining: 18887768, 100.00%.
汽车房地产似乎是一种更好的方式。
需要注意的是,在Visual Studio中可以看到"真实"的性能。
- 在启用优化的发布模式下编译
- 转到"调试"->"选项和设置",取消选中"在模块加载时抑制JIT优化(仅限托管)"
- (可选)取消选中"仅启用我的代码",否则您可能无法介入代码
现在,即使附加了调试器,jitted程序集也将是相同的,如果愿意的话,您可以介入优化的disassembly。这对于理解CLR如何优化代码至关重要。