h5自助建站系统,wordpress 用户 购物,桂林人论坛电脑版,响应式网站科技一、垃圾回收
1.避免不必要的对象创建
①避免循环创建对象
如果对象并不会随每次循环而改变状态#xff0c;那么在循环中反复创建对象将带来性能损耗。高效的做法是将对象提到循环外面创建。
②在需要逻辑分支中创建对象
如果对象只在某些逻辑分支中才被用到#xff0c;…一、垃圾回收
1.避免不必要的对象创建
①避免循环创建对象
如果对象并不会随每次循环而改变状态那么在循环中反复创建对象将带来性能损耗。高效的做法是将对象提到循环外面创建。
②在需要逻辑分支中创建对象
如果对象只在某些逻辑分支中才被用到那么应只在该逻辑分支中创建对象。
③使用常量避免创建对象
程序中不应出现如 new Decimal(0) 之类的代码这会导致小对象频繁创建及回收正确的做法是使用Decimal.Zero常量。我们有设计自己的类时也可以学习这个设计手法应用到类似的场景中。
④使用StringBuilder做字符串连接
string是不变类使用 操作连接字符串将会导致创建一个新的字符串。如果字符串连接次数不是固定的例如在一个循环中则应该使用 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。
这里有一个细节应注意StringBuilder 内部 Buffer 的缺省值为 16 这个值实在太小。按 StringBuilder 的使用场景Buffer 肯定得重新分配。经验值一般用 256 作为 Buffer 的初值。当然如果能计算出最终生成字符串长度的话则应该按这个值来设定 Buffer 的初值。使用 new StringBuilder(256) 就将 Buffer 的初始长度设为了256。
⑤避免不必要的调用ToUpper 或ToLower 方法
String是不变类调用ToUpper或ToLower方法都会导致创建一个新的字符串。如果被频繁调用将导致频繁创建字符串对象。这违背了前面讲到的“避免频繁创建对象”这一基本原则。
例如bool.Parse方法本身已经是忽略大小写的调用时不要调用ToLower方法。
另一个非常普遍的场景是字符串比较。高效的做法是使用 Compare 方法这个方法可以做大小写忽略的比较并且不会创建新字符串。
还有一种情况是使用 HashTable 的时候有时候无法保证传递 key 的大小写是否符合预期往往会把 key 强制转换到大写或小写方法。实际上 HashTable 有不同的构造形式完全支持采用忽略大小写的 key: new HashTable(StringComparer.OrdinalIgnoreCase)。
2.不要使用空析构函数
如果类包含析构函数由创建对象时会在 Finalize 队列中添加对象的引用以保证当对象无法可达时仍然可以调用到 Finalize 方法。垃圾回收器在运行期间会启动一个低优先级的线程处理该队列。相比之下没有析构函数的对象就没有这些消耗。如果析构函数为空这个消耗就毫无意 义只会导致性能降低因此不要使用空的析构函数。在实际情况中许多曾在析构函数中包含处理代码但后来因为种种原因被注释掉或者删除掉了只留下一个空壳此时应注意把析构函数本身注释掉或删除掉。
3.实现 IDisposable 接口
垃圾回收事实上只支持托管内在的回收对于其他的非托管资源例如 Window GDI 句柄或数据库连接在析构函数中释放这些资源有很大问题。原因是垃圾回收依赖于内在紧张的情况虽然数据库连接可能已濒临耗尽但如果内存还很充足的话 垃圾回收是不会运行的。
C#的 IDisposable 接口是一种显式释放资源的机制。通过提供 using 语句还简化了使用方式编译器自动生成 try ... finally 块并在 finally 块中调用 Dispose 方法。对于申请非托管资源对象应为其实现 IDisposable 接口以保证资源一旦超出 using 语句范围即得到及时释放。这对于构造健壮且性能优良的程序非常有意义
为防止对象的 Dispose 方法不被调用的情况发生一般还要提供析构函数两者调用一个处理资源释放的公共方法。同时Dispose 方法应调用 System.GC.SuppressFinalize(this)告诉垃圾回收器无需再处理 Finalize 方法了。
4.最快的空串比较方法
将String对象的Length属性与0比较是最快的方法if (str.Length 0)
其次是与String.Empty常量或空串比较if (str String.Empty)或if (str )
注C#在编译时会将程序集中声明的所有字符串常量放到保留池中intern pool相同常量不会重复分配。
二、多线程
1.线程同步
线程同步是编写多线程程序需要首先考虑问题C#为同步提供了 Monitor、Mutex、AutoResetEvent 和 ManualResetEvent 对象来分别包装 Win32 的临界区、互斥对象和事件对象这几种基础的同步机制C#还提供了一个lock语句方便使用编译器会自动生成适当的 Monitor.Enter 和 Monitor.Exit 调用。
①同步粒度
同步粒度可以是整个方法也可以是方法中某一段代码为方法指定 MethodImplOptions.Synchronized 属性将标记对整个方法同步。例如
[MethodImpl(MethodImplOptions.Synchronized)]
public static SerialManager GetInstance()
{if (instance null){instance new SerialManager();}return instance;
} 通常情况下应减小同步的范围使系统获得更好的性能。简单将整个方法标记为同步不是一个好主意除非能确定方法中的每个代码都需要受同步保护。
②同步策略
使用 lock 进行同步同步对象可以选择 Type、this 或为同步目的专门构造的成员变量。
★避免锁定Type
锁定Type对象会影响同一进程中所有AppDomain该类型的所有实例这不仅可能导致严重的性能问题还可能导致一些无法预期的行为。这是一个很不 好的习惯。即便对于一个只包含static方法的类型也应额外构造一个static的成员变量让此成员变量作为锁定对象。
★避免锁定 this
锁定 this 会影响该实例的所有方法。假设对象 obj 有 A 和 B 两个方法其中 A 方法使用 lock(this) 对方法中的某段代码设置同步保护。现在因为某种原因B 方法也开始使用 lock(this) 来设置同步保护了并且可能为了完全不同的目的。这样A 方法就被干扰了其行为可能无法预知。所以作为一种良好的习惯建议避免使用 lock(this) 这种方式。
使用为同步目的专门构造的成员变量这是推荐的做法。方式就是 new 一个 object 对象 该对象仅仅用于同步目的。如果有多个方法都需要同步并且有不同的目的那么就可以为些分别建立几个同步成员变量。
③集合同步
C#为各种集合类型提供了两种方便的同步机制Synchronized 包装器和 SyncRoot 属性。
// Creates and initializes a new ArrayList
ArrayList myAL new ArrayList();
myAL.Add( The );
myAL.Add( quick );
myAL.Add( brown );
myAL.Add( fox );
//Creates a synchronized wrapper around the ArrayList
ArrayList mySyncdAL ArrayList.Synchronized(myAL);
调用 Synchronized 方法会返回一个可保证所有操作都是线程安全的相同集合对象。考虑 mySyncdAL[0] mySyncdAL[0] test 这一语句读和写一共要用到两个锁。一般讲效率不高。推荐使用 SyncRoot 属性可以做比较精细的控制。
④使用 ThreadStatic 替代 NameDataSlot
存 取 NameDataSlot 的 Thread.GetData 和 Thread.SetData 方法需要线程同步涉及两个锁一个是 LocalDataStore.SetData 方法需要在 AppDomain 一级加锁另一个是 ThreadNative.GetDomainLocalStore 方法需要在 Process 一级加锁。如果一些底层的基础服务使用了 NameDataSlot将导致系统出现严重的伸缩性问题。
规避这个问题的方法是使用 ThreadStatic 变量。示例如下
public sealed class InvokeContext
{[ThreadStatic]private static InvokeContext current;private Hashtable maps new Hashtable();
}
2.多线程编程
①使用 Double Check 技术创建对象
internal IDictionary KeyTable
{get{if (this._keyTable null){lock (base._lock){if (this._keyTable null){this._keyTable new Hashtable();}}}return this._keyTable;}
}
创建单例对象是很常见的一种编程情况。一般在 lock 语句后就会直接创建对象了但这不够安全。因为在 lock 锁定对象之前可能已经有多个线程进入到了第一个 if 语句中。如果不加第二个 if 语句则单例对象会被重复创建新的实例替代掉旧的实例。如果单例对象中已有数据不允许被破坏或者别的什么原因则应考虑使用 Double Check 技术。
3.系统类型
①避免无意义的变量初始化动作
CLR保证所有对象在访问前已初始化其做法是将分配的内存清零。因此不需要将变量重新初始化为0、false或null。
需要注意的是方法中的局部变量不是从堆而是从栈上分配所以C#不会做清零工作。如果使用了未赋值的局部变量编译期间即会报警。不要因为有这个印象而对所有类的成员变量也做赋值动作两者的机理完全不同
②以引用方式传递值类型参数
值类型从调用栈分配引用类型从托管堆分配。当值类型用作方法参数时默认会进行参数值复制这抵消了值类型分配效率上的优势。作为一项基本技巧以引用方式传递值类型参数可以提高性能。
③为 ValueType 提供 Equals 方法
.NET默认实现的 ValueType.Equals 方法使用了反射技术依靠反射来获得所有成员变量值做比较这个效率极低。
如果我们编写的值对象其 Equals 方法要被用到例如将值对象放到 HashTable 中那么就应该重载 Equals 方法。
public struct Rectangle
{public double Length;public double Breadth;public override bool Equals ( object ob){if(ob is Rectangle)return Equels ((Rectangle)ob))elsereturn false ;}private bool Equals (Rectangle rect){return this .Length rect.Length this .Breadth rect.Breach;}
}
4.避免装箱和拆箱
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 ]; // The element is unboxed
5.异常处理
异常也是现代语言的典型特征。与传统检查错误码的方式相比异常是强制性的不依赖于是否忘记了编写检查错误码的代码、强类型的、并带有丰富的异常信息例如调用栈。
★不要吃掉异常
关于异常处理的最重要原则就是不要吃掉异常。这个问题与性能无关但对于编写健壮和易于排错的程序非常重要。这个原则换一种说法就是不要捕获那些你不能处理的异常。
吃掉异常是极不好的习惯因为你消除了解决问题的线索。一旦出现错误定位问题将非常困难。除了这种完全吃掉异常的方式外只将异常信息写入日志文件但并不做更多处理的做法也同样不妥。
★不要吃掉异常信息
有些代码虽然抛出了异常但却把异常信息吃掉了。
为异常披露详尽的信息是程序员的职责所在。如果不能在保留原始异常信息含义的前提下附加更丰富和更人性化的内容那么让原始的异常信息直接展示也要强得多。千万不要吃掉异常。
★避免不必要的抛出异常
抛出异常和捕获异常属于消耗比较大的操作在可能的情况下应通过完善程序逻辑避免抛出不必要不必要的异常。与此相关的一个倾向是利用异常来控制处理逻辑。尽管对于极少数的情况这可能获得更为优雅的解决方案但通常而言应该避免。
★避免不必要的重新抛出异常
如果是为了包装异常的目的即加入更多信息后包装成新异常那么是合理的。但是有不少代码捕获异常没有做任何处理就再次抛出这将无谓地增加一次捕获异常和抛出异常的消耗对性能有伤害。
6.反射
反射是一项很基础的技术它将编译期间的静态绑定转换为延迟到运行期间的动态绑定。在很多场景下特别是类框架的设计可以获得灵活易于扩展的架构。但带来的问题是与静态绑定相比动态绑定会对性能造成较大的伤害。
①反射分类
type comparison 类型判断主要包括 is 和 typeof 两个操作符及对象实例上的 GetType 调用。这是最轻型的消耗可以无需考虑优化问题。注意 typeof 运算符比对象实例上的 GetType 方法要快只要可能则优先使用 typeof 运算符。
member enumeration 成员枚举用于访问反射相关的元数据信息例如Assembly.GetModule、Module.GetType、Type对象上的 IsInterface、IsPublic、GetMethod、GetMethods、GetProperty、GetProperties、 GetConstructor调用等。尽管元数据都会被CLR缓存但部分方法的调用消耗仍非常大不过这类方法调用频度不会很高所以总体看性能损失程 度中等。
member invocation成员调用包括动态创建对象及动态调用对象方法主要有Activator.CreateInstance、Type.InvokeMember等。
②动态创建对象
C#主要支持 5 种动态创建对象的方式
1. Type.InvokeMember
2. ContructorInfo.Invoke
3. Activator.CreateInstance(Type)
4. Activator.CreateInstance(assemblyName, typeName)
5. Assembly.CreateInstance(typeName)
最快的是方式 3 与 Direct Create 的差异在一个数量级之内约慢 7 倍的水平。其他方式至少在 40 倍以上最慢的是方式 4 要慢三个数量级。
③动态方法调用
方法调用分为编译期的早期绑定和运行期的动态绑定两种称为Early-Bound Invocation和Late-Bound Invocation。
Early-Bound Invocation可细分为Direct-call、Interface-call和Delegate-call。
Late-Bound Invocation主要有Type.InvokeMember和MethodBase.Invoke还可以通过使用LCGLightweight Code Generation技术生成IL代码来实现动态调用。
从测试结果看相比Direct CallType.InvokeMember要接近慢三个数量级MethodBase.Invoke虽然比Type.InvokeMember要快三 倍但比Direct Call仍慢270倍左右。可见动态方法调用的性能是非常低下的。我们的建议是除非要满足特定的需求否则不要使用
④推荐的使用原则
1 如果可能则避免使用反射和动态绑定
2 使用接口调用方式将动态绑定改造为早期绑定
3 使用Activator.CreateInstance(Type)方式动态创建对象
4 使用typeof操作符代替GetType调用
反模式
1 在已获得Type的情况下却使用Assembly.CreateInstance(type.FullName)
三、基本代码技巧
这里描述一些应用场景下可以提高性能的基本代码技巧。对处于关键路径的代码进行这类的优化还是很有意义的。普通代码可以不做要求但养成一种好的习惯也是有意义的。
1.循环写法
可以把循环的判断条件用局部变量记录下来。局部变量往往被编译器优化为直接使用寄存器相对于普通从堆或栈中分配的变量速度快。如果访问的是复杂计算属性 的话提升效果将更明显。for (int i 0, j collection.GetIndexOf(item); i j; i)
需要说明的是这种写法对于CLR集合类的Count属性没有意义原因是编译器已经按这种方式做了特别的优化。
2.拼装字符串
拼装好之后再删除是很低效的写法。有些方法其循环长度在大部分情况下为1这种写法的低效就更为明显了
public static string ToString(MetadataKey entityKey)
{string str ;object [] vals entityKey.values;for ( int i 0 ; i vals.Length; i ){str , vals[i].ToString();}return str ? : str.Remove( 0 , 1 );
}
推荐下面的写法
if(str.Length 0 )str vals[i].ToString();
elsestr , vals[i].ToString(); 其实这种写法非常自然而且效率很高完全不需要用个Remove方法绕来绕去。
3.避免两次检索集合元素
获取集合元素时有时需要检查元素是否存在。通常的做法是先调用ContainsKey或Contains方法然后再获取集合元素。这种写法非常符合逻辑。 但如果考虑效率可以先直接获取对象然后判断对象是否为null来确定元素是否存在。对于Hashtable这可以节省一次GetHashCode调用和n次Equals比较。 如下面的示例 public IData GetItemByID(Guid id)
{IData data1 null ;if( this .idTable.ContainsKey(id.ToString()){data1 this .idTable[id.ToString()] as IData;}return data1;
}
其实完全可用一行代码完成return this.idTable[id] as IData;
4.避免两次类型转换
考虑如下示例其中包含了两处类型转换
if (obj is SomeType)
{SomeType st (SomeType)obj;st.SomeTypeMethod();
}
效率更高的做法如下
SomeType st obj as SomeType;
if (st ! null ){st.SomeTypeMethod();
} 5.Hashtable Hashtable是一种使用非常频繁的基础集合类型。需要理解影响Hashtable的效率有两个因素一是散列码GetHashCode方法二 是等值比较Equals方法。
Hashtable首先使用键的散列码将对象分布到不同的存储桶中随后在该特定的存储桶中使用键的Equals方法进 行查找。
良好的散列码是第一位的因素最理想的情况是每个不同的键都有不同的散列码。Equals方法也很重要因为散列只需要做一次而存储桶中查找键可能需要做多次。
从实际经验看使用Hashtable时Equals方法的消耗一般会占到一半以上。
System.Object类提供了默认的GetHashCode实现使用对象在内存中的地址作为散列码。
我们遇到过一个用Hashtable来缓存对 象的例子每次根据传递的OQL表达式构造出一个ExpressionList对象再调用QueryCompiler的方法编译得到 CompiledQuery对象。以ExpressionList对象和CompiledQuery对象作为键值对存储到Hashtable中。
ExpressionList对象没有重载GetHashCode实现其超类ArrayList也没有这样最后用的就是System.Object类 的GetHashCode实现。由于ExpressionList对象会每次构造因此它的HashCode每次都不同所以这个 CompiledQueryCache根本就没有起到预想的作用。
这个小小的疏漏带来了重大的性能问题由于解析OQL表达式频繁发生导致 CompiledQueryCache不断增长造成服务器内存泄漏解决这个问题的最简单方法就是提供一个常量实现例如让散列码为常量0。
虽然这会导 致所有对象汇聚到同一个存储桶中效率不高但至少可以解决掉内存泄漏问题。当然最终还是会实现一个高效的GetHashCode方法的。 6.大批量数据操作
当需要对数据库进行大批量数据操作的时候推荐使用分批操作的功能比如一百万条数据将其分为每一万条数据进行数据库操作而不是每条数据循环去进行操作。
查询 - 分批查询对数据库的压力较小如果那一张表在这个时候可能其他地方也更新或新增 可能需要考虑增加with(NOLOCK) 当然如果是EF 就套上读未提交的事务(会变卡) 也可以让查询不加锁。
删除 - 首推根据主健进行删除因为数据库根据主键的索引查找和删除数据非常快当然分批更好。