Showing posts with label производительность. Show all posts
Showing posts with label производительность. Show all posts

November 7, 2019

Типы с диапазоном

Или Subrange Types типа INT(1..42);

Переменную целого типа можно вписать в заранее ограниченный диапазон. Иначе говоря, ограничить ее значение сверху и снизу. Работает только для переменных целого типа: SINT, USINT, INT, UINT, DINT, UDINT, BYTE, WORD, DWORD, LINT, ULINT, LWORD.

Поиграем в такой песочнице:

PROGRAM MAIN
VAR
    i: DINT;
    value: INT;
    _value_: INT(10..90);
    analog: REAL;

    pValue : PVOID;

Для красоты я обрамил переменную с диапазоном символами подчеркивания. С переменного такого типа можно делать все то же самое, что и с переменными целого типа. По сути это не отдельный тип данных с ограничением, а предписание компилятору по сборке кода и в некоторых случаях (расскажу позже) по контролю за значением переменной.
К сожалению нельзя сделать так: _analog_ : REAL (4.0 .. 20.0); и не потому, что точки мешают, а по причине неточности типов с плавающей точкой.

Примеряем


Что можно делать:

_value_ := 42;
_value_ := value;

_value_ := _value_ + 1;
_value_ := _value_ + 100; // O_o

_value_ := DINT_TO_INT(i);
_value_ := REAL_TO_INT(analog);

Что компилятор не пропустит:

_value_ := 0;
FOR _value_ := 11 TO 89 DO


Играем с указателями


Я решил проверить контролирует ли кто-нибудь во время работы выход значения переменной за указанный диапазон. И через указатель записал ноль в переменную value. И ноль записался (ничего страшного не произошло). А затем я прибавил к переменной единицу и... значение переменной увеличилось на 1:

// _value_ = 10;

value := 0;
pValue := ADR(_value_);
MEMCPY(pValue, ADR(value), SIZEOF(_value_));
// _value_ = 0;

_value_ := _value_ + 1;
// _value_ = 1;

Вообще, если циклически выполнять _value_ := _value_ + 1; то мы увидим весь диапазон значений INT от -32768 до +32767. Без каких-либо ограничений.

Вывод: среда исполнения не контролирует значение переменной, для нее это обычный целочисленный тип.


Неявный контроль времени исполнения


Или `implicit checks`. Это целое семейство ФБ неявного контроля значений переменных (например, ФБ контролирующие деление на ноль):



Чтобы контроль работал во время исполнения программы необходимо добавить в проект функциональные блоки специального назначения:



...выбрать L|Range Checks и пересобрать проект. Добавленные ФБ будут контролировать значение переменной и корректировать его, если значение выйдет за пределы. Если теперь повторно провернуть трюк с указателями, но уже с включенной проверкой, то при следующем обращении к переменной, среда исполнения через соответствующие ФБ исправит/впишет значение переменной в указанный при объявлении диапазон.


Перформанс


Для тех кто не в курсе, измерения производительности выполняются с целью уловить происходит ли дополнительная работа за кулисами или нет. Никакого смысла в оптимизации на 1-2 наносекундах не было и нет, хотя кто знает, что у вас там творится. По вертикали время в сотнях наносекунд: 83000 = 8.3 миллисекунды.

Испытательный полигон:

// HOWMUCH: DINT := 1_000_000;

CASE state OF
1,5:; // пустой цикл

2: // инкремент
    FOR i := 0 TO HOWMUCH DO
        value := value + 1;
    END_FOR

3: // константа, в диапазоне
    FOR i := 0 TO HOWMUCH DO
        _value_ := 42;
    END_FOR

4: // инкремент, выход из диапазона
    FOR i := 0 TO HOWMUCH DO
        _value_ := _value_ + 1;
    END_FOR


Итак, без контроля:


Добавляю контрольные ФБ и пересобираю проект:

Контролируемый выход за пределы диапазона требует дополнительной работы.


Вывод

  • Тип по сути остается обычным целочисленным типом (INT, BYTE, ...).
  • Диапазон — это рекомендация для компилятора.
  • Диапазон учитывается в рантайме только при наличии специальных ФБ, скрыто контролирующих и исправляющих значение переменной.
  • Неявный контроль требует немного дополнительной работы, что логично, так как коррекция значений требует дополнительного вызова ФБ.

November 5, 2019

InvokeRpcMethod — вызов удаленного метода

С помощью ADS можно не только передавать данные, но и вызывать методы ПЛК задачи.

TcAdsClient client = new TcAdsClient();
client.Connect(new AmsAddress("5.28.214.97.1.1", AmsPort.R0_RTS + 1));
short result = -1;
var args = new object[]{ result, (short)1, (short)2 };
try {
    result = (short)client.InvokeRpcMethod("MAIN.fbBox", "M_FuncPub", args);
}
catch { }


PROGRAM / ACTION


Проверим странное...
...оно не работает. Нет такого символа
Value cannot be null.
Parameter name: symbol
Работоспособность не зависит от размещения атрибута: что внутри экшена, что над объявлением программы, что и там, и здесь. Результат отрицательный.


FB


Action

Опять нестандартное. И оно тоже не работает.
The RPC method 'A_Func01' is not supported on symbol 'MAIN.fbBox!
Кстати, опечатка в сообщении эксепшена — не хватает кавычки.


Method

Для метода, ожидаемо, работает. Причем, внезапно, вызов метода работает даже когда Runtime в состоянии Stop. Иначе говоря, когда порт создан (851), но ПЛК задача еще не запущена.

Команда __NEW при RPC вызовах также работает, даже в состоянии "стоп", и по прежнему выжирает память роутера. Контроль памяти роутера описан в New, Delete и память роутера. Поэтому если память выделяется внутри RPC метода, а освобождается в другом месте — получится Ахтунг и протечка памяти!

Итоги:
  • Уровень доступа метода не влияет на доступность извне. Любой из PUBLIC, PRIVATE, PROTECTED, INTERNAL доступен для внешнего вызова.
  • Вызов требует наличия всех параметров объявленных в ФБ как VAR_INPUT
  • Точки останова внутри методов не срабатывают при RPC вызовах.


Насколько быстро?


Тестировать будем на CX9020, x32, TwinCAT 3.1.4022.25 и на десктопе Core i7-3630QM x64 TwinCAT 3.1.4024.0. Для интереса сравним производительность относительно обычного чтения символа.

results[i] = (uint)client.InvokeRpcMethod("MAIN.fbBox", "M_FuncPub", args);
// VS
results[i] = (uint)client.ReadSymbol("MAIN.CycleCount", typeof(uint), false);


Будем последовательно и синхронно читать по 1000 значений за раз. В случае с InvokeRpcMethod соответственно вызывать метод и получать результат. Результатом же чтения будет номер текущего цикла ПЛК задачи. При последовательном и многократном чтении, получим массив номеров циклов. Сравнивая два соседних значения и умножая разницу на время цикла ПЛК задачи, получим время затраченное на чтение.

Начну с обычного чтения ReadSymbol. По горизонтали — номер выборки. По вертикали — время в миллисекундах, затраченное на чтение символа. Цветом обозначено время цикла ПЛК-задачи. Первым будет CX9020, вторая картинка — десктоп:

ReadSymbol, CX9020



ReadSymbol, десктоп Core i7 x64


Даже "на глаз" видно, что десктоп справляется "стабильнее" и быстрее. К тому же, в случае десктопа все происходит на локалхосте и нет сетевых прослоек, привносящих дополнительные лаги. Краткий вывод: для циклов от 4 миллисекунд данные возвращаются стабильно. Меньше 4мс могут быть задержки (видимо запросы попадают на границу цикла).

Кроме этого есть запросы с нулевым временем выполнения. Это озорует роутер. При отправке запросов чаще, чем время одного цикла, роутер делает вид, что значение еще не изменилось и быстро шлет обратный ответ с тем же, старым значением. Ну и отлично.

Теперь про RPC запросы. Порядок контроллеров прежний:

RPC, CX9020



RPC, десктоп Core i7 x64


RPC выполняется дольше чем запрос данных одной переменной. Это ожидаемо, так как нужно принять запрос с аргументами, выполнить код, и только затем отправить ответ. 3-4-5 циклов, в зависимости от того, куда упадет запрос относительно границы кванта системного времени.

Правило 4мс по прежнему работает.


Выводы


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

Если же необходимо реализовать контурные режимы или передавать данные чаще чем раз в 4мс, то необходимо реализовать промежуточный буфер и отправлять несколько значений за раз, используя обычные функции для работы с переменными ReadAny, WriteSymbol, etc.


PS: Инструменты


Для проведения тестирования и рисования результатов, иногда достаточно обычного экселя (Excel), что и было использовано:




August 22, 2018

Экономия на абстракциях

Чем более крупный проект я пишу, тем больше хочется выделить что-то в отдельную подпрограмму, затем размножить это в виде функционального блока, а в дальнейшем (где-то на следующей неделе) вынести этот код в специальную библиотеку и даже написать документацию с комментариями. Иностранные коллеги озабочены паттернами программирования и ООП, я смотрю в сторону функциональщины и конечных автоматов, но разговор не об этом.

Что если разбивка на множество подпрограмм привносит дополнительную нагрузку на процессор ПЛК и снижает быстродействие?

Быстродействие важно и любые телодвижения в сторону абстракций, приводят к увеличению количества вызовов подпрограмм (функций, методов, других кусков кода). А ведь нам нужно успеть уложиться в миллисекунды базового времени (Base Time, в первую очередь), и только затем улучшить сопровождаемость (когда-нибудь потом, но лучше прямо сейчас).

Спагетти код ругают: все-таки трудно ориентироваться в бесконечной портянке неструктурированного кода непонятного назначения. Поэтому я хочу знать — имеет смысл экономить на абстракциях или я могу разбивать код на сколько угодно удобных мне частей?

Итак, исследуем влияние количества вызовов подпрограмм на загруженность процессора. Проверять TwinCAT 2 мне не очень-то и хотелось, но я проверил и не пожалел.

PROGRAM MAIN
VAR
    i: UINT;
END_VAR

FOR i := 0 TO 65000 DO
    PRG2();
END_FOR

(* ********************** *)

PROGRAM PRG2
VAR
    c : UINT;
END_VAR

;
// c := c + 1;
// c := c + 1;

Основная программа MAIN каждый цикл совершает ряд вызовов подпрограммы PRG2. Количество вызовов определяется количеством итераций цикла. Их максимальное число равно 65000 — как в примере выше. Так как нас в основном интересует нагрузка от ветвления программы, то подпрограмма PRG2 пустая. Точнее, она состоит из единственного оператора точка-с-запятой (;). Чтобы не было совсем банально, я разбавил подпрограмму парой строк со счетчиком (и это было правильным решением).

По вертикали (Y) — нагрузка процессора в процентах, по горизонтали (X) — количество вызовов подпрограммы за время одного цикла:
Ничего необычного: чем больше вызовов, тем больше нагрузка на процессор. При этом нужно учесть два факта: первый — подпрограмма PRG2 ничего не делает и нагрузка растет только за счет количества передачи управления из одной программы в другую; и второй — тест синтетический и в реальных ситуациях такого количеств вызовов за один цикл не бывает.

Теперь проверим TwinCAT 3:
...и он показывает более линейную зависимость.

Теперь об аномалиях второй версии TwinCAT. В подпрограмме PRG2 я убрал пустой оператор ";" и раскомментировал одну строку со счетчиком. Зафиксировал количество вызовов на числе 65000. Замерил нагрузку. Раскомментировал вторую строчку со счетчиком (теперь их две). Замерил нагрузку и впал в задумчивость.

Итак, внимание! С одной строкой счетчика, нагрузка составила 72%. С двумя строками счетчика — 43%. Больше кода — меньше нагрузка! Объяснить такое поведение можно только работой некоего оптимизирующего звена в компиляторе кода, но такая неявность, явно вводит в заблуждение. Я немедленно проверил то же самое в TwinCAT 3...

...и он по прежнему оказался линеен и предсказуем: 1 строка — 56%, две строки — 58%.


Выводы

  1. Вызовы подпрограмм добавляют нагрузку на процессор.
  2. Я решил, что не буду обращать на эту нагрузку внимание, так как ощутимой она становится только при каких-то очень больших числах вызовов за время одного цикла. Даже для CX8xxx серии.
  3. TwinCAT 3 стабильнее и предсказуемей, чем его вторая реализация.
  4. Upd. 29 мая 2019 года, TwinCAT 2 v2.11.2302 странности с "меньше кода - больше нагрузка" не наблюдаются. Возможно был баг с выравниванием переменных в памяти. 


P.S.: посчитаем вызовы


А как вообще прикинуть — сколько вызовов происходит в проекте?

Возьмем проект ЧПУ системы, она точно будет большой. Посчитаем скольку всего происходит вызовов подпрограмм (функций и т. п.) во всем проекте. Конечно же, за один цикл будут происходить не все вызовы, но и я собираюсь считать приблизительно, и по максимуму. Заодно отбросим вызовы, происходящие внутри библиотечных функций... что-то там в итоге должно сойтись.

Для начала, открываем проект и экспортируем его в единый файл экспорта *.exp. Затем, открываем EXP-файл в текстовом редакторе (Notepad++) и автозаменой удаляем все вхождения следующих строк: (*, STRING(, IF (. Файл в итоге будет испорчен, но для нас важнее исключить посторонние элементы. Остается сделать глобальный поиск символа открывающей скобки (, который традиционно используется при вызове подпрограмм. Как вариант, поискать закрывающие скобки, а точнее конец вызова функции, что-то похожее на );

Количество вхождений искомых символов, будет приблизительно равно количеству вызовов функций и подпрограмм. Часть совпадений придется на математические формулы, но их не так много. В то же время часть вызовов мы все равно не обнаружим, так как они скрыты внутри библиотек и в других трудно доступных местах. Одно компенсирует другое, и в среднем получится более-менее правдоподобно.


На картинке выше подсвечены не все скобки, но они учтены и подсчитаны правильно.

В итоге, я нашел 560 вызовов с открывающими скобками и 433 с закрывающими. Это в два раза меньше 1000 (1-2% CPU, как в пустой программе) и совсем далеко от 65000 (56-58% CPU). Еще раз, это максимум, который можно вызвать, но который никогда не будет достигнут: программа разбита на шаги (машина состояний), в каждом из которых выполняется лишь небольшая часть всей технологической программы.

February 7, 2018

Деление на ноль

Давно ли вы делили на ноль?



Случайные ошибки


PROGRAM MAIN
VAR
    a, b, c : INT;
END_VAR

a := 12 / 0;

Такую случайную ошибку среда разработки отловит еще на этапе компиляции:



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

PROGRAM MAIN
VAR
    a, b, c : INT;
END_VAR

a := 12 / b; // <<<< !!!!

...TwinCAT переходит с состояние "стоп" (Stop), выбрасывая в консоль сообщение об ошибке:
Error 01.02.2018 15:02:24 456 ms | 'Port_851' (851): Exception (Exception Code: 0xc0000094, Integer divide by zero) in PLC Application Untitled1 Instance, Task PlcTask (EBP: 0xa9b5eeb8, EIP: 0xa7927056, ESP: 0xa9b5ee90)

Что произойдет если у вас несколько задач (тасков)? Остановится ли только "эта" задача или все сразу? Как поведет себя система в многоядерной архитектуре? И что с изолированными (isolated) выделенными ядрами?


Изолируем и форкаем


Добавляю и запускаю вторую, параллельную задачу. И сразу же роняю первую, установив переменную b := 0. Первая задача падает в точку останова, но вторая задача продолжает работать — счетчик тикает:



Пробую восстановить задачу после падения, устранив ошибку: значение переменной b := 1, затем пытаюсь продолжить выполнение первой задачи. Не получается. Пробую подтвердить на экране ПЛК сообщение об ошибке деления на ноль (кнопка ОК). TwinCAT целиком переключается в режим конфигурирования (синий значок), но теперь уже с остановкой всех задач: и ошибочных, и корректных. При этом:
  • Reset Cold — не помогает.
  • Rest Origin — помогает, так как он начисто сносит весь рантайм задачи (это заметно по предложению пересоздать порт при последующем запуске). Затем можно перезапустить задачу заново, но непонятно, что в это время творится с другими задачами.

При всех этих телодвижениях среда разработки VS 2015 ведет себя очень нестабильно, а может быть это контроллер ведет себя некорректно. Не уверен — кто из них. Возможно оба. В надежде повысить стабильность, я выделил одно из ядер ПЛК целиком под TwinCAT: 100% производительности в печенку процессора. Ничего не меняется. Пытаюсь перезапустить ошибочную задачу, но TwinCAT переходит в режим конфигурации, с полной остановкой всех ядер и задач. Версия TwinCAT контроллера: 3.1.4020.32.

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


Отлаживаем


Для отладки и только отладки (почему, я расскажу позже) TwinCAT предлагает нам стандартное средство из проверочных функциональных блоков — Object POUs for implicit checks. Теперь их можно создавать тыкая мышкой в: POUs → Add → POU for implicit checks... → +Division Checks. После этого в ПЛК-проект добавятся несколько автоматически сгенерированных функций. Необходимо перекомпилировать проект и перезагрузить его в контроллер целиком (download). Загрузка проекта на лету (online change) с сохранением данных (значений переменных) на этот раз недоступна, так как в проекте появились новые программные объекты (POUs).

Я экспериментирую с делением целого типа, вот пример такой функции:

// Implicitly generated code : DO NOT EDIT
FUNCTION CheckDivDInt : DINT
VAR_INPUT
    divisor:DINT;
END_VAR

// Implicitly generated code : Only an Implementation suggestion
{noflow}
IF divisor = 0 THEN
    CheckDivDInt:=1;
ELSE
    CheckDivDInt:=divisor;
END_IF;
{flow}

Несмотря на предостережение: "НЕ РЕДАКТИРОВАТЬ", — редактировать можно и нужно. Например, добавить сообщение об ошибке в лог и вытащить номер текущего шага из других программных объектов.

Интересно, что эта функция будет вызываться для каждой операции деления! Значением входного параметра divisor будет значение делителя/знаменателя из операции деления, а результат функции (возвращаемое значение) будет подставляться вместо делителя в операции деления. Попробуйте в теле функции заменить CheckDivDInt:=divisor; на CheckDivDInt:=2; и все ваши операции деления, независимо от значения делителя, превратятся в банальным делитель на двойку. Но стоит только поделить на ноль, как вместо делителя (равного нулю) будет подставлена единица.

Не всегда и всюду нужна проверка, поэтому разработчики предоставили нам средство быстрого отключения функции проверки для заданных программных блоков — атрибут {attribute 'no_check'}. Его необходимо добавить в первую строку области объявления переменных, до строк PROGRAM, FUNCTION_BLOCK или FUNCTION. Тем более, что использование автопроверки вызывает дополнительную нагрузку на процессор.


Оценка производительности


Справочная система предупреждает о дополнительной нагрузке на ПЛК при использовании функций проверки деления (а также функций проверки на выход за пределы диапазона или проверки адреса указателя). Давайте измерим эту нагрузку. Для этого я написал специальный тест производительности:

FOR c := 1 TO 1000000 DO
    a := 12 / b;
END_FOR

Управлять нагрузкой будем с помощью количества циклов FOR: от 100 000 до 1 000 000 (столбец A). Непосредственно нагрузку подсмотрим в закладке Online раздела SYSTEM. Всего необходимо рассмотреть три различных случая:
  1. Без контроля деления на ноль, выставлен атрибут "no_check" — зеленый столбец B.
  2. С включенным контролем деления на ноль — желтый столбец C.
  3. С включенным контролем деления на ноль и, через установку переменной b := 0, имитируем деление на ноль в каждой итерации цикла — красный столбец D. Предполагается максимальная нагрузка.
И сразу результат. По вертикальной оси гистограммы отражена нагрузка на процессор ПЛК в процентах (80% — заданный мною потолок для TwinCAT):