MicroStation CE下三种编程语言的运行效率比较


我们知道,在MicroStation CE下目前支持三种编程语言(MVBA、C# 和C++)做二次开发。那么这三种语言生成的程序最终运行效率如何呢?本文针对该问题做了一个实际测试,供大家在选择编程语言时做参考。

硬件环境:HP ZBOOK-15 ( i7-8850H CPU、32G内存、1.5T 固态硬盘)

软件环境:Windows 10 企业版 、MicroStation CONNECT Edition Update 16.2

我们用了两个不同的程序来测试运行时间的长短。一个是在默认图层上连续创建2000个圆柱体;另一个是在每个单独的图层上创建2000个圆柱体。

先看MVBA源代码:

Sub CreateCones()
   startTime = Timer
   Dim myCone As ConeElement
   Dim basePt As Point3d, topPt As Point3d
   For i = 1 To 2000
      basePt = Point3dFromXYZ(i, 0, 0)
      topPt = Point3dFromXYZ(i, 0, 1)
      Set myCone = CreateConeElement2(Nothing, 1, basePt, topPt)
      ActiveModelReference.AddElement myCone
   Next
   Debug.Print Format(Timer - startTime, "0.000s")
End Sub

Sub CreateConesWith2000Lvls()
   startTime = Timer
   Dim myLvl As Level
   Dim myCone As ConeElement
   Dim basePt As Point3d, topPt As Point3d
   For i = 1 To 2000
      Set myLvl = ActiveDesignFile.AddNewLevel("LevelTest" & i)
      basePt = Point3dFromXYZ(i, 0, 0)
      topPt = Point3dFromXYZ(i, 0, 1)
      Set myCone = CreateConeElement2(Nothing, 1, basePt, topPt)
      myCone.Level = myLvl
      ActiveModelReference.AddElement myCone
   Next
   Debug.Print Format(Timer - startTime, "0.000s")
End Sub

CreateCones完成仅用时0.053秒,可以说是瞬间完成。但出人预料的是,CreateConesWith2000Lvls却耗时98.992秒,差不多是CreateCones的1868倍,大大出乎我们的预料。推测有可能是VBA代码在后台频繁写层表导致的。

再来看C/C++代码,我们知道,在MSV8i时代,我们大都用MDL C函数来创建元素,这种方式在MSCE下仍然适用但不推荐。在MSCE下我们推荐使用新的MDL C++类的方法编程。故以下源代码分了四种情况在计时。情况1是用MDL C++在默认图层创建2000个圆柱体;情况2是用MDL C++在单独的2000个图层上每层创建一个圆柱体;情况3是用MDL C在默认图层创建2000个圆柱体;情况4是用MDL C在单独的2000个图层上每层创建一个圆柱体。它们消耗的时间分别为:0.014秒、0.123秒、0.020秒、0.144秒。总体看,用C++和用C编程时间相差很小,令人惊奇的是,C++代码居然比C还要快,这个有点超出我们的想象。深入分析源代码发现,Bentley最底层是都用C++写了,为了能使以前的C代码继续运行,反而在C函数中调用了C++封装的类。这一点解释了为何MS SDK中的C函数反而比C++慢的原因。但CreateConesWith2000Lvls的运行时间仅仅是CreateCones的7~8倍,远远低于VBA的1868倍。

void timeTest(WCharCP unparsed)
{
	long myCase = wcstol(unparsed, NULL, 10);
	double startTime = BeTimeUtilities::QuerySecondsCounter();
	switch (myCase)
	{
	case 1: // --- MDL(C++) CreateCones
		{
			DPoint3d basePt, topPt;
			for (int i = 0; i < 2000; i++)
			{
				basePt = DPoint3d::From(i * 1000, 0, 0);
				topPt = basePt; topPt.z = 1000;
				DgnConeDetail dgnCone(basePt, topPt, 1000, 1000, true);
				ISolidPrimitivePtr pSolid = ISolidPrimitive::CreateDgnCone(dgnCone);

				EditElementHandle eeh;
				DraftingElementSchema::ToElement(eeh, *pSolid, nullptr, *ACTIVEMODEL);
				eeh.AddToModel();
			}
		}
		break;
	case 2: // --- MDL(C++) CreateCones with 2000 levels
		{
			DPoint3d basePt, topPt;
			FileLevelCacheP pLvlCache = ACTIVEMODEL->GetFileLevelCacheP();
			ElementPropertiesSetterPtr propSetter = ElementPropertiesSetter::Create();
			for (int i = 0; i < 2000; i++)
			{
				basePt = DPoint3d::From(i * 1000, 0, 0);
				topPt = basePt; topPt.z = 1000;
				DgnConeDetail dgnCone(basePt, topPt, 1000, 1000, true);
				ISolidPrimitivePtr pSolid = ISolidPrimitive::CreateDgnCone(dgnCone);

				EditElementHandle eeh;
				DraftingElementSchema::ToElement(eeh, *pSolid, nullptr, *ACTIVEMODEL);
				WPrintfString lvlName(L"Level %d", i + 1);
				EditLevelHandle lvlHandle = pLvlCache->CreateLevel(lvlName.GetWCharCP(), LEVEL_NULL_CODE, LEVEL_NULL_ID);
				propSetter->SetLevel(lvlHandle->GetLevelId());
				propSetter->Apply(eeh);
				eeh.AddToModel();
			}
		}
		break;
	case 3: // --- MDL(C) CreateCones
		{
			DPoint3d basePt, topPt;
			basePt.y = basePt.z = topPt.y = 0;
			topPt.z = 1000;
			for (int i = 0; i < 2000; i++)
			{
				basePt.x = topPt.x = i * 1000;
				mdlCone_create(dgnBuf, NULL, 1000, 1000, &basePt, &topPt, NULL);
				mdlElement_add(dgnBuf);
			}
		}
		break;
	default: // --- MDL(C) CreateCones with 2000 levels
		{
			DPoint3d basePt, topPt;
			basePt.y = basePt.z = topPt.y = 0;
			topPt.z = 1000;
			LevelId lvlId;
			for (int i = 0; i < 2000; i++)
			{
				basePt.x = topPt.x = i * 1000;
				mdlCone_create(dgnBuf, NULL, 1000, 1000, &basePt, &topPt, NULL);
				WPrintfString lvlName(L"Level %d", i + 1);
				mdlLevel_create(&lvlId, ACTIVEMODEL, lvlName.GetWCharCP(), LEVEL_NULL_CODE);
				mdlElement_setProperties(dgnBuf, &lvlId, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
				mdlElement_add(dgnBuf);
			}
		}
		break;
	}
	double endTime = BeTimeUtilities::QuerySecondsCounter();
	WPrintfString wStr(L"Case %d time = %.3f seconds", myCase, endTime - startTime);
	mdlDialog_dmsgsPrint(wStr);
}

最后来测试一下C# 程序的执行效率。C# 编程有两套完全不同的对象模型:一套是基于VBA的,我们叫做Addin Interop(互操作),它的编程思路和VBA完全类似,仅仅是换了一种语言的写法而已;另外一套是MSCE中才能用的.NET编程,它是对MDL C++类的封装,所以,编程思路和MDL C++类似。针对两种对象模型和CreateCones、CreateConesWith2000Lvls两种情况也是四种情况。最终的测试结果为:情况1(Addin Interop,CreateCones)耗时0.070秒;情况2(Addin Interop,CreateConesWith2000Lvls)耗时102.413秒;情况3(Addin .NET,CreateCones)耗时0.021秒;情况4(Addin .NET,CreateConesWith2000Lvls)耗时0.136秒。

public static void TimeTestForCreate(string unparsed)
        {
            DateTime beforDT = DateTime.Now;
            switch (unparsed)
            {
                case "1":  // ---- Interop Create Cone in default level
                    {
                        BIM.Application app = BMI.Utilities.ComApp;
                        BIM.Point3d basePt, topPt;
                        BIM.ConeElement myCone;
                        for(int i = 0; i< 2000; i++)
                        {
                            basePt = app.Point3dFromXYZ(i, 0, 0);
                            topPt = app.Point3dFromXYZ(i, 0, 1);
                            myCone = app.CreateConeElement2(null, 1, basePt, topPt);
                            app.ActiveModelReference.AddElement(myCone);
                        }
                    }
                    break;
                case "2":  // ---- Interop Create Cone in 2000 levels
                    {
                        BIM.Application app = BMI.Utilities.ComApp;
                        BIM.Point3d basePt, topPt;
                        BIM.ConeElement myCone;
                        BIM.Level myLvl;
                        for (int i = 0; i < 2000; i++)
                        {
                            basePt = app.Point3dFromXYZ(i, 0, 0);
                            topPt = app.Point3dFromXYZ(i, 0, 1);
                            myCone = app.CreateConeElement2(null, 1, basePt, topPt);
                            myLvl = app.ActiveDesignFile.AddNewLevel("LevelTest" + i);
                            myCone.let_Level(myLvl);
                            app.ActiveModelReference.AddElement(myCone);
                        }
                    }
                    break;
                case "3":  // ---- .NET Create Cone in default level
                    {
                        DgnModel dgnModel = Session.Instance.GetActiveDgnModel();
                        DPoint3d basePt, topPt;
                        DVector3d vec0 = new DVector3d(1, 0, 0), vec90 = new DVector3d(0, 1, 0);
                        for (int i = 0; i < 2000; i++)
                        {
                            basePt = new DPoint3d(i * 1000, 0, 0);
                            topPt = basePt; topPt.Z = 1000;
                            DgnConeDetail dgnCone = new DgnConeDetail(basePt, topPt, vec0, vec90, 1000, 1000, true);
                            SolidPrimitive solid = SolidPrimitive.CreateDgnCone(dgnCone);
                            Element elem = DraftingElementSchema.ToElement(dgnModel, solid, null);
                            elem.AddToModel();
                        }
                    }
                    break;
                default:   // ---- .NET Create Cone in 2000 levels
                    {
                        DgnModel dgnModel = Session.Instance.GetActiveDgnModel();
                        DPoint3d basePt, topPt;
                        DVector3d vec0 = new DVector3d(1, 0, 0), vec90 = new DVector3d(0, 1, 0);
                        FileLevelCache lvlCache = dgnModel.GetFileLevelCache();
                        ElementPropertiesSetter setter = new ElementPropertiesSetter();
                        for (int i = 0; i < 2000; i++)
                        {
                            basePt = new DPoint3d(i * 1000, 0, 0);
                            topPt = basePt; topPt.Z = 1000;
                            DgnConeDetail dgnCone = new DgnConeDetail(basePt, topPt, vec0, vec90, 1000, 1000, true);
                            SolidPrimitive solid = SolidPrimitive.CreateDgnCone(dgnCone);
                            Element elem = DraftingElementSchema.ToElement(dgnModel, solid, null);
                            EditLevelHandle lvlHandle = lvlCache.CreateLevel("Level" + i);
                            setter.SetLevel(lvlHandle.LevelId);
                            setter.Apply(elem);
                            elem.AddToModel();
                        }
                    }
                    break;
            }
            DateTime afterDT = DateTime.Now;
            TimeSpan ts = afterDT.Subtract(beforDT);
            CommonClass.ShowMessage("Case " + unparsed + ": "+ ts.TotalSeconds + "second");
        }
    }

测试结果:

编程方法MVBAAddin-InteropAddin-.NETMDL-CMDL-C++
CreateCone耗时(秒)0.0530.0700.0210.0200.014
CreateConeWith2000Lvls耗时(秒)98.992102.4130.1360.1440.123
倍数1867.81463.06.57.28.8
MSV8i中是否支持                   ✘         ✘
MSCE中是否支持                             

可以看出,在以上五种编程方法中,用C# 调用VBA互操作这种是最慢的,而采用C++面向对象编程是最快的。不过,Addin .NET和MDL C++这两种是我们强烈推荐的。

其实单从CreateCones函数的耗时来看,最慢的Addin-Interop方法(即通过C# 调用VBA这套COM接口)和最快的MDL-C++相比也仅仅相差5倍,而且都是在百分秒级的,基本上可以忽略不计。但CreateConeWith2000Lvls的耗时就相差太大了,经过深入分析底层代码,发现在MVBA的AddNewLevel封装中除了调用了C的mdlLevel_create函数外,它还做了大量同步和刷新的一些操作,正是这些额外操作拖慢了MVBA的执行速度,这个不是语言的问题,而是函数封装的问题。遇到此类MVBA问题,要想提高程序运行效率只能是改换后三种编程方式或者自己用COM封装一个mdlLevel_create来供VBA使用了。