使用字段提高性能






2.55/5 (9投票s)
通过使用内部字段而不是广泛使用公共属性来设计应用程序,可以显著提高性能和减小代码大小。
引言
通过使用内部字段而不是广泛使用公共属性来设计应用程序,可以显著提高性能和减小代码大小。公共属性常常被盲目地认为是“良好的编程实践”,而没有考虑该实践是否适用。
.NET 环境中的优化消除了属性带来的许多(但不是全部)性能损失。我们在结论中提出了一些哲学性的论证。
支持公共属性的论点
当某个值需要暴露给其他开发者的程序集时,将其定义为属性是有意义的。例如,在我们的程序集版本 1 中,我们定义了以下属性和字段:
public int MyProperty { get; set; }
public int MyField;
随着时间的推移,现在其他人正在使用我们的程序集并依赖它。后来我们发现一个必须解决的问题,例如值必须始终为正数。我们可以按如下方式修改我们的属性,使其能够容忍我们程序集的消费者传递负值。
public int MyProperty
{
get { return this.myProperty; }
set { this.myProperty = value >= 0 ? value : 0; }
}
private int myProperty;
我们的程序集的用户无需重新编译即可使用我们的更新。然而,我们无法对我们的字段做同样的处理。要为我们的字段添加值检查代码,我们必须先将其转换为属性,如果这样做,我们将破坏与先前版本的兼容性。这将使公共字段不是“良好的编程实践”。
属性和字段的机制
使用属性而不是字段是有成本的。字段本质上是内存中的一个位置,我们可以使用一条 CPU 指令(在 X86 的情况下是“mov”)来获取和设置值。然而,属性是一种自动生成代码的方式。以下两种方法是生成的代码:
public int get_MyProperty()
{
return this.myProperty;
}
public void set_MyProperty(int value)
{
this.myProperty = value >= 0 ? value : 0;
}
private int myProperty;
我们不再使用一条 CPU 指令来访问我们的值,而是通过调用方法来获取或设置值。CPU 现在必须执行一个对方法的调用指令,而方法必须执行“mov”指令,然后执行返回指令。除了额外的指令外,还需要将指令指针推入和弹出堆栈。与字段相比,属性的工作量至少是其五倍。CPU 硬件有许多技术用于提高效率,例如并行执行推入和弹出操作,但这超出了我们的范围。
内部字段
我们看到,对于公共值使用属性是有意义的,并且我们需要承担其成本。然而,这种负担不必施加在我们程序集内部使用的值上。如果某个值,至少在当前版本中,仅在我们程序集内部使用,我们就不应该公开它,而应该将其保持为内部状态,如下所示:
internal int MyValue;
通过使用字段消除了属性的开销。如果现在我们正在为我们的程序集编写版本 2,并且发现必须添加我们的验证逻辑,我们可以简单地将我们的字段更改为属性并重新编译。通常,我们程序集的其他部分不受影响,也不会出现兼容性问题。
internal int MyValue
{
get { return this. myValue; }
set { this. myValue = value >= 0 ? value : 0; }
}
private int myValue;
将公共属性作为内部字段访问
通过在内部公开底层值,我们可以通过公共属性获得一些内部效率。这允许我们的程序集使用该值作为字段,同时允许在未来版本中进行更新。
public int MyProperty
{
get { return this.myProperty; }
set { this.myProperty = value; }
}
internal int myProperty;
如果我们发现必须添加验证逻辑,我们可以搜索对我们字段的所有内部引用,并根据需要将其更改为使用公共属性。由于我们所做的更改仅影响我们的程序集,因此不会影响其他用户。
当我们拥有解决方案时使用公共字段
当我们的解决方案中使用多个程序集,但这些程序集不在我们的解决方案外部共享时,我们可以像描述内部字段一样使用公共字段。
即使我们有一个大型团队致力于单个解决方案,我们也可以将值公开为公共字段,就像上面的“MyValue
”示例一样。任何人都可以随时将字段更改为属性,而不会中断团队其他成员的工作。通过采用这一策略,我们可以实现效率和可更新性的平衡。
优化消除了大部分开销
Microsoft .NET Framework 中的编译器提供了显著的优化。虽然“debug”版本如上所述执行,但“release”代码会删除对 getter 和 setter 的调用,并直接访问后备字段。
这种优化效果显著,但并非完全。增加一个字段是一条指令,而增加一个属性仍然是三条指令。此优化也可能存在于未来的编译器版本中。
libraryClass.MyField++;
inc dword ptr [esi+8]
libraryClass.MyProperty++;
mov eax,dword ptr [esi+4]
inc eax
mov dword ptr [esi+4],eax
注意:要在 Visual Studio 2008 中查看优化代码的反汇编,您必须更改一些默认选项。单击“工具”,然后单击“选项”。
在“调试常规”中,取消选中“仅我的代码(仅托管)”和“加载时禁止 JIT 优化(仅托管)”。
然而,属性的 getter 和 setter 定义仍然存在,JIT 编译器必须在每次应用程序加载时执行优化。字段本质上是在 JIT 编译器启动之前就进行了优化,并且只需要对程序集清单进行一次条目,从而减小了文件大小。
哲学论证
一些人会争辩说,始终使用属性,让编译器做决定是最简单的。我们建议,当您编写代码时,您应该编写您打算执行的内容。属性有时像字段一样执行,而字段总是像字段一样执行。
如果您为多个环境编写代码,那么您不能假定优化会延续下去。属性和字段的概念本质上是普遍的,即使对于 8 位嵌入式处理器也是如此,但优化则不然。
在许多情况下,例如许多数据绑定场景,属性是必需的,而字段则不是选项。
很多时候,尤其是在内部应用程序中,debug 版本是投入生产使用的版本。在这种情况下,优化优势就消失了。
在应用程序开发期间的编译时间、应用程序启动时间和文件大小都受到属性与字段选择的影响。对于一个实例,影响微乎其微。对于复杂的应用程序,影响是真实的。
结论
当我们在开发供外部解决方案使用的程序集时,公共属性的巨大价值就体现出来了。当我们拥有解决方案并盲目使用属性时,我们会增加开发工作的巨大、不必要的开销,并损害应用程序的性能。
对于独立程序员或大型团队来说,明智地使用字段和属性的精炼策略可以实现性能提升和代码大小减小。