I am an architect, for a long time I designed buildings and structures, but since last summer I began programming in C # using the Revit API. I already have several add-on modules for Revit and now I want to share some development experiences for Revit. It is assumed that readers can write macros for Revit in C #.
Recently, exploring multi-threaded calculations came to some unexpected conclusions and got rid of the misconceptions associated with the work of Revit API. In this article I will write how to speed up the work of macros several times, what are the limitations when working with parallel computing in Revit API, so that finally there are no questions left.
The last time I wrote about parallel computing, I solved an abstract problem, considering the work of computing in multi-threaded mode. Now consider the following practical task:
- Select a few hundred walls, find the middle of each wall in the plan.
- Check the distance between the middle points of all the walls, and find the two most closely spaced walls relative to their centers.
- Perform this task in several ways and determine the fastest in terms of computation speed.
')
When performing parallel computations, we can work directly with Revit API objects (in this case, with Wall), but at the same time, we need to keep two things in mind:
- We cannot open several parallel transactions at once. At the same time, only one transaction can be opened in Revit, and by creating transactions for parallel computing, we will simultaneously create several parallel transactions.
- Inside transactions, you cannot modify Revit API objects in parallel mode. Therefore, always separate the data processing of Revit API objects and the transformation of the project.
It would seem that you can proceed to the test, but there is another circumstance. When performing some calculations in a loop with Revit objects, it is important to prevent the intermediate values from being recalculated (in our case, the calculation of the midpoint of the wall). As it turns out, this is a very expensive operation that you definitely need to cache.
We will go further in the studies and create a class that will even get the properties of Revit API objects first, that is, cache them. In addition, our class will be able to cache the value of the midpoint of the wall. Well, let's get started now.
First, create a WallTesting macro. Do not forget to add a couple of libraries needed to work with parallel computing.
using System.Threading.Tasks; // using System.Collections.Concurrent; //,
And now we will create a caching class that will first obtain the properties of wall objects necessary for the operation.
public class MyCacheWall // Wall { private LocationCurve wallLine; // , private XYZ p1 = new XYZ(); // private XYZ p2 = new XYZ(); // public XYZ pCenter; // , , public MyCacheWall (LocationCurve WallLine) // { this.wallLine = WallLine; // - } public XYZ GetPointCenter (bool cash) //, , // { if (cash) // { if (pCenter == null) { p1 = wallLine.Curve.GetEndPoint(0); p2 = wallLine.Curve.GetEndPoint(1); return pCenter = new XYZ((p2.X + p1.X)/2, (p2.Y + p1.Y)/2, (p2.Z + p1.Z)/2);// 9 } else return pCenter; } else // { p1 = wallLine.Curve.GetEndPoint(0); p2 = wallLine.Curve.GetEndPoint(1); return pCenter = new XYZ((p2.X + p1.X)/2, (p2.Y + p1.Y)/2, (p2.Z + p1.Z)/2);// 9 } } public double GetLenght (XYZ x) //, { XYZ vector = new XYZ((pCenter.X - xX), (pCenter.Y - xY), (pCenter.Z - xZ)); // return vector.GetLength(); // } }
In general, the composition of the caching class and its work is painted in the comments. Now we can write the WallWithCashParallel method, which will work with our caching class MyCashWall and will be able to perform our tasks in parallel or sequential mode, and we will also be able to choose whether to cache the midpoint calculation.
string WorkWithWallCashParallel(Document doc, ICollection<ElementId> selectedIds, bool cash, bool parallel) // cash , // parallel { List<MyCacheWall> wallList = new List<ThisApplication.MyCacheWall>(); // - Wall List <double> minPoints = new List<double>(); // ConcurrentBag <double> minPointsBag = new ConcurrentBag<double>(); // , DateTime end; // DateTime start = DateTime.Now; // foreach (ElementId e in selectedIds) // Wall MyWall. // , , { Element el = doc.GetElement(e); // Id Wall w = el as Wall; //, if (w != null) // - { wallList.Add( new MyCacheWall (w.Location as LocationCurve)); // MyCacheWall Wall } } if (parallel) // { System.Threading.Tasks.Parallel.For(0, wallList.Count, x => // MyWall // (). { List <double> allLenght = new List<double>(); // wallList[x].GetPointCenter(cash); // . foreach (MyCacheWall nn in wallList) // () { double n = wallList[x].GetLenght( nn.GetPointCenter(cash) ); if (n != 0) // allLenght.Add(n); // } allLenght.Sort(); // minPointsBag.Add(allLenght[0]); // });// minPoints.AddRange(minPointsBag); // , } else { for(int x = 0; wallList.Count > x; x++) // MyWall // (). { List <double> allLenght = new List<double>(); // wallList[x].GetPointCenter(cash); // . foreach (MyCacheWall nn in wallList) // () { double n = wallList[x].GetLenght( nn.GetPointCenter(cash) ); if (n != 0) // allLenght.Add(n); // } allLenght.Sort(); // minPoints.Add(allLenght[0]); // }// } minPoints.Sort(); // double minPoint = minPoints[0]; // end = DateTime.Now; // TimeSpan ts = (end - start); return ts.TotalMilliseconds.ToString() + " . " + "\n. - " + (minPoint*304.8).ToString(); }
In the attached file, you will also find the WorkWithWall and WorkWithWallCashValue methods.
The WorkWithWall method solves our problems in parallel and sequential mode, works directly with Revit API objects, but does not cache the calculation of the midpoint of the wall.
The WorkWithWallCashValue method also solves our problems in parallel and sequential mode, it works directly with Revit API objects, but this method caches the calculation of the midpoint of the wall.
Now we will create the main working method WallTesting:
public void WallTesting () { Document doc = this.Document; // Selection selection = this.Selection; // ICollection<ElementId> selectedIds = this.Selection.GetElementIds(); // TaskDialog.Show("Revit", "*** ***\n" +"\n , - " + WorkWithWall(doc, selectedIds, true) +"\n , - " + WorkWithWall(doc, selectedIds, false) +"\n\n*** ***\n" +"\n , - " + WorkWithWallCashValue(doc, selectedIds, true) +"\n , - " + WorkWithWallCashValue(doc, selectedIds, false) +"\n\n*** ***\n" +"\n , - " + WorkWithWallCashParallel(doc, selectedIds, false, true) +"\n , - " + WorkWithWallCashParallel(doc, selectedIds, false, false) +"\n\n*** ***\n" +"\n , - " + WorkWithWallCashParallel(doc, selectedIds, true, true) +"\n , - " + WorkWithWallCashParallel(doc, selectedIds, true, false) ); }
Now the work is completed, it remains to create about 2000 walls, run the macro and see how it works. I did not make an exception handler, in case the walls are not highlighted before running the macro. So, do not forget to select the walls first.
findingsIn general, we can draw quite interesting conclusions. Let us take the benchmark with which we will compare everything, sequential processing of objects in a loop without any caching of intermediate results.
It is obvious that it is unreasonable to process arrays of elements without saving the results of intermediate calculations (midpoint of the wall). At the same time, the speed of parallel processing of Revit API elements directly compared to simple sequential processing of Revit API elements will decrease with an increase in the number of processed elements in the loop. The difference reaches up to 6 times when processing a cycle of 2000 walls.
If we cache the values of intermediate calculations (we preserve the values of the middle point of the wall), we will already get a substantial increase in speed, the calculations will become faster by 414 and 170 times with parallel and sequential processing of the walls of an array of 2000 walls.
If we spend a little more time to create classes that cache the properties of the Revit API elements, we will also get a solid performance gain — 212 and 80 times with parallel and sequential processing of our own classes. However, the need to calculate the average point of the walls at each cycle pass remains a bottleneck of such a solution.
But if you make classes that cache the properties of Revit objects, then you just need to make caching and intermediate values of the calculations. When such classes work in parallel and sequential mode, the difference compared to simple sequential processing of Revit API elements is 354 and 127 times.
ConclusionIn most cases, it is enough to think through the code properly and simply not to allow repeated calculations of the same values.
Parallel computations will help to make such code 2 times faster, but it matters if you want to cycle more than a few thousand objects or perform complex computations in a loop. Obviously, it is not necessary to parallelize the calculations if there are only a couple of dozens of objects in the loop.
If you are going to process a large number of Revit API objects, creating caching classes that preserve both object properties and intermediate values will not give a greater performance increase compared to working directly with Revit API objects (provided that intermediate calculations are cached for Revit API objects). But with this approach, it may be easier to write code to calculate intermediate results.
PS: If you want to experiment and see for yourself how different methods behave with different numbers of selected walls, I attached sample files. This is the file "Test2000Wall.rvt" in which there are 2000 walls with a distance of 1000mm from each other (in the axes). Top right, the distance between the walls in the axes of 700mm.
The file "TestParallelWall.cs" is a ready macro for tests. This macro processes 2,000 walls in approximately 12 minutes. Obviously, you should not experiment with processing arrays of elements without saving the results of intermediate calculations. To do this, a macro "TestLightParallelWall.cs" was created with the WorkWithWall method deleted. This macro processes 2,000 walls in a few seconds.
bim3d.ru/fileadmin/images/user_upload/Test2000Wall.rvtbim3d.ru/fileadmin/images/user_upload/TestParallelWall.csbim3d.ru/fileadmin/images/user_upload/TestLightParallelWall.csPS.PS: The first version of this article contained various inaccuracies and even misconceptions regarding parallel computations in Revit. I beg your petition and post this more accurate version of the article.