热线电话
  • 010-88558925010-88558943
  • 010-88558955010-88558948
CMIC专家更多

中国工程院院士沈昌祥

党的二十大报告提出,到2035年基本实现...更多>>

赛迪院长张立:中国新

全球范围内,中国步入培育经济增长新动能的...更多>>

中国市场情报中心 > 文章 > 产业投资
VB.NET应用程序性能的优化

发布时间:2003-06-27 14:09:37

来源:赛迪网

作者:Gordon Brown,陶刚翻译

【打印】 【进入博客】 【推荐给朋友】
摘要本文介绍怎样优化Visual Basic .NET应用程序的性能

介绍

Visual Basic .NET的一个主要目标是比以前版本执行得更快。但是性能依赖于应用程序如何设计。本文讲解了能帮助你优化应用程序性能的几种重要的考虑事项。

中间语言

Visual Basic .NET和C#都编译为微软中间语言。两种语言中等效的源代码通常编译成相同的中间语句代码,应用程序执行的性能也相同。性能不是在两种语言中间作出选择的标准。

执行频率

本文中的一些建议可能是稍微改变某些语句,但是在循环或者频繁调用的过程中这将极大地提高性能。此外,被多次执行的代码块也是优化的候选部分。

瓶颈

效率最高的优化是识别应用程序中的瓶颈或者速度慢的地方,并提高它们的速度。通常的瓶颈是长时间的循环和数据库的访问。稍微优化每个表达式和过程调用不会花费太多的开发工作。

依赖应用程序

性能非常依赖于独立应用程序的特性。本文的建议仅仅是指导,不作保证。你能对这些建议作一些调整以适应特定的应用程序。

平台

Visual Basic .NET为推荐的系统硬件配置优化过了,包括集成的开发环境(IDE)和运行时(runtime)。如果没有推荐容量的内存,性能可能不好,在运行大的或者多个应用程序时就会出现这种情况。

测试条件

本文中的一些专门的测试运行在600 MHz Pentium III处理器、256 MB内存、运行Windows 2000 Professiona和Visual Studio .NET 2002的计算机上。这种特殊的测试有很多紧密相连的循环组成,只是用于试验用于比较的代码元素。换句话说,那些循环中除了代码元素外没有其它的信息。因此表现出来的时间差别是极端的,你不要希望正常的应用程序中有如此大的差别。

预备信息

本文是基于一些预备信息的。本文中的建议随着经验的改变不断更新和重新定义。同时,在未来的版本中一些考虑因素也会改变。

数据类型(Data Types)

应用程序的变量、属性、过程参数、过程返回值都会影响应用程序的性能。

值类型和引用类型

变量类型在分配给它们的内存中保持数据。因为每个变量类型的实例是独立的,并且不能被一个以上变量访问,所以变量类型可以在堆栈(stack)中保持和管理。引用类型只保持指向存储数据的内存的指针。因为有一个以上变量能执行该数据,因此引用类型必须在堆(heap)中保持和管理。

堆的管理比执行堆栈分配困难。它的总开销包括堆分配、对象访问、无用信息收集(garbage collection,GC)。这意味着当不需要引用类型的灵活性时,较小的值类型(value type)是比引用类型好的选择。但是当值类型变大时就没有这个好处了。例如,分配一个5个字节的值类型就比分配一个引用类型花费的时间长。

采用较小的值类型将把性能提高30%左右。注意它也依赖其它的因素,例如硬件平台、系统载入和数据大小等。

Object类型和迟绑定

作为Object声明的引用类型变量可以指向任何类型的数据。但是这种灵活性也会降低性能,因为Object变量通常是迟绑定的。

如果引用类型变量声明为特定类(例如Form),它就是早绑定的,这样就允许Visual Basic编译器在编译时就作一定的优化,例如类型检查和成员查看表。当在运行时访问早绑定对象变量的成员时,编译器已经完成了大量的管理工作了。

如果变量声明为Object类型或者没有明确的数据类型它就迟绑定。当代码访问类似变量的成员时,通用语言运行时被迫在运行时执行类型检查和成员查看表。注意,如果Option Explicit设置为Off,没有明确数据类型的变量声明就是Object类型。

早绑定比迟绑定的对象的性能好得多,同时它使更容易阅读和维护,并减少了运行时错误的数量。这意味着在设计时你就应该使用具体类的类型声明对象变量。在不必要的时候要尽量避免Object类型的使用。指向值类型数据的Object变量消耗更多的内存,包括指针的消耗和数据副本(copy)的消耗。

数据类型的宽度

最高效的数据类型使用运行时平台的数据宽度。在当前的平台上,对于计算机和操作系统来说数据宽度是32位的。因此Visual Basic .NET中目前效率最高的数据类型是整数(Integer),接着是长整型(Long)、短整型(Short)和字节型(Byte)。你可以通过关闭整型溢出检查(例如通过设置RemoveIntegerChecks属性)来提高短整型和字节型的性能,但是这会导致由于没有检查到溢出而出现的不正确运算的风险。在运行时你不能重复打开和关闭这种检查,只能为应用程序的下次建立设置这个值。

如果需要小数值,最好的选择是双精度型(Double),因为当前平台的浮点处理器使用双精度执行所有操作。按效率排序接着的是单精度(Single)和十进制(Decimal)。

表示单位

在应用程序的很多情况中能使用整型数据类型(Integer、 Long、 Short、Byte)代替小数类型。当能够选择单位时,这种情况经常发生。例如,如果你要表现图像的大小,你能使用象素代替英寸或厘米。因为在图像中象素的数量是整数,可以把它存储在整型变量中。如果选择英寸就不得不处理小数值,因此必须使用双精度变量,效率较低。表示的最小单位通常是整型、更大的单位通常是小数的。如果你要执行很多单位的操作,整型数据类型将提高性能。

装箱(Boxing)和取出(Unboxing)

当你把值类型处理为引用类型时,通用语言运行时必须处理装箱(Boxing)。装箱是必要的,例如你声明了一个整型变量并且把它指定给一个Object变量或者把它作为Object参数传递给某个过程。在这种情况下,通用语言运行时必须把变量装箱并转换成Object类型,它复制该变量,把副本嵌入新的分配的对象中,并且存储它的类型信息。

如果随后你把包装的变量指定给值类型的变量,通用语言运行时必须取出该变量,把数据从堆实例中的复制到值类型变量。而且包装的变量必须在堆中管理,无论它是否被取出过。

包装和取出明显降低了性能。如果应用程序频繁地把值类型变量作为对象处理,最好最初使用引用类型声明。另一种选择是只包装变量一次,在使用过程中一直保留它,再次需要该变量时取出它。

你可以通过设置Option Strict On来消除无意中做的包装。这可以帮助你找到无意中包装了某个值类型,它强制你使用明确的转换,这通常比包装效率高。注意,你不可能使用显式转换绕过包装。CObj(<变量类型>) 和CType(<变量类型>, Object)都包装了值类型。

数组

避免使用行列比需要的要大的数组。数组中定义的维数越小,它的执行效率越高。在一维和二维数组之间这种区别是明显的,因为通用语言运行时会优化一维数组。

目前数组的数组的效率比多维数组效率高。换句话说,A(9)(9)的执行效率比A(9,9)高。这是因为数组的数组可以从一维数组的优化中得到好处。它们的效率差别可能超过30%。

数组列表

System.Collections名字空间的ArrayList类支持动态数组,它能够根据需要改变大小。为了使用它,必须使用ArrayList数据类型而不是标准的数组来声明变量。接着你能够调用Add、AddRange、Insert、InsertRange、 Remove和RemoveRange方法来添加和删除元素。

如果你频繁地改变数组的大小并且需要保留已存在元素的值,ArrayList对提供的性能就比ReDim语句和Preserve关键字的性能要高。ArrayList的缺点是它的所有元素都是Object类型的,因此后绑定。ReDim的优点能否补偿后绑定的缺点依赖于不同的应用程序。你可以试一下两种方法并比较它们的性能。

声明(Declarations)

前面谈到先绑定比后绑定的速度快,而且拥有更好的错误检查。你应该使用最具体、适合类的类型来声明对象变量。

假设你指定所有的对象变量为Control类型,但是大多数而不是所有的都是Form类型。尽管Form比Control的类型更加具体,但是你不能用System.Windows.Forms.Form类型声明所有变量,因为你有可能需要一些Button或者Label类型的对象。你必须把那些变量声明为System.Windows.Forms.Control类型,因为它是能接受每个指定的对象的最具体的类型。

属性、变量和常数

变量执行速度比属性快。变量访问简单地从内存中存取,属性访问需要调用该属性上的Get或Set方法,而它们除了存取值外还要作其它的操作。注意编译器把WithEvents变量作为属性实现,因此它没有获得使用变量的好处。

常量执行得比变量快,因为变量会被编译成代码。常量的访问甚至不必从内存中取(除了Date或Decimal常量外)。

选项设置

Option Explicit On强制你定义所有的变量,这使代码更容易阅读和维护。必须确定在每个声明,包括过程参数中使用As子句。如果你没有用As指定,变量和参数都使用Object数据类型,而它通常不是最佳的类型。使用As会提高性能,因为它把类型推理从运行时移到了编译时。

Option Explicit On不接受隐式的收缩,在每个声明中都必须有As语句,并且不管Option Explicit的设置如何都不允许迟绑定。你仍然能执行类型转换收缩,但是必须使用类似CInt或CType的显式转换关键字,因为它能保护代码不受迟的绑定的影响,从而提高了性能。

Option Compare Binary指定字符串基于字符的二进制表示进行比较和存储,而不考虑类似大小写对的相当的字符。如果应用程序逻辑允许,你能使用二进制比较。因为代码不需要处理大小写不敏感性,或者按某种文化中的字符排序的问题,它提高了性能。

对象集合和对象数组

当有一系列处理方法相似的相关联的对象时,可以把它们放入对象数组中,或者把这些对象作为某个集合的成员。下面的考虑事项可以帮助你在两者之间作出选择:

●通用语言运行时能为数组优化代码,但是如果每个都访问集合的话就需要一个或者更多的调用。因此,当数组支持所有必要的操作时,通常更好。

●对于索引的访问,数组从来不慢,并且通常比集合快。

●对于键控制的访问,应该使用集合。数组不支持使用键字段的访问,因此你必须编写代码搜索整个数组元素查找键。

●对于插入和删除,集合通常更好。数组没有直接支持添加和删除元素。如果你在数组的末尾插入或删除,必须使用ReDim语句,而它会降低性能。为了随处插入或删除,必须使用ArrayList对象代替标准的数组。与此对比,在集合中插入和删除都是直接的操作,并且不管元素涉及的位置如何它们的速度都相同。

磁盘文件I/O

Visual Basic .NET提供了三种访问磁盘文件的主要途径:

●传统的运行时函数,例如FileOpen或WriteLine。

●微软脚本语言运行时的文件系统对象(FSO)模型。

●System.IO名字空间中的.NET框架组件对象模型。

传统的运行时文件函数为早期版本的Visual Basic提供兼容性。FSO提供与脚本语言的兼容性,为应用程序提供它所需要的功能。每个模型都是作为调用System.IO名字空间中类的成员的一系列包装对象实现的。因此,它们的效率没有直接使用System.IO的效率高。此外,FSO要求应用程序执行COM交互操作,花费更大的资源。

你能通过使用System.IO类来提高性能:

●使用Path、Directory和File处理驱动器和文件夹层次。

●使用FileStream处理通常的读写。

●使用BinaryReader和BinaryWriter处理二进制或未知数据。

●使用StreamReader和StreamWriter处理文本文件。

●使用BufferedStream为输入输出流提供缓冲。

通常FileStream提供的磁盘执行性能最高。它使用自己的缓冲和自己的文件操作,不包装任何其它任何输入输出过程。但是,如果磁盘输入输出不是应用程序的瓶颈,其它的类可能更方便。例如,如果要处理文本文件并且磁盘性能并不重要时,你可能更喜欢StreamReader或者StreamWriter。

缓冲区

把缓冲区设置为合适的大小,特别是使用BufferedStream或者FileStream时。尽管可以根据应用程序修改,但是通常情况下理想的大小是4KB的倍数。比4KB小的缓冲可能可能因为大量数据而产生太多的输入/输出操作,从而降低了性能。缓冲太大可能为了提高性能消耗了太大的内存。根据硬件,适合的上限可以从8KB到64KB。

异步I/O

如果应用程序与磁盘操作绑定紧密,可以在等待I/O传递完成时,利用磁盘等待时间执行其它事务。为了达到这个目标,可以使用FileStream类提供的异步I/O。这种途径可能需要更多的编码,但是由于在读写操作过程中执行了一些代码从而提高了性能。如果传递大量的数据,或者磁盘等待时间很长,这种优化很有效。

操作符(Operations)

整型运算比浮点或小数快得多。在不需要小数或分数的计算中,把所有的变量和常量声明为整型,最好是Integer型。要记住把整型与浮点型之间的转换会降低性能。

操作符

在执行整数除法时,如果只需要商而不需要余数,使用整数除号(\操作符)。\ 操作符比 / 操作符快10倍以上。

赋值操作符

类似+=的赋值操作符比组合的操作符(分开的+和=)简洁一些,并使代码更容易阅读。如果你在操作一个表达式而不是简单的变量(例如数组元素),使用赋值操作符性能可以明显提高。这是因为表达式(例如MyArray(SubscriptFunction(Arg1, Arg2)))只计算一次。

连接

连接字符串时应该使用连接操作符(&)代替加操作符(+)。只有操作数都是String类型时它们才相等。在不是这种情况时,+ 操作符会后绑定并且必须执行类型检查和转换。& 操作符是专门为字符串设计的。

Boolean测试

要测试If语句的Boolean时,直接在测试中读取变量比使用= 操作符与True比较简单。尽管性能没有不同,下面的例子更好:

If BoolVariable = True Then   '不必要指定True
' ...
End If
If BoolVariable Then   ' 更简洁
' ...
End If

短回路

在可能的情况下,使用短回路的Boolean操作符、AndAlso和OrElse。它们依赖其它表达式的结果略过了表达式的计算,从而节约了时间。在使用AndAlso时,如果表达式左边的结果时False,最后的结果就决定了,并且忽略右边表达式的计算。同样使用OrElse时,如果左边表达式的结果是True将忽略右边表达式的计算。注意如果它在找到列表尾部前已经找到了匹配的值,Case语句也能短回路多个表达式和范围列表。

成员访问

有些成员访问操作符(.操作符)调用的方法或属性返回一个对象。例如,在System.Windows.Forms.Form类中,Controls属性返回ControlCollection对象。类似的访问操作符要求对象建立、堆分配和管理和无用单元收集(GC)。如果在循环中有这类成员访问操作符,每次要建立一个新对象,将极大降低性能。如果你的目标是在每个循环中处理相同的对象,这也许会导致一个逻辑错误,因为该类型的每次访问建立了不同的对象。

避免重新验证

如果你对条件元素的成员做了很多引用,例如MyForm.Controls.Item(Subscript),使用With ... End With结构将提高性能:

With MyForm.Controls.Item(Subscript)   ' Evaluate the qualification once.
   .Name = "Control number " & CStr(Subscript)
   .Text = .Name
   ' Access other members of MyForm.Controls.Item(Subscript)
   .Refresh()
End With

上面的代码只计算MyForm.Controls.Item(Subscript)一次。它运行的速度比验证每个成员访问至少快2倍。但是,如果该元素没有被验证,例如AnswerForm或者 Me,使用With ... End With就没有性能的提高。

类型转换

当一个类衍生自另一个类时它们就有了继承关系。如果有两种类型的对象并且需要把一种类型转换为另一种,可以使用DirectCast关键字代替CType关键字。DirectCast的性能好一点,因为它不使用运行时帮助函数。注意如果两种类型之间没有继承关系,DirectCast产生InvalidCastException错误。

属性缓冲

如果你重复(例如在执行很多次的循环之中)访问某个属性, 能使用缓冲属性值来提高性能。为了达到这个目标,在进入循环前把属性值指定给某个变量,接着在循环中使用该变量。如果可能,当循环结束后可以把该变量的值指定给属性。

如果访问重复使用的属性是代码中重要的一部分,缓冲大约可以把性能提供三倍。

但是,如果Get或者Set方法处理存取值外还做了其它的处理,也许你不能缓冲某个属性。

异常处理

传统的Visual Basic使用On Error GoTo和On Error Resume Next语句处理错误。这种处理不容易设计,并且结果源代码令人费解,难以阅读和维护。

Visual Basic .NET提供了使用Try ... Catch ... Finally语句的结构化异常处理。它是基于一个灵活并容易阅读的控制结构的。结构化异常处理能检查给定代码块的几个不同错误并分别处理。

两种处理都影响性能。使用On Error Resume Next语句迫使编译器为On Error Resume Next语句后面的代码块生成额外的中间语言。Catch块在入口和出口处改变了Err对象的状态。初步的测试显示当代码块的语句少于20条时两种方法的性能相当。但是,如果有数百条语句使用Try和Catch执行得更快,因为On Error Resume Next为同样得代码生成更多得中间语言。

如果没有必须使用On Error语句的原因,最好使用结构化异常处理。

抛出异常

尽管结构化异常处理很有用,但是你应该只把它用于异常。异常不一定是错误,它有可能是突然发生或者正常操作中没有预料到的事情。抛出异常比检测和转移(例如使用Select结构或者While循环)花费更多的处理时间。异常用于通常的流控制时,使代码更难阅读。你不应该使用它们来转移或者返回值。

捕捉异常

Try ... Catch ... Finally结构花费的性能很少,除非抛出了异常。换句话说,建立异常处理程序不会降低性能,因此当你希望很少发生时,不需要犹豫是否使用结构化异常处理。

字符串

String类的实例是不变的。因此,每次改变String变量时,丢弃了已分配的String对象并建立新的。如果你多次操作相同的String变量将花费很多内存和性能开销。通常的操作是连接,但是String的方法,例如Insert和PadRight也建立新的实例。

字符串构造程序

System.Text名字空间的StringBuilder类支持可变的字符串,它在修改后保留相同的实例。使用它时,需要使用StringBuilder数据类型代替String类型。你可以调用Append、 Insert、 Remove 、Replace方法来维护该字符串。

如果有大量的连接和转换,使用StringBuilder类,它的执行速度是String数据类型的三倍。如果希望,在维护结束时可以使用ToString方法把最后的字符串数据复制到String对象。但是如果你不希望非常频繁地维护相同的实例,String是比较好的选择。这是因为StringBuilder的开销是String的一倍。在建立时,StringBuilder构造函数比String的构造函数花费的时间长。结论是在多数情况下必须使用ToString。

连接

有时你需要把所有的字符串组合到一个语句中,例如:

MyString = PrefixString & ": " & MyString & " -- " & SuffixString

类似上面例子中的语句只建立新字符串一次,因为它使用了 & 操作符,所有不会迟绑定。

当你把字符串组合到一个语句中时,Visual Basic在编译时组合它们。它在中间语言中生成了最后的、结果字符串,也提高了执行时的性能。注意如果ChrW函数的参数是常量,它的结果可以作为常量使用。

字符串函数

Format函数执行了大量的检查、类型转换和其它处理,包括按当前文化(culture)格式化。如果你不需要这些特殊的功能,使用适当的ToString方法代替它。ToString方法比CStr转换关键字要快,因为CStr在调用ToString前要做一些分析。

Asc 和 Chr函数适用于单字节字符集(SBCS)和双字节字符集(DBCS)。它们必须考虑当前线程的代码页,接着把字符转换为Unicode(或者相反的操作)。AscW和ChrW函数的效率更高,因为它们只在Unicode中工作,并且不依赖当前线程的文化和代码页设置。

过程(Procedure)

在循环中调用某个过程和把过程放在循环外两者之间有一个平衡。如果把过程包含在循环中,就避免了调用机制的开销,但是应用程序的其它位置不能访问该节点了,如果在别的地方复制了该段代码,将使维护更加困难,容易出现更新同步错误的问题。在循环外定义过程使循环的代码更容易阅读,同时使应用程序的其它位置可以调用该过程。如果过程很大的话调用的开销并不重要,但是如果该过程只执行一个小的事务(例如访问某个类对象的成员),调用的开销就很重要了,在这种情况下把对成员的访问直接放在循环内部获得的性能更高。

过程大小

过程是JIT编译的主题。过程只在第一次调用时才被编译。在编译时JIT编译器试图对过程执行多种优化,例如为小的过程调用生成内联代码,很大的过程却不能从优化中受益。包含超过1000行代码的过程从JIT优化中得到的好处很小。

调用和返回

在Visual Basic早期版本中,在自己的模块中调用过程比从其它模块中调用过程的速度快,在自己项目中调用过程比从其它项目中调用过程的速度快。在Visual Basic .NET,它们的速度是没有区别的,因此过程的位置与调用代码关联并不是性能的判断标准。

在逻辑允许的时候使用Return语句。编译器能对该代码优化比使用Exit Function、Exit Property或Exit Sub、或允许End Function、End Get、End Set或 End Sub产生返回的优化要好一些。

虚调用

虚调用(virtual call)调用可重载的过程。注意它依赖于过程是否使用Overridable关键字声明了,而不是是否定义了重载。进行虚调用时,通用语言运行时必须检查对象的运行时类型,决定调用哪个重载,与此对照的是,非虚调用可以从编译时类型中获得所有必要的信息。

从性能的角度看,虚调用花费的时间是非虚调用的两倍左右。对于值类型(例如调用结构上的某个过程)它们的区别是明显的(结构不能定义Overridable成员,但是它从Object继承了Equals、GetHashCode和ToString,能够执行Overridable成员接口)。你应该只在有明显好处的时候定义Overridable过程,应该把调用尽可能限制到NotOverridable过程。

过程参数

当你使用ByRef关键字给过程传递一个参数时,Visual Basic复制下层变量的指针,无论该变量是值类型还是引用类型。当使用ByVal传递参数时,将复制下层变量的内容。对于引用类型,内容只包括对象本身的指针;对于值类型,包含所有的变量数据。

ByRef和ByVal之间的性能差别是明显的,特别是对引用类型。对值类型,它们的差别依赖于数据类型的宽度。大多数值类型的宽度约为指针的大小,在这种情况下两者性能相等。但是大类型(例如长结构体)通过ByRef传递避免了复制所有数据,可能效率更高。另一方面,当你传递适当大小的值类型(Integer或Double),因为编译器优化了调用代码(例如在寄存器中保持了参数),所以ByVal效率更高。

因为通常情况下ByVal和ByRef之间性能的差别并不重要,因此在两种机制之间选择时最好考虑其它的条件。通过ByRef传递参数的好处是过程能够通过修改传递给它的参数变量的内容给调用代码返回一个值。通过ByVal传递参数的好处是它保护变量不受过程的修改。在没有强制原因要使用ByRef时,应该使用ByVal传递参数。

循环

最小化Try代码块中的循环数量,并最小化循环中Try代码块的数量。长循环可以放大结构化异常处理的开销。

你可能会考虑For、Do或While是否更有效率,初步的测试表明它们之间的区别不大,因此性能不是在这些循环类型中选择的考虑因素。

集合

当遍历某个集合时可以使用For或For Each循环。如果希望在遍历过程中增加、删除或者排序集合的元素,For循环可以产生更可靠的结果。它允许你确定遍历的次序,而且如果你在For Each循环中试图改变它的成员,从System.Collections名字空间的CollectionBase类衍生来的集合会产生异常。

For Each循环使你能够控制IEnumerable.GetEnumerator方法返回的枚举对象集合。这意味着你没有必要预计遍历的次序。当你使用Item属性不能访问集合的成员时For Each是很有用的。

For与 For Each循环之间的性能差别不明显。

添加成员

在你调用集合的Add方法时要避免使用Before和After参数。当你使用Before或After指定集合的某个位置时,就强迫集合在添加新成员前查找另一个成员。

如果你使用的集合有AddRange方法,优先使用它。AddRange在一个调用中添加整个的列表或集合。有几个集合类暴露了AddRange方法(例如System.Collections名字空间的ArrayList类和System.Windows.Forms名字空间的ComboBox.ObjectCollection类)。

线程

如果应用程序要化很长时间等待它的某个操作完成,考虑使用异步处理,使用System.Threading名字空间中Thread类的方法。当等待用于响应和读写持久的媒介时这是很有用的。异步处理和多线程需要额外的代码,但是它们能显著地提高性能。

但是要注意,多线程也有开销并且使用必须小心。只有很短使用期限的线程效率低下,并且上下文(context)转换明显花费了很多执行时间。你应该最小化长时间线程的数量,并尽量少进行切换。

多重缓冲

如果应用程序大量使用读写并且使用了异步输入输出,使用多重缓冲(multiple buffering)是很有益的。在多重缓冲中为正在读或写的文件分配两个以上缓冲区。在等待一个缓冲区中的输入输出完成时,可以处理其它缓冲区的数据。多重缓冲又成为摆动(swing)缓冲,当你使用两个缓冲区时通常叫做双倍缓冲。

过程间调用

你应该最小化过程间调用、远程调用和跨应用程序域调用,因为信号编集(marshaling)的开销很大。这在跨越COM协同操作边界(即在受控代码和不受控代码之间)调用时很明显。当你要做类似调用时,把调用组合为执行多个事务(例如初始化一个对象的所有字段)而不是执行一个短事务的调用。

Blittable数据类型

Blittable类型在受控和不受控内存中的表示是一样的,它可以在不用转换的情况下跨越受控/不受控边界进行复制。当应用程序进行COM协同操作调用时,试着使用Blittable数据类型的参数。Visual Basic .NET 中的Blittable类型是Byte、Short、Integer、Long、Single和Double。通用语言运行时的类型System.SByte、 System.UInt16、System.UInt32和System.UInt64也是blittable类型的。

有些混合类型也可能是blittable的。所有成员为blittable类型的结构体本身就是blittable类型的。即使类的所有成员都是blittable类型的,它也自动成为blittable类型,但是可以通过仅使用blittable成员来提高信号编集的性能。你也可以设置System.Reflection名字空间中TypeAttributes枚举的ExplicitLayout成员,这可以强迫类的成员按指定的偏移量编集,不会被通用语言运行时重新调整。

信号编集

信号编集是过程间调用的重要部分,减小信号编集能提高性能。Blittable数据类型和显式成员布局是最小化信号编集开销的最高效的方法。使用结构体代替类通常可以提高性能。

受控代码可以使用平台调用功能在动态链接库(DLL)中实现的不受控的函数(例如Win32 API中的函数)。为了使用平台调用,使用Function和Lib关键字和Declare语句声明外部引用。通过Declare语句调用比调用COM对象效率更高。

其它优化

编译和预编译

源代码被Visual Basic .NET编译器编译成微软中间语言(MSIL)。MSIL驻留在应用程序的.exe文件中,被通用语言运行时的JIT(just-in-time)编译器读取。JIT编译器通常在第一次调用某个过程时把该过程的MSIL编译成目标平台的本地代码。

在调用经常使用的过程前编译它们是很有用的。集成开发环境(IDE)给Visual Basic .NET标准库执行了预编译过程,并把本地代码放入全局组件缓存(GAC)的一个特殊的段中。这使得JIT编译对于Visual Basic .NET运行时函数不必要了,从而提高了性能。

在很多情况中,应用程序中的某个过程可能是预编译的好目标。例如,Windows Form应用程序一般使用很多共享库,在开始时调用很多过程,预编译这些过程能提高性能。

你可以在安装过程中使用ngen.exe预编译应用程序的一部分。注意ngen.exe不调用JIT编译器而是调用自己的编译器。在预编译代码前应该考虑下面的事项:

●只能预编译客户端应用程序。

●根本不能预编译ASP.NET应用程序。

●预编译只在应用程序开始时能提高性能。

●性能的提高不会太大。你可以预编译或者不进行预编译来比较时间差别。

●预编译可能轻微降低经常调用的过程的运行时性能。这是因为ngen.exe不能做一些JIT编译器能作的优化。

动态链接库

载入动态链接库(DLL)要花费大量的执行时间。载入DLL而只调用一两个过程是效率很低的。应该尽量少生成DLL,即使这使它们相对较大。这意味着应用程序应该使用尽可能少的项目和大的解决方案。

调试连编和零星连编

你能通过用编译成零星连编代替调试连编来提高性能。这使得编译器优化,使中间语言结果更小更快。但是,这些优化改变了代码次序,使调试更加困难。在开发的时候你也许希望编译成调试连编。

优化显示速度

在Windows Form应用程序中,有时希望重要的窗体和控件尽可能快的显示。可以考虑下面的方法:

●避免不必要的控件重画。一种有用的途径是在设置控件的属性时隐藏它。

●当需要重画控件或者任何显示对象时,尽量重画对象的新的暴露区域。这会减少用户的等待时间。

●在Visual Basic .NET中没有与早期版本中的Image相等的控件,你必须使用PictureBox控件显示大多数图形,而且也不支持AutoRedraw功能。

优化可感觉到的显示速度

为了让用户知道应用程序没有停止运行了,你可以试图优化可感觉到的显示速度。下面一些建议可能有帮助:

●使用进展指示器,例如System.Windows.Form名字空间中ProgressBar类的ProgressBar控件。这可以帮助用户确信应用程序仍然在运行。

●在只需要一两秒钟的短操作中,可以使用Masked Edit控件的MousePointer属性把鼠标指针变为沙漏形。

●在应用程序需要重要数据前预先载入数据。这包括窗体和控件以及其它的数据项。尽管载入这些数据需要相同的时间,但是它减少了用户需要看到它们时的等待时间。

●在你预先载入窗体或者控件时,把它们隐藏。这样减少了描绘的必要。

●当应用程序在等待用户输入时,在后台使用线程和计时器作一些小的事务。这样在用户请求数据时可以帮助准备数据。

●通常最大化应用程序的早期显示速度(第一次载入时的显示)。下面的建议可以考虑:

保持早期的窗体和控件尽可能简单以减少载入和初始化时间。

在每个窗体的load事件的第一行调用me.show。

避免立即载入不必要的模块。小心避免调用导致这类过早调用载入的过程。

●如果显示包括动画或者经常改变元素显示,在当前图像显示时使用双重或者多重缓冲来准备下一张图像。可能控件能使用System.Windows.Forms名字空间的ControlStyles枚举,并且DoubleBuffer成员能帮助防止闪烁。

减少内存消耗

保持可执行代码的数量最小很重要。你可能需要减少应用程序数据的内存需要,这种减少通常提高了性能,因为更小的执行文件运行更快,排除了内存交换提高了执行数度。下面的建议可以帮助你实现这些功能:

●最小化同时载入的窗体的数量。在需要窗体时才载入,除非希望优化可以感觉到的显示速度。当使用完窗体时,卸在它并把变量设置为Nothing。

●在窗体上使用尽可能少的控件。使用需要的最小、最简单的控件。例如使用标签代替文本框。

●把相关的过程保持在相同的模块中。这样最小化了载入的模块。

●避免使用比需要大的数据类型,特别是在数组中。

●当使用完大的数据类型变量时,把它设置为Nothing。这通常在字符串、数组和其它潜在的大对象中使用。

●如果已经使用完了数组中的一些元素但是需要保持另外的元素,使用ReDim释放不必要的内存。

结论

当你设计和编写Visual Basic .NET应用程序以优化性能时,要紧记下面内容:

●将优化的工作集中在运行在循环或频繁调用的过程的代码上。

●找到应用程序中最慢的位置并优化它们来优化性能。

●使用强类型变量,可能时给对象变量上安排早绑定。

●设置Option Strict On和Option Explicit On。

●最小化内存的使用。

●当不需要调试连编时编译成零星的部分。

●把应用程序设计为大的解决方案,项目尽可能少。

●测试性能,不要假设某种技术一定比其它的技术的性能好。

按规则编码,所有逻辑的设计适当,只是优化性能的第一步。如果你的应用程序没有设计好,那么调整可能几乎没有什么帮助。

【本文译自】《Performance Optimization in Visual Basic .NET》

【原作者】Gordon Brown

【参考原文】点击阅读

(责任编辑:郁单曰)

相关报道
  • --

联系我们:8610-8855 8955 zhouhl@staff.ccidnet.com

广告发布: 8610-88558925

方案、案例展示: 8610-88558925

Copyright 2000-2011 CCIDnet.All rights reserved.

京ICP000080号 网站-3