本想接着上一篇详解泛型接着写一篇使用泛型时需要注意的一个性能问题, 但是后来想着不如将之前的详解 XX 系列更正为现在的效率优化 XX 系列, 记录在工作时遇到的一些性能优化的经验和技巧, 如果有什么不足, 还请大家多多指出;
在使用集合时, 通常为了防止装箱操作而选择 List<T>,Dictionary<TKey, TValue > 等泛型集合, 但是在使用过程中如果使用不当, 依然会产生大量的装箱操作;
首先, 将值类型的实例当做引用类型来使用时, 即会产生装箱, 例如:
- int num = 10;
- object obj = num;
- IEquatable<int> iEquatable = num;
其次, 对于自定义结构, 在正常使用时, 通常需要注意一些误装箱的操作:
- public struct MyStruct
- {
- public int MyNum;
- }
对该结构 MyStruct 的实例调用基类 Object 中的方法时, 都会进行装箱操作, 对于静态方法 (Equals,ReferenceEquals) 很好理解, 对于实例方法, 在 CLR 调用实例方法时, 实际上会把调用这个方法的对象当作第一个参数传入实例方法, 而基类 Object 中的实例方法都会将 Object 类型的对象作为第一个参数, 因此也会发生装箱, 这其中的实例方法包括 GetType 和虚方法 Equals,GetHashCode,ToString;
其中, GetType 方法本身就是通过堆内存中与实例数据一起存储的方法表指针来获取实例类型信息的, 对于值类型实例, 本身就没有这个开销成员, 此处应使用 typeof()运算符代替避免装箱;
三个虚方法可以通过在 MyStruct 中重写来防止装箱操作; 但是对于 Equals 方法, 有一些需要区别注意的地方:
在调用值类型基类 ValueType 中的 ValueType.Equals(object obj)方法进行比较操作时, 会对当前实例和实参 obj 进行装箱, 共两次装箱 (抽象基类 ValueType 依然是类类型); 在 MyStruct 中重写了该方法 MyStruct.Equals(object obj), 在调用 myStruct1.Equals(myStruct2) 时, 依然会对 myStruct2 进行装箱, 共一次装箱, 此时我们可以在 MyStruct 中声明一个 Equals 的重载方法, 参数类型同样为 MyStruct, 同时对 == 和!= 运算符进行重载:
- public struct MyStruct
- {
- public int MyNum;
- public override bool Equals(object obj) // 调用时会对实参进行装箱
- {
- if (!(obj is MyStruct))
- {
- return false;
- }
- MyStruct other = (MyStruct)obj; // 拆箱
- return this.MyNum == other.MyNum;
- }
- public bool Equals(MyStruct other) // 重载 Equals 方法, 避免装箱
- {
- return this.MyNum == other.MyNum;
- }
- public static bool operator ==(MyStruct left, MyStruct right) // 比较时通常采用 == 运算符
- {
- return left.Equals(right);
- }
- public static bool operator !=(MyStruct left, MyStruct right)
- {
- return !(left == right);
- }
- }
此时, 在调用 myStruct1.Equals(myStruct2),myStruct1 == myStruct2,myStruct1 != myStruct2 时都不再产生装箱操作;
但是, 在使用泛型方法时, 例如对于以下的方法, 重载方法并不会生效:
- static bool MyFunc<T>(T obj1, T obj2)
- {
- return obj1.Equals(obj2);
- }
查看其生成的 IL 代码可以清楚的知道不生效的原因:
其中默认对 obj2 进行了 box 指令调用, 而对于 obj1, 在调用 callvir 指令时加入了前缀 constrained 指令, 则会判断 obj1 的类型定义中是否存在 Equals 方法的重写, 如果有则调用重写方法, 如果没有, 则装箱后调用基类 ValueType 中的虚方法; 前面 MyStruct 的定义中重写了 Equals 方法, 因此会调用该重写方法, 此时只触发一次对 obj2 的装箱, 但依然不是我们想要的;
为了避免这个问题, 我们需要在 MyStruct 的定义中实现 IEquatable<T > 接口, 并在这个泛型方法的声明中添加约束:
- public struct MyStruct : IEquatable<MyStruct>
- {
- public int MyNum;
- public override bool Equals(object obj)
- {
- if (!(obj is MyStruct))
- {
- return false;
- }
- MyStruct other = (MyStruct)obj;
- return this.MyNum == other.MyNum;
- }
- public bool Equals(MyStruct other) // 实现 IEquatable<T > 接口中的方法
- {
- return this.MyNum == other.MyNum;
- }
- public static bool operator ==(MyStruct left, MyStruct right)
- {
- return left.Equals(right);
- }
- public static bool operator !=(MyStruct left, MyStruct right)
- {
- return !(left == right);
- }
- }
- static bool MyFunc<T>(T obj1, T obj2) where T : IEquatable<T>
- {
- return obj1.Equals(obj2);
- }
此时, 查看其 IL 代码, 可以发现没有了 box 指令, 避免了装箱操作:
对泛型集合 List<Mystruct > 使用一些内含比较的实例方法时, 也会遇到上面的装箱问题, 解决方法同样是实现 IEquatable<T > 接口; 以常用的 Contains 方法举例:
List<MyStruct > 中的 Contains 方法中会调用泛型抽象类 EqualityComparer<T>.Default 的实例来进行比较, 而在抽象类 EqualityComparer<T > 中, 会根据类型参数 T 实例化对应的具体类实例, 具体可查看 EqualityComparer<T>.CreateComparer()中的实例生成逻辑, 其中, 会根据 T 是否实现了 IEquatable<T > 接口而实例化不同的类的实例:
- internal class GenericEqualityComparer<T>: EqualityComparer<T> where T: IEquatable<T>
- internal class ObjectEqualityComparer<T>: EqualityComparer<T>
这两个类的具体实现这里不再赘述;
基于上面的理解, 对于值类型, 实现基类的虚方法和 IEquatable<T > 接口对于避免装箱十分有必要;
如果您觉得阅读本文对您有帮助, 请点一下 "推荐" 按钮, 您的认可是我写作的最大动力!
作者: Minotauros
来源: https://www.cnblogs.com/minotauros/p/10041644.html