November 7, 2019

Subrange Types

Infosys it at Subrange Types or it looks like INT(1..42);

You can bound a variable of the integer type into the predefined range. In other words you can set the low limit and the hi limit for the integer variable. Thus you can create a new integer type. But is it realy? 

Actual for the following types only: SINT, USINT, INT, UINT, DINT, UDINT, BYTE, WORD, DWORD, LINT, ULINT, LWORD.

Let's play in the sandbox. For the clarity, I have used underscores in the names of subrange variables.

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

    pValue : PVOID;

You can do same actions with a variable of subrange type as with a variable of an integer type. In fact, this is not a new data type with a new bounds (or restrictions), but it is a suggestion or a clue to the compiler how to check some cases (I will discuss it later) or how to control variable value.
Unfortunately, you cannot do this: _analog_ : REAL (4.0 .. 20.0); and it is not about too many dots in a row but because of the inaccuracy of the floating-point types.

Truth Or Dare


Possible:

_value_ := 42;
_value_ := value;

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

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

Are not allowed:

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


Excavating Memory


So I decided to check whether or not someone controls the output of the variable value for the specified range during the operation process. I wrote zero to the variable `value` through the pointers. And zero was recorded (nothing bad happened). And then I have added 1 to the variable and suddenly, the value of the variable increased by 1. Where is the bounds? 

// _value_ = 10;

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

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

In general, if we performe cyclically _value_ := _value_ + 1; we will see the whole range of INT values from -32768 to +32767, without any restrictions from the runtime system.

Conclusion: the runtime does not control the value of the variable; it works like a regular integer type, and nothing else.


Implicit Checks


There is a whole family of POUs for implicit checks of the variable value (e.g. division by zero checks POUs):



You have to add some special POUs to the project to control data bounds during the execution:



...then choose L|Range Checks and finally rebuild project. Newcome POUs will control the variable and limits it if the value overstep the bounds defined by you. And now if you repeat the last used trick with pointers but with already activated implicit checking you can get completely different result. Runtime system will call corresponding POU that will limit variable to the predefined bounds.


Performance


FYI, performance measurements are performed to catch whether additional work is happening behind the scenes or not. There was no point in optimizing for 1-2 nanoseconds, although who knows what's going on there. Vertical axis in hundreds of nanoseconds: 83000 = 8.3 milliseconds.

Testing ground:

// HOWMUCH: DINT := 1_000_000;

CASE state OF
1,5:; // empty loop

2: // increment
    FOR i := 0 TO HOWMUCH DO
        value := value + 1;
    END_FOR

3: // const in bounds
    FOR i := 0 TO HOWMUCH DO
        _value_ := 42;
    END_FOR

4: // increment that overstep the bounds
    FOR i := 0 TO HOWMUCH DO
        _value_ := _value_ + 1;
    END_FOR


Without POUs for implicit checks:

Added POUs for implicit checks then the project was rebuilt:

Conclusion: POUs for implicit checks cause extra load.


Resume

  • In fact, this is not a new data type, just an ordinary integer type (INTBYTE, ...) with extra information about subrange.
  • Range are clue for a compiler.
  • Range controlled by extra POUs for implicit checks. This is optional.
  • Implicit checks cause extra load because of execution extra POUs.

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

Или 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 6, 2019

Производительность ReadAny и Reactive в 5.0.0-preview1

В продолжении темы измерения скорости чтения, посмотрим что там с превью версией под .NET Core 3. Заодно увидим как обстоят дела с реактивностью.


ReadAny


static void Main(string[] args)
{
    AdsSession session = new AdsSession(new AmsAddress("5.28.214.97.1.1", AmsPort.R0_RTS + 1));
    session.Connect();
    var connection = session.Connection;

    const int HOWMUCH = 1000;
    List results = new List();

    uint hMain_CycleCount = connection.CreateVariableHandle("MAIN.CycleCount");

    for (int i = 0; i < HOWMUCH; i++)
        results.Add(
            (uint)connection.ReadAny(hMain_CycleCount, typeof(uint)));

    foreach (var cycleNumber in results)
        Console.WriteLine(cycleNumber);
}


.NET Core 3, AdsRouterConsole



Для сравнения повтор картинки из предыдущего поста для .NET 4.7.1, TwinCAT.IO Router



Reactive


static void Main(string[] args)
{
    AdsSession session = new AdsSession(new AmsAddress("5.28.214.97.1.1", AmsPort.R0_RTS + 1));
    session.Connect();

    const int HOWMUCH = 1000;

    List<uint> results = new List<uint>();

    var valueObserver = Observer.Create(
        val => { // on next
            results.Add(val);
            //Console.WriteLine(val);
        },

        exc => { // on error
            Console.WriteLine(exc.Message);
        },

        () => {  //on completed
            Console.WriteLine($"Read {results.Count} cycles");

            foreach (var cycleNumber in results)
                Console.WriteLine(cycleNumber);
        });

    var subscription = session.Connection
        .WhenNotification("MAIN.CycleCount", NotificationSettings.ImmediatelyOnChange)
        .Take(HOWMUCH)
        .Subscribe(valueObserver);

    Console.WriteLine("ENTER to stop...");
    Console.ReadLine();
}


Все совсем плохо:



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

При времени цикла меньше одной секунды, перестает работать делегат отвечающий за onComplеte часть обозревателя. Именно поэтому пришлось добавить в onNext вывод промежуточных результатов.

Пока что очень сыро и сильно напрягает непоследовательность приходящих элементов.

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), что и было использовано: