我们知道,在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");
}
}
测试结果:
| 编程方法 | MVBA | Addin-Interop | Addin-.NET | MDL-C | MDL-C++ |
| CreateCone耗时(秒) | 0.053 | 0.070 | 0.021 | 0.020 | 0.014 |
| CreateConeWith2000Lvls耗时(秒) | 98.992 | 102.413 | 0.136 | 0.144 | 0.123 |
| 倍数 | 1867.8 | 1463.0 | 6.5 | 7.2 | 8.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使用了。