跨程序集,类与结构体究竟谁快

作者: 工程材料  发布:2019-10-07

前面的三次测试都是在同一个项目内的,既处于同一个程序集。那么,跨程序集调用会怎么样呢?

上次我对C#类与结构体做了一次速度评测()。经过一段时间思索,发现还可以进一步探讨——

以前我一直有个疑惑——在C#中,究竟是类(class)比较快,还是结构体(struct)比较快?
当时没有深究。

上次我分别测试了类与结构体()、密封类()的函数调用速度评测。现在进行进一步分析,解读编译器生成的MSIL(微软中间语言)代码。

因为为了保证可维护性,我们会把一些常用操作封装到类库中去。然后实际项目开发时,引用该类库,使得解决方案中存在多个项目。编译完成后,将会是一个主要的exe和若干个dll文件,主exe程序集中的代码会调用dll程序集,既形成了跨程序集调用。

第一、栈变量。上次的“硬编码”,是访问类中的静态变量的。若改为访问函数中的栈变量,性能会不会有所提高?
第二、栈分配(stackalloc)。既然要测试栈变量,我们还可以顺便测试一下在栈上分配的内存块的访问性能。
第三、64位整数。由于32位系统的成功,我们已经习惯了使用32位整数(int)。现在64位系统逐渐普及,我们得为此做好准备。对于指针操作时经常要用到的偏移量增减运算来说,是使用32位整数,还是使用64位整数,或写两套代码?这需要测试后才能决定。
第四、密封类(sealed)。听说密封类能提高性能,我们可以测试一下。有两种测试方式,一是为原来的派生类增加sealed关键字,二是专门另外写一个密封类。我决定同时使用这两种方法,分别测试其性能。

最近我遇到一个难题,需要将一些运算大的指针操作代码给封装一下。原先为了性能,这些代码是以硬编码的形式混杂在算法逻辑之中,不但影响了算法逻辑的可读性,其本身的指针操作代码枯燥、难懂、易写错,不易维护。所以我希望将其封装一下,简化代码编写、提高可维护性,但同时要尽可能地保证性能。
由于那些指针操作代码很灵活,简单的封装不能解决问题,还需要用到接口(interface)以实现一些动态调用功能。
为了简化代码,还打算实现一些泛型方法。
本来还想因32位指针、64位指针的不同而构造泛型类,可惜发现C#不支持将int/long作为泛型类型约束,只好作罢。将设计改为——分别为32位指针、64位指针编写不同的类,它们实现同一个接口。

一、前期准备

分析跨程序集调用的性能,有助优化类库架构的设计。

一、测试代码

  测试代码如下——

using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;

namespace TryPointerCall
{
    /// <summary>
    /// 指针操作接口
    /// </summary>
    public interface IPointerCall
    {
        /// <summary>
        /// 指针操作
        /// </summary>
        /// <param name="p">源指针</param>
        /// <returns>修改后指针</returns>
        unsafe byte* Ptr(byte* p);
    }

#region 非泛型
    /// <summary>
    /// [非泛型] 指针操作基类
    /// </summary>
    public abstract class PointerCall : IPointerCall
    {
        public abstract unsafe byte* Ptr(byte* p);
    }

    /// <summary>
    /// [非泛型] 指针操作派生类: 指针+偏移
    /// </summary>
    public sealed class PointerCallAdd : PointerCall
    {
        /// <summary>
        /// 偏移值
        /// </summary>
        public int Offset = 0;

        public override unsafe byte* Ptr(byte* p)
        {
            return unchecked(p + Offset);
        }
    }

    /// <summary>
    /// [非泛型] 指针操作密封类: 指针+偏移
    /// </summary>
    public sealed class SldPointerCallAdd : IPointerCall
    {
        /// <summary>
        /// 偏移值
        /// </summary>
        public int Offset = 0;

        public unsafe byte* Ptr(byte* p)
        {
            return unchecked(p + Offset);
        }
    }

    /// <summary>
    /// [非泛型] 指针操作结构体: 指针+偏移
    /// </summary>
    public struct SPointerCallAdd : IPointerCall
    {
        /// <summary>
        /// 偏移值
        /// </summary>
        public int Offset;

        public unsafe byte* Ptr(byte* p)
        {
            return unchecked(p + Offset);
        }
    }

#endregion

#region 泛型
    // !!! C#不支持将整数类型作为泛型约束 !!!
    //public abstract class GenPointerCall<T> : IPointerCall where T: int, long
    //{
    //    public abstract unsafe byte* Ptr(byte* p);

    //    void d()
    //    {
    //    }
    //}

#endregion

#region 全部测试
    /// <summary>
    /// 指针操作的一些常用函数
    /// </summary>
    public static class PointerCallTool
    {
#if DEBUG
        private const int CountLoop = 10000000; // 循环次数
#else
        private const int CountLoop = 200000000;    // 循环次数
#endif

        /// <summary>
        /// 调用指针操作
        /// </summary>
        /// <typeparam name="T">具有IPointerCall接口的类型。</typeparam>
        /// <param name="ptrcall">调用者</param>
        /// <param name="p">源指针</param>
        /// <returns>修改后指针</returns>
        public static unsafe byte* CallPtr<T>(T ptrcall, byte* p) where T : IPointerCall
        {
            return ptrcall.Ptr(p);
        }
        public static unsafe byte* CallClassPtr<T>(T ptrcall, byte* p) where T : PointerCall
        {
            return ptrcall.Ptr(p);
        }
        public static unsafe byte* CallRefPtr<T>(ref T ptrcall, byte* p) where T : IPointerCall
        {
            return ptrcall.Ptr(p);
        }

        // C#不允许将特定的结构体作为泛型约束。所以对于结构体只能采用上面那个方法,通过IPointerCall接口进行约束,可能会造成性能下降。
        //public static unsafe byte* SCallPtr<T>(T ptrcall, byte* p) where T : SPointerCallAdd
        //{
        //    return ptrcall.Ptr(p);
        //}

        private static int TryIt_Static_Offset;
        private static unsafe byte* TryIt_Static_Ptr(byte* p)
        {
            return unchecked(p + TryIt_Static_Offset);
        }
        /// <summary>
        /// 执行测试 - 静态调用
        /// </summary>
        /// <param name="sOut">文本输出</param>
        private static unsafe void TryIt_Static(StringBuilder sOut, int CountLoop)
        {
            TryIt_Static_Offset = 1;

            // == 性能测试 ==
            byte* p = null;
            Stopwatch sw = new Stopwatch();
            int i;
            unchecked
            {
                #region 测试
                // 硬编码.栈变量
                int iOffset = 1;
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = p + iOffset;
                }
                sw.Stop();
                sOut.AppendLine(string.Format("硬编码.栈变量:t{0}", sw.ElapsedMilliseconds));

                // 硬编码.栈分配
                int* pOffset = stackalloc int[1];
                pOffset[0] = 1;
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = p + pOffset[0];
                }
                sw.Stop();
                sOut.AppendLine(string.Format("硬编码.栈分配:t{0}", sw.ElapsedMilliseconds));

                // 硬编码.静态
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = p + TryIt_Static_Offset;
                }
                sw.Stop();
                sOut.AppendLine(string.Format("硬编码.静态:t{0}", sw.ElapsedMilliseconds));

                // 静态调用
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = TryIt_Static_Ptr(p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("静态调用:t{0}", sw.ElapsedMilliseconds));

                #endregion // 测试
            }
        }

        private static long TryIt_Static64_Offset;
        private static unsafe byte* TryIt_Static64_Ptr(byte* p)
        {
            return unchecked(p + TryIt_Static64_Offset);
        }
        /// <summary>
        /// 执行测试 - 静态调用
        /// </summary>
        /// <param name="sOut">文本输出</param>
        private static unsafe void TryIt_Static64(StringBuilder sOut, int CountLoop)
        {
            TryIt_Static64_Offset = 1;

            // == 性能测试 ==
            byte* p = null;
            Stopwatch sw = new Stopwatch();
            int i;
            unchecked
            {
                #region 测试
                // 硬编码.栈变量
                long iOffset = 1;
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = p + iOffset;
                }
                sw.Stop();
                sOut.AppendLine(string.Format("64硬编码.栈变量:t{0}", sw.ElapsedMilliseconds));

                // 硬编码.栈分配
                long* pOffset = stackalloc long[1];
                pOffset[0] = 1;
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = p + pOffset[0];
                }
                sw.Stop();
                sOut.AppendLine(string.Format("64硬编码.栈分配:t{0}", sw.ElapsedMilliseconds));

                // 硬编码.静态
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = p + TryIt_Static64_Offset;
                }
                sw.Stop();
                sOut.AppendLine(string.Format("64硬编码.静态:t{0}", sw.ElapsedMilliseconds));

                // 静态调用
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = TryIt_Static64_Ptr(p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("64静态调用:t{0}", sw.ElapsedMilliseconds));

                #endregion // 测试
            }
        }

        /// <summary>
        /// 执行测试 - 非泛型
        /// </summary>
        /// <param name="sOut">文本输出</param>
        private static unsafe void TryIt_NoGen(StringBuilder sOut, int CountLoop)
        {
            // 创建
            PointerCallAdd pca = new PointerCallAdd();
            SldPointerCallAdd dpca = new SldPointerCallAdd();
            SPointerCallAdd spca;
            pca.Offset = 1;
            spca.Offset = 1;

            // 转型
            PointerCall pca_base = pca;
            IPointerCall pca_itf = pca;
            IPointerCall dpca_itf = dpca;
            IPointerCall spca_itf = spca;

            // == 性能测试 ==
            byte* p = null;
            Stopwatch sw = new Stopwatch();
            int i;
            unchecked
            {
                #region 调用
                #region 直接调用
                // 调用派生类
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = pca.Ptr(p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("调用派生类:t{0}", sw.ElapsedMilliseconds));

                // 调用密封类
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = dpca.Ptr(p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("调用密封类:t{0}", sw.ElapsedMilliseconds));

                // 调用结构体
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = spca.Ptr(p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("调用结构体:t{0}", sw.ElapsedMilliseconds));

                #endregion  // 直接调用

                #region 间接调用
                // 调用基类
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = pca_base.Ptr(p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("调用基类:t{0}", sw.ElapsedMilliseconds));

                // 调用派生类的接口
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = pca_itf.Ptr(p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("调用派生类的接口:t{0}", sw.ElapsedMilliseconds));

                // 调用密封类的接口
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = dpca_itf.Ptr(p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("调用密封类的接口:t{0}", sw.ElapsedMilliseconds));

                // 调用结构体的接口
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = spca_itf.Ptr(p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("调用结构体的接口:t{0}", sw.ElapsedMilliseconds));

                #endregion  // 间接调用

                #endregion  // 调用

                #region 泛型调用

                #region 泛型基类约束
                // 基类泛型调用派生类
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = CallClassPtr(pca, p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("基类泛型调用派生类:t{0}", sw.ElapsedMilliseconds));

                // 基类泛型调用基类
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = CallClassPtr(pca_base, p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("基类泛型调用基类:t{0}", sw.ElapsedMilliseconds));

                #endregion // 泛型基类约束

                #region 泛型接口约束 - 直接调用
                // 接口泛型调用派生类
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = CallPtr(pca, p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("接口泛型调用派生类:t{0}", sw.ElapsedMilliseconds));

                // 接口泛型调用密封类
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = CallPtr(dpca, p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("接口泛型调用密封类:t{0}", sw.ElapsedMilliseconds));

                // 接口泛型调用结构体
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = CallPtr(spca, p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("接口泛型调用结构体:t{0}", sw.ElapsedMilliseconds));

                // 接口泛型调用结构体引用
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = CallRefPtr(ref spca, p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("接口泛型调用结构体引用:t{0}", sw.ElapsedMilliseconds));

                #endregion  // 直接调用

                #region 间接调用
                // 接口泛型调用基类
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = CallPtr(pca_base, p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("接口泛型调用基类:t{0}", sw.ElapsedMilliseconds));

                // 接口泛型调用派生类的接口
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = CallPtr(pca_itf, p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("接口泛型调用派生类的接口:t{0}", sw.ElapsedMilliseconds));

                // 接口泛型调用密封类的接口
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = CallPtr(dpca_itf, p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("接口泛型调用密封类的接口:t{0}", sw.ElapsedMilliseconds));

                // 接口泛型调用结构体的接口
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = CallPtr(spca_itf, p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("接口泛型调用结构体的接口:t{0}", sw.ElapsedMilliseconds));

                #endregion  // 间接调用

                #endregion  // 泛型调用

            }
        }

        /// <summary>
        /// 执行测试 - 泛型
        /// </summary>
        /// <param name="sOut">文本输出</param>
        private static unsafe void TryIt_Gen(StringBuilder sOut, int CountLoop)
        {
            // !!! C#不支持将整数类型作为泛型约束 !!!
        }

        /// <summary>
        /// 执行测试
        /// </summary>
        public static string TryIt()
        {
            StringBuilder sOut = new StringBuilder();
            sOut.AppendLine("== PointerCallTool.TryIt() ==");
            TryIt_Static(sOut, CountLoop);
            TryIt_Static64(sOut, CountLoop);
            TryIt_NoGen(sOut, CountLoop);
            TryIt_Gen(sOut, CountLoop);
            sOut.AppendLine();
            return sOut.ToString();
        }

        /// <summary>
        /// 执行测试 - static
        /// </summary>
        public static string TryItStatic()
        {
            StringBuilder sOut = new StringBuilder();
            int cnt = CountLoop * 10;
            sOut.AppendLine("== PointerCallTool.TryItStatic() ==");
            TryIt_Static(sOut, cnt);
            TryIt_Static64(sOut, cnt);
            sOut.AppendLine();
            return sOut.ToString();
        }
    }
#endregion

}

 

在C#中,有两类封装技术——
1.基于类(class)的封装。在基类中定义好操作方法,然后在派生类中实现操作方法。
2.基于结构体(struct)的封装。在接口中定义好操作方法,然后在结构体中实现该接口的操作方法。
我分别使用这两类封装技术编写测试代码,然后做性能测试。

先找到“IL 反汇编程序”(开始程序Microsoft Visual Studio 2010Microsoft Windows SDK Tools)——

一、测试办法

二、测试环境

  编译器——
VS2005:Visual Studio 2005 SP1。
VS2010:Visual Studio 2010 SP1。
采用上述编译器编译为Release版程序,最大速度优化。

  机器A——
HP CQ42-153TX
处理器:Intel Core i5-430M(2.26GHz, Turbo 2.53GHz, 3MB L3)
内存容量:2GB (DDR3-1066)

  机器B——
DELL Latitude E6320 
处理器:Intel i3-2310M(2.1GHz, 3MB L3)
内存容量:4GB (DDR3-1333,双通道)

  测试环境——
A_2005:机器A,VS2005,Window 7 32位。
A_2010:机器A,VS2010,Window 7 32位。
B_2005:机器B,VS2005,Window XP SP3 32位。
B_2010:机器B,VS2010,Window XP SP3 32位。
B64_2005:机器B,VS2005,Window 7 64位(x64)。
B64_2010:机器B,VS2010,Window 7 64位(x64)。

经过反复思索,考虑 类、结构体、接口、泛型 的组合,我找出了15种函数调用模式——
硬编码
静态调用
调用派生类
调用结构体
调用基类
调用派生类的接口
调用结构体的接口
基类泛型调用派生类
基类泛型调用基类
接口泛型调用派生类
接口泛型调用结构体
接口泛型调用结构体引用
接口泛型调用基类
接口泛型调用派生类的接口
接口泛型调用结构体的接口

图片 1

其实步骤很简单——
1.重新打开VS2005。或关闭解决方案。
2.新建一个“Windows应用程序”项目。如“TryPointerCallns2005”。
3.添加项目,选择上次的“TryPointerCall2005”。现在解决方案中就有两个项目了。
4.选择第2步时新建的项目(TryPointerCallns2005),添加引用,将“TryPointerCall2005”加上去。
5.调整项目属性,允许不安全的代码。
6.新建一个类(TestCall),将测试代码全部Copy过来(注意只复制测试代码,不复制IPointerCall、PointerCall等实现)。
7.修改窗口界面,调用TestCall的测试代码。

三、硬编码与静态调用 的测试结果(栈变量、栈分配、64位整数)

  因为硬编码与静态调用很可能会被执行函数展开优化,速度明显比其他测试项目要快。所我另外写了一个测试函数(TryItStatic),将循环次数设为原来的10倍。

  测试结果如下(单位:毫秒)——

模式 A_2005 A_2010 B_2005 B_2010 B64_2005 B64_2010
硬编码.栈变量: 1608 1623 957 966 960 959
硬编码.栈分配: 1612 1617 1073 957 961 960
硬编码.静态: 1609 1613 957 971 961 960
静态调用: 1608 1611 1063 958 961 963
64硬编码.栈变量: 1610 1617 967 957 959 1010
64硬编码.栈分配: 1610 1619 1034 957 960 1012
64硬编码.静态: 1609 1618 999 996 957 1010
64静态调用: 1610 1615 959 1002 957 7696

  结果分析——
先看32位与64位的区别。发现在大多数情况,32位与64位的速度是一样的。唯一就是64位整数运算代码在“64位平台+VS2010”上运行时,速度比在32位下还慢,尤其是静态调用慢了好几倍,硬编码代码的速度也有所下降。真的很奇怪,既然运行的是同一份程序,为什么64位比32位还慢,难道是.Net 4.0在x64平台上的即时编译器的问题?不解。
栈变量、栈分配、静态变量的访问速度几乎一致,看来可以放心地随意使用。

  看来以后写指针操作代码时,只写64位整数版就行了。

测试代码为—— 

运行“IL 反汇编程序”,打开编译后的exe。展开节点,双击叶子节点查看MSIL代码——

图片 2

四、密封类 的测试结果

  测试结果如下(单位:毫秒)——
模式 A_2005 A_2010 B_2005 B_2010 B64_2005B64_2010
硬编码.栈变量: 162 162 95 95 96 95
硬编码.栈分配: 161 161 95 95 95 97
硬编码.静态: 161 165 97 95 97 95
静态调用: 161 163 95 95 96 97
64硬编码.栈变量: 161161989596 100
64硬编码.栈分配: 160162959795 100
64硬编码.静态: 162 162 95 97 95 100
64静态调用: 161 161 95 95 97 770
调用派生类: 563 568 670 668 676 580
调用密封类: 161 162 101 103 102 767
调用结构体: 163 161 116 102 191 772
调用基类: 566 573 668 660 675 577
调用派生类的接口: 727 731 767 862 862 770
调用密封类的接口: 721 730 957 862 870 771
调用结构体的接口: 104511341318134013441253
基类泛型调用派生类: 910795127478912561287
基类泛型调用基类: 902 785 1092 676 1346 1250
接口泛型调用派生类: 1407733163486216331633
接口泛型调用密封类: 1405808173395617431638
接口泛型调用结构体: 5661606711018641250
接口泛型调用结构体引用: 48016170098769961
接口泛型调用基类: 1409728176776416311635
接口泛型调用派生类的接口: 1410727170296617301634
接口泛型调用密封类的接口: 1402808171995816351637
接口泛型调用结构体的接口: 161711281859149922082117

  将测试结果重新排版一下,突出不同实现方法的速度区别——

环境 分类 基类 派生类 密封类 结构体 结构体的引用
A_2005 直接调用 566 563 161 163  
  接口调用   727 721 1045  
  基类约束泛型调用 902 910      
  接口约束泛型调用   1407 1405 566 480
  接口约束泛型调用接口 1409 1410 1402 1617  
A_2010 直接调用 573 568 162 161  
  接口调用   731 730 1134  
  基类约束泛型调用 785 795      
  接口约束泛型调用   733 808 160 161
  接口约束泛型调用接口 728 727 808 1128  
B_2005 直接调用 668 670 101 116  
  接口调用   767 957 1318  
  基类约束泛型调用 1092 1274      
  接口约束泛型调用   1634 1733 671 700
  接口约束泛型调用接口 1767 1702 1719 1859  
B_2010 直接调用 660 668 103 102  
  接口调用   862 862 1340  
  基类约束泛型调用 676 789      
  接口约束泛型调用   862 956 101 98
  接口约束泛型调用接口 764 966 958 1499  
B64_2005 直接调用 675 676 102 191  
  接口调用   862 870 1344  
  基类约束泛型调用 1346 1256      
  接口约束泛型调用   1633 1743 864 769
  接口约束泛型调用接口 1631 1730 1635 2208  
B64_2010 直接调用 577 580 767 772  
  接口调用   770 771 1253  
  基类约束泛型调用 1250 1287      
  接口约束泛型调用   1633 1638 1250 961
  接口约束泛型调用接口 1635 1634 1637 2117  

  综合来看,密封类的性能最好,在大多数测试项目中名列前茅——
“直接调用”时能被内联(inline)优化,与“硬编码”一样快,快于派生类。
“接口调用”、“泛型调用接口”时与派生类性能一致,快于结构体的“接口调用”。
唯一就是在“泛型调用”时,落后于结构体,与派生类差不多稍微慢一点。
再就是奇怪的“64位平台+VS2010”问题,密封类、结构体在直接调用时,还不如派生类。

  最后总结一下可能会被内联优化的调用类型——
32位平台+VS2005:调用密封类、调用结构体。
32位平台+VS2010:调用密封类、调用结构体、接口约束泛型调用结构体。
64位平台+VS2005:调用密封类、调用结构体。
64位平台+VS2010:(无)。

(完)

测试程序exe——

源代码下载——

目录——
C#类与结构体究竟谁快——各种函数调用模式速度评测:
再探C#类与结构体究竟谁快——考虑栈变量、栈分配、64位整数、密封类:
三探C#类与结构体究竟谁快——MSIL(微软中间语言)解读:
四探C#类与结构体究竟谁快——跨程序集(assembly)调用:

using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;

namespace TryPointerCall
{
    /// <summary>
    /// 指针操作接口
    /// </summary>
    public interface IPointerCall
    {
        /// <summary>
        /// 指针操作
        /// </summary>
        /// <param name="p">源指针</param>
        /// <returns>修改后指针</returns>
        unsafe byte* Ptr(byte* p);
    }

#region 非泛型
    /// <summary>
    /// [非泛型] 指针操作基类
    /// </summary>
    public abstract class PointerCall : IPointerCall
    {
        public abstract unsafe byte* Ptr(byte* p);
    }

    /// <summary>
    /// [非泛型] 指针操作派生类: 指针+偏移
    /// </summary>
    public class PointerCallAdd : PointerCall
    {
        /// <summary>
        /// 偏移值
        /// </summary>
        public int Offset = 0;

        public override unsafe byte* Ptr(byte* p)
        {
            return unchecked(p + Offset);
        }
    }

    /// <summary>
    /// [非泛型] 指针操作结构体: 指针+偏移
    /// </summary>
    public struct SPointerCallAdd : IPointerCall
    {
        /// <summary>
        /// 偏移值
        /// </summary>
        public int Offset;

        public unsafe byte* Ptr(byte* p)
        {
            return unchecked(p + Offset);
        }
    }

#endregion

#region 泛型
    // !!! C#不支持将整数类型作为泛型约束 !!!
    //public abstract class GenPointerCall<T> : IPointerCall where T: int, long
    //{
    //    public abstract unsafe byte* Ptr(byte* p);

    //    void d()
    //    {
    //    }
    //}

#endregion

#region 全部测试
    /// <summary>
    /// 指针操作的一些常用函数
    /// </summary>
    public static class PointerCallTool
    {
        private const int CountLoop = 200000000;    // 循环次数

        /// <summary>
        /// 调用指针操作
        /// </summary>
        /// <typeparam name="T">具有IPointerCall接口的类型。</typeparam>
        /// <param name="ptrcall">调用者</param>
        /// <param name="p">源指针</param>
        /// <returns>修改后指针</returns>
        public static unsafe byte* CallPtr<T>(T ptrcall, byte* p) where T : IPointerCall
        {
            return ptrcall.Ptr(p);
        }
        public static unsafe byte* CallClassPtr<T>(T ptrcall, byte* p) where T : PointerCall
        {
            return ptrcall.Ptr(p);
        }
        public static unsafe byte* CallRefPtr<T>(ref T ptrcall, byte* p) where T : IPointerCall
        {
            return ptrcall.Ptr(p);
        }

        // C#不允许将特定的结构体作为泛型约束。所以对于结构体只能采用上面那个方法,通过IPointerCall接口进行约束,可能会造成性能下降。
        //public static unsafe byte* SCallPtr<T>(T ptrcall, byte* p) where T : SPointerCallAdd
        //{
        //    return ptrcall.Ptr(p);
        //}

        private static int TryIt_Static_Offset;
        private static unsafe byte* TryIt_Static_Ptr(byte* p)
        {
            return unchecked(p + TryIt_Static_Offset);
        }
        /// <summary>
        /// 执行测试 - 静态调用
        /// </summary>
        /// <param name="sOut">文本输出</param>
        private static unsafe void TryIt_Static(StringBuilder sOut)
        {
            TryIt_Static_Offset = 1;

            // == 性能测试 ==
            byte* p = null;
            Stopwatch sw = new Stopwatch();
            int i;
            unchecked
            {
                #region 测试
                // 硬编码
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = p + TryIt_Static_Offset;
                }
                sw.Stop();
                sOut.AppendLine(string.Format("硬编码:t{0}", sw.ElapsedMilliseconds));

                // 静态调用
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = TryIt_Static_Ptr(p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("静态调用:t{0}", sw.ElapsedMilliseconds));

                #endregion // 测试
            }
        }

        /// <summary>
        /// 执行测试 - 非泛型
        /// </summary>
        /// <param name="sOut">文本输出</param>
        private static unsafe void TryIt_NoGen(StringBuilder sOut)
        {
            // 创建
            PointerCallAdd pca = new PointerCallAdd();
            SPointerCallAdd spca;
            pca.Offset = 1;
            spca.Offset = 1;

            // 转型
            PointerCall pca_base = pca;
            IPointerCall pca_itf = pca;
            IPointerCall spca_itf = spca;

            // == 性能测试 ==
            byte* p = null;
            Stopwatch sw = new Stopwatch();
            int i;
            unchecked
            {
                #region 调用
                #region 直接调用
                // 调用派生类
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = pca.Ptr(p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("调用派生类:t{0}", sw.ElapsedMilliseconds));

                // 调用结构体
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = spca.Ptr(p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("调用结构体:t{0}", sw.ElapsedMilliseconds));

                #endregion  // 直接调用

                #region 间接调用
                // 调用基类
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = pca_base.Ptr(p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("调用基类:t{0}", sw.ElapsedMilliseconds));

                // 调用派生类的接口
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = pca_itf.Ptr(p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("调用派生类的接口:t{0}", sw.ElapsedMilliseconds));

                // 调用结构体的接口
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = spca_itf.Ptr(p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("调用结构体的接口:t{0}", sw.ElapsedMilliseconds));

                #endregion  // 间接调用

                #endregion  // 调用

                #region 泛型调用

                #region 泛型基类约束
                // 基类泛型调用派生类
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = CallClassPtr(pca, p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("基类泛型调用派生类:t{0}", sw.ElapsedMilliseconds));

                // 基类泛型调用基类
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = CallClassPtr(pca_base, p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("基类泛型调用基类:t{0}", sw.ElapsedMilliseconds));

                #endregion // 泛型基类约束

                #region 泛型接口约束 - 直接调用
                // 接口泛型调用派生类
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = CallPtr(pca, p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("接口泛型调用派生类:t{0}", sw.ElapsedMilliseconds));

                // 接口泛型调用结构体
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = CallPtr(spca, p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("接口泛型调用结构体:t{0}", sw.ElapsedMilliseconds));

                // 接口泛型调用结构体引用
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = CallRefPtr(ref spca, p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("接口泛型调用结构体引用:t{0}", sw.ElapsedMilliseconds));

                #endregion  // 直接调用

                #region 间接调用
                // 接口泛型调用基类
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = CallPtr(pca_base, p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("接口泛型调用基类:t{0}", sw.ElapsedMilliseconds));

                // 接口泛型调用派生类的接口
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = CallPtr(pca_itf, p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("接口泛型调用派生类的接口:t{0}", sw.ElapsedMilliseconds));

                // 接口泛型调用结构体的接口
                sw.Reset();
                sw.Start();
                for (i = 0; i < CountLoop; ++i)
                {
                    p = CallPtr(spca_itf, p);
                }
                sw.Stop();
                sOut.AppendLine(string.Format("接口泛型调用结构体的接口:t{0}", sw.ElapsedMilliseconds));

                #endregion  // 间接调用

                #endregion  // 泛型调用

            }
        }

        /// <summary>
        /// 执行测试 - 泛型
        /// </summary>
        /// <param name="sOut">文本输出</param>
        private static unsafe void TryIt_Gen(StringBuilder sOut)
        {
            // !!! C#不支持将整数类型作为泛型约束 !!!
        }

        /// <summary>
        /// 执行测试
        /// </summary>
        public static string TryIt()
        {
            StringBuilder sOut = new StringBuilder();
            sOut.AppendLine("== PointerCallTool.TryIt() ==");
            TryIt_Static(sOut);
            TryIt_NoGen(sOut);
            TryIt_Gen(sOut);
            sOut.AppendLine();
            return sOut.ToString();
        }
    }
#endregion

}

图片 3

二、测试环境

编译器——
VS2005:Visual Studio 2005 SP1。
VS2010:Visual Studio 2010 SP1。
采用上述编译器编译为Release版程序,最大速度优化。

二、结果分析

  编译器——
VS2005:Visual Studio 2005 SP1。
采用上述编译器编译为Release版程序,最大速度优化。

机器A——
HP CQ42-153TX
处理器:Intel Core i5-430M(2.26GHz, Turbo 2.53GHz, 3MB L3)
内存容量:2GB (DDR3-1066)

然后我们将测试函数调用的那行代码复制提取出来。如上图的“IL_004c”行。
在复制提取过程中,发现VS2005与VS2010生成的函数调用代码是完全一样的。删除啰嗦的名称空间,将结果整理为表格——

  机器——
HP CQ42-153TX
处理器:Intel Core i5-430M(2.26GHz, Turbo 2.53GHz, 3MB L3)
内存容量:2GB (DDR3-1066)

机器B——
DELL Latitude E6320
处理器:Intel i3-2310M(2.1GHz, 3MB L3)
内存容量:4GB (DDR3-1333,双通道)

模式 MSIL 亮点
静态调用 call       uint8*  TryIt_Static_Ptr(uint8*) 静态函数
调用派生类 callvirt   instance uint8*  PointerCall::Ptr(uint8*) 虚方法
调用密封类 callvirt   instance uint8*  SldPointerCallAdd::Ptr(uint8*) 虚方法
调用结构体 call       instance uint8*  SPointerCallAdd::Ptr(uint8*) 方法(非虚)
调用基类 callvirt   instance uint8*  PointerCall::Ptr(uint8*) 虚方法
调用派生类的接口 callvirt   instance uint8*  IPointerCall::Ptr(uint8*) 虚方法
调用密封类的接口 callvirt   instance uint8*  IPointerCall::Ptr(uint8*) 虚方法
调用结构体的接口 callvirt   instance uint8*  IPointerCall::Ptr(uint8*) 虚方法
基类泛型调用派生类 call       uint8*  CallClassPtr<class PointerCallAdd>(!!0, uint8*) class
基类泛型调用基类 call       uint8*  CallClassPtr<class PointerCall>(!!0, uint8*) class
接口泛型调用派生类 call       uint8* CallPtr<class  PointerCallAdd>(!!0, uint8*) class
接口泛型调用密封类 call       uint8* CallPtr<class  SldPointerCallAdd>(!!0, uint8*) class
接口泛型调用结构体 call       uint8*  CallPtr<valuetype SPointerCallAdd>(!!0, uint8*) valuetype
接口泛型调用结构体引用 call       uint8*  CallRefPtr<valuetype SPointerCallAdd>(!!0&, uint8*) valuetype
接口泛型调用基类 call       uint8* CallPtr<class  PointerCall>(!!0, uint8*) class
接口泛型调用派生类的接口 call       uint8* CallPtr<class  IPointerCall>(!!0, uint8*) class
接口泛型调用密封类的接口 call       uint8* CallPtr<class  IPointerCall>(!!0, uint8*) class
接口泛型调用结构体的接口 call       uint8* CallPtr<class  IPointerCall>(!!0, uint8*) class

三、测试结果

测试环境——
A_2005:机器A,VS2005,Window 7 32位。
A_2010:机器A,VS2010,Window 7 32位。
B_2005:机器B,VS2005,Window 7 64位(x64)。
B_2010:机器B,VS2010,Window 7 64位(x64)。
B_2010xp:机器B,VS2010,Window XP SP3 32位。

观察上面的表格,我们发现——
1.编译的IL代码时,并没有做内联(inline。将子函数展开)优化,而根据语义统统编译为不同的调用(call)。看来优化工作是JIT(即时编译器)负责的。
2.调用结构体是 方法调用(call instance)。JIT可根据此信息安排内联优化。
3.调用派生类是 虚方法调用(callvirt instance)。因为被编译为 调用基类的虚方法(PointerCall::Ptr),所以JIT认为其是正常的虚方法调用,不优化。
4.调用密封类是 虚方法调用(callvirt instance),与派生类调用一致。但由于其留下了类型信息(SldPointerCallAdd::Ptr),JIT发现它是一个密封类,于是安排内联优化。
5.泛型方法虽然也是用call指令,但它带有泛型参数,所以其行为与普通call调用不同。
6.结构体调用泛型方法时,会使用valuetype关键字。JIT可根据此信息安排优化(VS005的JIT有所优化;而VS2010的JIT将其进行彻底的内联优化)。

  测试结果如下——
== PointerCallTool.TryIt() ==
硬编码.栈变量: 162
硬编码.栈分配: 168
硬编码.静态: 164
静态调用: 160
64硬编码.栈变量: 161
64硬编码.栈分配: 160
64硬编码.静态: 160
64静态调用: 162
调用派生类: 1118
调用密封类: 1049
调用结构体: 1069
调用基类: 1128
调用派生类的接口: 1132
调用密封类的接口: 1135
调用结构体的接口: 1453
基类泛型调用派生类: 1769
基类泛型调用基类: 1774
接口泛型调用派生类: 2230
接口泛型调用密封类: 2240
接口泛型调用结构体: 1215
接口泛型调用结构体引用: 1127
接口泛型调用基类: 2241
接口泛型调用派生类的接口: 2230
接口泛型调用密封类的接口: 2244
接口泛型调用结构体的接口: 2302

测试结果(单位:毫秒)——

 

  可见,在跨程序集调用时,调用密封类、调用结构体不会被即时编译器(JIT)做内联优化,而被当做普通的虚方法调用。

模式 A_2005 A_2010 B_2005 B_2010 B_2010xp
硬编码 163 162 23 24 95
静态调用 162 161 23 23 95
调用派生类 570 487 456 487 606
调用结构体 162 160 95 620 100
调用基类 565 571 453 513 874
调用派生类的接口 810 728 779 708 929
调用结构体的接口 1052 1055 1175 1175 1267
基类泛型调用派生类 975 568 1055 1148 671
基类泛型调用基类 984 569 1055 1152 671
接口泛型调用派生类 1383 729 1346 1531 1062
接口泛型调用结构体 566 162 767 1149 107
接口泛型调用结构体引用 487 164 752 816 100
接口泛型调用基类 1378 812 1337 1535 1072
接口泛型调用派生类的接口 1376 810 1338 1533 1102
接口泛型调用结构体的接口 1542 1133 2486 2013 1365

附录A、转为接口时的IL代码

  所以,对于那些要求性能的代码,应尽量保证在同一程序集内,避免将其放在类库工程,这样能尽可能地利用即时编译器(JIT)内联优化的机会。

结果分析——
先看第1列数据(A_2005),发现“静态调用”、“调用结构体”与“硬编码”的时间几乎一致,很可能做了函数展开优化。其次最快的是“接口泛型调用结构体引用”,比“接口泛型调用结构体”快了16%。但是“接口泛型调用结构体的接口”最慢,“调用结构体的接口”也比较慢。其他的基于类的调用模式的速度排在中间。而且发现泛型方法速度较慢。
然后看第2列数据(A_2010),发现“接口泛型调用结构体”、“接口泛型调用结构体引用”也与“硬编码”的时间几乎一致,表示它们也是做了函数展开优化的,看来在VS2010中不需要使用ref优化结构体参数。“调用结构体的接口”、“接口泛型调用结构体的接口”两个都成了垫底。泛型方法的速度有了很大的提高,几乎与非泛型调用速度相当。
再看第3列数据(B_2005),并与第1列(A_2005)进行比较,发现“静态调用”与“硬编码”的时间几乎一致,而“调用结构体”要慢一些。“接口泛型调用结构体”、“接口泛型调用结构体引用”比较慢,排在了“调用基类”、“调用派生类”的后面。可能是64位环境(x64)的特点吧。
再看第4列数据(B_2010),并与第3列(B_2005)进行比较,发现大部分变慢了,尤其是结构体相关的,难道VS2010的x64性能还不如VS2005?我将平台改为“x64”又编译了一次,结果依旧。
再看第5列数据(B_2010xp),发现32位WinXP下的大部分项目比64位Win7下要快,真诡异。而且发现“静态调用”、“调用结构体”与“硬编码”的时间几乎一致,看来“调用结构体”一直是被函数展开优化的,而64位下的静态调用有着更深层次的优化,所以比不过。

派生类转为接口——
  IL_001d:  ldloc.0
  IL_001e:  stloc.s    V_4

(完)

我觉得在要求性能的情况下,使用结构体封装指针操作比较好,因为直接调用时会做函数展开优化,大多数情况下与硬编码的性能一致。在遇到需要一些灵活功能时,可考虑采用“接口泛型调用结构体引用”的方式,速度有所下降。接口方式最慢,尽可能不用。一定要用接口的话,应优先选择非泛型版。

密封类转为接口——
  IL_0020:  ldloc.1
  IL_0021:  stloc.s    V_5

源代码下载——

(完)

结构体转为接口——
  IL_0023:  ldloc.2
  IL_0024:  box        TryPointerCall.SPointerCallAdd
  IL_0029:  stloc.s    V_6

目录——
C#类与结构体究竟谁快——各种函数调用模式速度评测:
再探C#类与结构体究竟谁快——考虑栈变量、栈分配、64位整数、密封类:
三探C#类与结构体究竟谁快——MSIL(微软中间语言)解读:
四探C#类与结构体究竟谁快——跨程序集(assembly)调用:

测试程序exe——

可见结构体转为接口时多了装箱操作,影响了性能。

附录B、结构体泛型调用的IL代码

源代码下载——

接口泛型调用结构体——
  IL_0391:  ldloc.2
  IL_0392:  ldloc.s    V_7
  IL_0394:  call       uint8* TryPointerCall.PointerCallTool::CallPtr<valuetype TryPointerCall.SPointerCallAdd>(!!0,uint8*)

接口泛型调用结构体引用——
  IL_03dd:  ldloca.s   V_2
  IL_03df:  ldloc.s    V_7
  IL_03e1:  call       uint8* TryPointerCall.PointerCallTool::CallRefPtr<valuetype TryPointerCall.SPointerCallAdd>(!!0&,uint8*)

目录——
C#类与结构体究竟谁快——各种函数调用模式速度评测:
再探C#类与结构体究竟谁快——考虑栈变量、栈分配、64位整数、密封类:
三探C#类与结构体究竟谁快——MSIL(微软中间语言)解读:
四探C#类与结构体究竟谁快——跨程序集(assembly)调用:

可见泛型调用的IL代码并不复杂,与普通调用基本一样,也是先将参数放入堆栈再call。对于引用参数,将“ldloc.*”指令换成“ldloca.s”指令就行了。

(完)

 

目录——
C#类与结构体究竟谁快——各种函数调用模式速度评测:
再探C#类与结构体究竟谁快——考虑栈变量、栈分配、64位整数、密封类:
三探C#类与结构体究竟谁快——MSIL(微软中间语言)解读:
四探C#类与结构体究竟谁快——跨程序集(assembly)调用:

本文由奇幻电玩城游戏下载发布于工程材料,转载请注明出处:跨程序集,类与结构体究竟谁快

关键词: