Я архитектор, долгое время проектировал здания и сооружения, но вот с лета прошлого года начал программировать на C# используя Revit API. У меня уже есть несколько модулей-надстроек для Revit и теперь я хочу поделиться некоторым опытом разработки для Revit. Предполагается, что читатели умеют писать макросы для Revit на C#.

Недавно исследуя многопоточные вычисления пришел к некоторым неожиданным для себя выводам и избавился от заблуждений связанных с работой Revit API. В этой статье я напишу как ускорить работу макросов в несколько раз, какие ограничения при работе с параллельными вычислениями у Revit API, что бы наконец не оставалось вопросов.

В прошлый раз, когда я писал о параллельных вычислениях, я решал абстрактную задачу, рассматривая работу вычислений в многопоточном режиме. Сейчас рассмотрим следующую практическую задачу:

  1. Выделим несколько сотен стен, найдем середину каждой стены в плане.
  2. Проверим расстояние между средними точками всех стен, и найдем две наиболее близко расположенные стены относительно их центров.
  3. Выполним эту задачу несколькими способами и определим самый быстрый с точки зрения скорости вычислений.


Выполняя параллельные вычисления, мы можем работать и непосредственно с объектами Revit API (в данном случае с Wall), но при этом нам надо помнить о двух обстоятельствах:

  1. Мы не можем открывать сразу несколько параллельных транзакций. Одновременно в Ревит может быть открыта только одна транзакция, а создавая транзакции при параллельных вычислениях мы одновременно создадим несколько параллельных транзакций.
  2. Внутри транзакций нельзя изменять объекты Revit API в параллельном режиме. Поэтому всегда отделяйте обработку данных объектов Revit API и трансформацию проекта.


Казалось бы можно приступить к тесту, но есть еще одно обстоятельство. Выполняя некоторые вычисления в цикле с объектами Ревит, важно не допустить повторного вычисления промежуточных значений (в нашем случае вычисление средней точки стены). Как оказывается, это очень затратная операция, которую обязательно надо кэшировать.

Мы же пойдем дальше в исследованиях и создадим класс, который даже будет предварительно получать свойства объектов Revit API, то есть кэшировать их. Кроме того наш класс сможет кэшировать значение средней точки стены. Что же, теперь приступим.
Сначала создадим макрос WallTesting. Не забудем добавить пару библиотек, необходимых для работы с параллельными вычислениями.

using System.Threading.Tasks; // Библиотека для работы параллельными задачами
using System.Collections.Concurrent; //Библиотека, содержащая потокобезопасные коллекции


И теперь создадим кэширующий класс, который будет предварительно получать необходимые для работы свойства объектов стен.

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 - x.X), (pCenter.Y - x.Y), (pCenter.Z - x.Z)); //Находим вектор между средней точкой первой стены и второй стены
                      
        return vector.GetLength(); //Находим длину вектора между средними точками двух стен      
    }    
}    


В общем состав кэширующего класса и его работа расписана в комментариях. Теперь можно написать метод WallWithCashParallel, который будет работать с нашим кэширующим классом MyCashWall и сможет выполнять наши задачи в параллельном или последовательном режиме, да еще мы сможет выбрать, стоит ли кэшировать вычисление средней точки.

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();                                        
}


В приложенном файле вы еще найдете методы WorkWithWall и WorkWithWallCashValue.
Метод WorkWithWall решает наши задачи в параллельном и последовательном режиме, работает непосредственно с объектами Revit API, но не кэширует вычисление средней точки стены.

Метод WorkWithWallCashValue тоже решает наши задачи в параллельном и последовательном режиме, работает непосредственно с объектами Revit API, но этот метод кэширует вычисление средней точки стены.

Теперь создадим главный рабочий метод 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)

       );                                       
}


Теперь работа завершена, остается создать примерно 2000 стен, запустить макрос у увидеть, как он работает. Я не делал обработчик исключения, на тот случай, если стены не выделены перед запуском макроса. Так, что не забудьте сначала выделить стены.

image

Выводы

В общем можно сделать достаточно интересные выводы. Возьмем за эталон, с которым все будем сравнивать, последовательную обработку объектов в цикле без всякого кэширования промежуточных результатов.

Очевидно, что выполнять обработку массивов элементов, не сохраняя при этом результаты промежуточных вычислений (точки середины стены) неблагоразумно. При этом скорость параллельной обработки непосредственно элементов Revit API по сравнению с простой последовательной обработкой элементов Revit API будет падать при возрастании количества обрабатываемых элементов в цикле. Разница достигает до 6 раз при обработке цикла из 2000 стен.

Если мы кэшируем значения промежуточных вычислений (предварительно сохраним значения средней точки стены), мы уже получим солидную прибавку к скорости, вычисления станут быстрее в 414 и 170 раз при параллельной и последовательной обработке стен массива из 2000 стен.

Если мы потратим немного больше времени, что бы создать классы кеширующие свойства элементов Revit API, то получим также солидный выигрыш в производительности — в 212 и 80 раз при параллельной и последовательной обработке собственных классов. Однако необходимость при каждом проходе цикла вычислять среднюю точку стен остается узким местом такого решения.
Но если уж вы сделаете классы, которые будут кэшировать свойства объектов Ревит, то тогда просто необходимо сделать кэширование и промежуточных значений вычислений. При работе таких классов в параллельном и последовательном режиме разница по сравнению с простой последовательной обработкой элементов Revit API — в 354 и 127 раз.

Заключение

В большинстве случаев достаточно продумать как следует код и просто не допустить повторных вычислений одних и тех же значений. Параллельные вычисления помогут сделать такой код еще в 2 раза быстрее, но это имеет значение, если вы хотите обработать в цикле больше нескольких тысяч объектов или провести сложные вычисления. Очевидно, не стоит распараллеливать вычисления, если в цикле будет всего пару десятков объектов.

Если вы собираетесь обработать большое количество объектов Revit API, то создание кэширующих классов, которые сохранят и свойства объектов и промежуточные значения, не даст большего роста производительности по сравнению с работой напрямую с объектами Revit API (при условии кэширования промежуточных вычислений у объектов Revit API). Но при таком подходе, возможно, легче будет написать код для вычисления промежуточных результатов.

PS: Если хотите поэкспериментировать и сами понаблюдать, как при разном количестве выделенных стен ведут себя разные методы, я приложил файлы с примерами. Это файл «Test2000Wall.rvt» в котором находится 2000 стен с расстоянием друг от друга 1000мм (в осях). Справа сверху расстояние между стенами в осях 700мм.

Файл «TestParallelWall.cs» — это готовый макрос для тестов. Этот макрос обрабатывает 2000 стен примерно за 12 минут. Очевидно, что не стоит экспериментировать с обработкой массивов элементов, не сохраняя при этом результаты промежуточных вычислений. Для этого был создан макрос «TestLightParallelWall.cs» у которого удален метод WorkWithWall. Этот макрос обрабатывает 2000 стен за несколько секунд.

bim3d.ru/fileadmin/images/user_upload/Test2000Wall.rvt
bim3d.ru/fileadmin/images/user_upload/TestParallelWall.cs
bim3d.ru/fileadmin/images/user_upload/TestLightParallelWall.cs

PS.PS: Первая версия этой статьи содержала разные неточности и даже заблуждения относительно параллельных вычислений в Ревит. Прошу прошения и выкладываю этот более точный вариант статьи.
Поделиться с друзьями
-->

Комментарии (2)


  1. trir
    14.02.2017 19:09

    «После проверим расстояние между центрами стенам, и найдем две наиболее близко расположенные стены относительно их центров.» — а можно просто построить Диаграмму Вороного, даже по отрезкам


    1. Akunets
      15.02.2017 01:14

      Можно было, но поиск расстояния между центрами был второстепенной задачей. Главное было проверить какие особенности есть у Ревита при работе с параллельными вычислениями и сравнить быстродействие всех, и даже заведомо медленных решений.