C#软件性能优化

C#软件性能优化

1.    性能

       衡量一个软件系统性能的常见指标有:响应时间、负载、资源使用率、并发数。在软件中有具体的提高性能需求时,我们需分析该系统性能的影响由哪些因素组成,再针对各部分进行性能优化。例如:我们在仪器设备软件中,从数据读写、算法计算、业务过程、通讯环节分析,根据需求的性能指标进行优化。

1.1      优化性能的原则

结构优化,减少数据交互的频次;

减小数据的交互量;

优化资源,减少创建对象的次数;

提高数据的读写速度;

算法与数据结构的优化;

业务过程优化;

通讯性能提升;

数据库优化;

1.2      常用优化性能的方法

1.2.1 避免不必要的对象创建

    如果对象并不会随每次循环而改变状态,那么在循环中反复创建对象将带来性能损耗。高效的做法是将对象提到循环外面创建。

1.2.2 避免不必要的抛出异常

抛出异常和捕获异常属于消耗比较大的操作,在可能的情况下,应通过完善程序逻辑避免抛出不必要不必要的异常。

如:预先做参数值的判断。

如果是为了包装异常的目的(即加入更多信息后包装成新异常),那么是合理的。但是有不少代码,捕获异常没有做任何处理就再次抛出,这将无谓地增加一次捕获异常和抛出异常的消耗。如以下代码则无谓增加一次捕获与抛出异常。

 Try

{

        ...

}

Catch(Exception ex)

{

        Throw ex;

}

   

 

1.2.3 避免装箱拆箱

    C#可以在值类型和引用类型之间自动转换,方法是装箱和拆箱。装箱需要从堆上分配对象并拷贝值,有一定性能消耗。如果这一过程发生在循环中或是作为底层方法被频繁调用,应当避免。

 ArrayList al = new ArrayList(); 

 for ( int i = 0 ; i < 1000 ; i ++ ) 

 { 

   al.Add(i); // Implicitly boxed because Add() takes an object 

 } 

int f = ( int )al[ 0 ];

则ArrayList可定义为List<int>

 

1.2.4 使用 StringBuilder 做字符串连接

    如果字符串连接次数不是固定的,例如在一个循环中,则应该使用 StringBuilder 类来做字符串连接工作。因为 StringBuilder 内部有一个 StringBuffer ,连接操作不会每次分配新的字符串空间。只有当连接后的字符串超出 Buffer 大小时,才会申请新的 Buffer 空间。典型代码如下:

 StringBuilder sb = new StringBuilder( 256 ); 

 for ( int i = 0 ; i < Results.Count; i ++ ) 

 {

       sb.Append (Results[i]); 

   } 

    如果连接次数是固定的并且只有几次,此时应该直接用 + 号连接,保持程序简洁易读。实际上,编译器已经做了优化,会依据加号次数调用不同参数个数的 String.Concat 方法。例如: String str = str1 + str2 + str3 + str4; 会被编译为 String.Concat(str1, str2, str3, str4)。该方法内部会计算总的 String 长度,仅分配一次,并不会如通常想象的那样分配三次。作为一个经验值,当字符串连接操作达到 10 次以上时,则应该使用 StringBuilder。 

1.2.5 避免不必要的调用 ToUpper 或 ToLower 方法 

       String是不变类,调用ToUpper或ToLower方法都会导致创建一个新的字符串。如果被频繁调用,将导致频繁创建字符串对象。这违背了前面讲到的“避免频繁创建对象”这一基本原则。 

例如,bool.Parse方法本身已经是忽略大小写的,调用时不要调用ToLower方法。 

另一个非常普遍的场景是字符串比较。高效的做法是使用 Compare 方法,这个方法可以做大小写忽略的比较,并且不会创建新字符串。 

还有一种情况是使用 HashTable 的时候,有时候无法保证传递 key 的大小写是否符合预期,往往会把 key 强制转换到大写或小写方法。实际上 HashTable 有不同的构造形式,完全支持采用忽略大小写的 key: new HashTable(StringComparer.OrdinalIgnoreCase)。

1.2.6 数据缓存

如果在我们的系统中,有一些基础数据会经常用到,如果频繁去访问数据库或文件系统读取,会由于频繁数据访问、读取、装箱成数据对象而消耗时间。我们可以预先将数据读取出这些基础数据到缓存后使用。

1.2.7 使用字典

在涉及数据列表中有大量的数据需根据其一个标识进行访问时,可以使用字典方式操作,这样相比列表操作,速度会有显著提升.

1.2.8 对象的引用代替复制

在传递数据值时,如果使用复制则会先创建一个新的实例,而引用传递的实际是对象指针,可以通过引用访问数据。

注意:被引用后有可能会被修改,这种修改是否违背开发者的本意。

 

1.2.9 开启虚拟加载

    在使用WPF的一些控件如:TreeView,ListBox, ListView, TreeView加载大量数据时,会消耗较长的时间,可以使用虚拟化打开来提高加载速度。

具体操作例如:在XAML控件的属性中加入 VirtualizingPanel.IsVirtualizing="True" 。

 

1.2.10 使用静态资源

  动态资源涉及到运行时查找和对象的构建, 从而会影响到性能,静态资源是预定义的资源,可以连接到XAML属性,它类似于编译时绑定,不会影响性能。但也需要注意,静态资源需要在编译时展示。

例如:一个DataGrid控件使用一个静态资源  Style="{StaticResource dataGridStyle}"

1.2.11 资源回收

       虽然.NET提供了全套的资源管理(无用资源回收:Garbage Collection,简称GC),但它不是万能的,我的系统资源是有限的,内存、文件、连接用完了要释放。

 例如:在有多次数据表读取使用后,应释放对象:

Void  ReadData()

{

              DataTable dt = new DataTable;

              FillDataTable(ref dt);

              ......

             // 使用完后

             dtData.Dispose();

            dtData = null;

            GC.Collect();

}

      

1.2.12 重写LINQ和Lambdas表达式

如果代码需要执行很多次的时候,可能需要对LINQ或者Lambdas表达式进行重写。

下面的例子使用LINQ以及函数式风格的代码来通过编译器模型给定的名称来查找符号。

class Symbol

{

    public string Name { get; private set; }

}

class Compiler

{

    private List<Symbol> symbols;

    public Symbol FindMatchingSymbol(string name)

    {

        return symbols.FirstOrDefault(s => s.Name == name);

    }

}

在此过程中,这么简单的一行代码隐藏了基础内存分配开销。在上面的展开FirstOrDefault调用的例子中,代码会调用IEnumerabole<T>接口中的GetEnumerator()方法。将symbols赋值给IEnumerable<Symbol>类型的enumerable 变量,会使得对象丢失了其实际的List<T>类型信息。这就意味着当代码通过enumerable.GetEnumerator()方法获取迭代器时,.NET Framework 必须对返回的值(即迭代器,使用结构体实现)类型进行装箱从而将其赋给IEnumerable<Symbol>类型的(引用类型) enumerator变量。

解决办法是重写FindMatchingSymbol方法,将单个语句使用六行代码替代,这些代码依旧连贯,易于阅读和理解,也很容易实现。

public Symbol FindMatchingSymbol(string name)

{

    foreach (Symbol s in symbols)

    {

        if (s.Name == name)

            return s;

    }

    return null;

}

代码中并没有使用LINQ扩展方法,lambdas表达式和迭代器,并且没有额外的内存分配开销。

 

1.2.13 减少通讯的频次,组合指令

以我们XMC5400为例,上下位机指令通迅时,按照约定的通讯协议,遵循Finished与ACK机制,双方都需对接到的信号作出多次反馈,完成一次指令最少也得超过15ms,如果在一个短时间段时发送大量指令,错误率必然会增加,因此,我们采取的方式是,使用组合指令,把原先多条指令合并为一条指令,这样减少通讯的压力。组合指令按照大平台原则来组合。

 

1.2.14 减少通讯数据量,简化参数

有些平时不经常变的参数放到下位机,作参数简化,减少每次通讯的字节数。例如不用每次都传电流参数,速度加速度因为成对出现,可使用速度等级。

 

1.3      数据库性能

1.3.1 使用字段数据类型的时候,尽可能的用小的数据类型

1.3.2 针对需要的字段适当的加上索引

1.3.3 应用层面进行优化

例如加上缓存,或者页面静态化,让后面的请求不再查询数据库,这样效率更高,将效率分摊至前端方便扩展应用服务器

1.3.4 多数据库布署

在有大量的表与数据时,可以使用主从、读写分离的方式减轻数据库服务器的压力,避免在单台机器上过度消耗资源

1.3.5 垂直分表

如果一张表字段过多且有一部分字段经常读写另一部分字段不常读写,可以对数据表进行拆分,将不经常读的内容和经常操作的内容放置不同表中。

1.3.6 不读取非需内容,降低磁盘IO

1.3.7 可以使用分区表技术,将一个表分成多个不同的文件

1.3.8 尽量不要使用like

1.3.9 搜索时加上主键,不用或者少用全文索引

1.3.10 优化相关配置的参数

如最大连接数, 恢复模式。

1.3.11 尽量不使字段为NULL值

1.3.12 尽量不在 where 子句中使用 or 来连接条件

1.3.13 使用Bulk操作

在大批量数据写入与更新时,可采用BULK操作,记录越多,越体现该操作的时间性能。

1.4      无需过早优化

有些优化对在整体软件性能言而并不能在一程度上改善,例如:我们检索一个仅包含几个元素的列表,那就并不需要对此再做一个字典或映射;有些功能使用频率低,而本已经满足要求的,可不需再做优化。

posted on 2018-08-31 15:42  youmeetmehere  阅读(1810)  评论(0编辑  收藏  举报

导航