Руководство программиста
Глава 2. Создание моделей блоков
§2.5. Статические переменные блоков
§2.5.3. Доступ к матрицам и массивам
Описываются особенности работы с матрицами и массивами: их размещение в памяти, макросы для доступа к ним, сервисные функции для изменения размера матриц. Приводится пример блока, умножающего вход-матрицу на вход-число и выдающего результат на выход.
Работать с массивами и матрицами несколько сложнее, чем с простыми переменными. Простые переменные имеют фиксированный размер, в то время как размер массивов и матриц может изменяться в процессе расчета. По этой причине в дереве переменных блока хранятся не сами ячейки матрицы, а указатель на динамически отводимый блок данных переменного размера, в котором они находятся (рис. 20).
Рис. 20. Размещение в памяти данных матрицы
Данные матрицы занимают в дереве переменных блока 16 байтов. Первые 8 байтов отведены под указатель на область данных матрицы, в которой хранятся ее текущий размер и элементы. В тридцатидвухбитной версии RDS этот указатель занимает только первые 4 байта отведенной восьмибайтной области, остальные 4 байта не используются. В шестидесятичетырехбитной версии указатель занимает все восемь байтов. Вторые 8 байтов данных матрицы содержат служебный указатель на блок управления матрицей, модель блока не должна изменять его. Область данных матрицы отводится динамически только в том случае, если матрица не пустая, то есть ее размер не 0×0 (для пустых матриц поле указателя на данные содержит NULL). Эта область начинается с двух четырехбайтовых целых чисел, указывающих текущий размер матрицы, за которыми последовательно, строка за строкой, записываются ее элементы. Размер элемента зависит от типа переменной. Например, если переменная определена как «матрица double», каждый ее элемент будет представлять собой вещественное число двойной точности (double) и занимать 8 байтов.
Для того, чтобы получить значение заданного элемента матрицы, функция модели сначала должна проверить матрицу на пустоту. Если указатель на данные матрицы равен NULL, то матрица пуста, и дальнейшие вычисления не имеют смысла. Если же указатель ссылается на какую-то область памяти, необходимо считать из нее размеры матрицы, и, при необходимости, проверить, существует ли в матрице элемент с указанными индексами. После этого смещение в байтах к элементу матрицы, находящемуся в строке r и столбце c, можно вычислить по следующей формуле:
(r × число_столбцов + с ) × размер_элемента + 8
Поскольку элементы матрицы хранятся в памяти построчно, для того, чтобы получить номер первого элемента строки r, необходимо умножить r на число элементов в строке, то есть на число столбцов. Затем к получившемуся значению добавляется номер элемента в строке с, в результате чего получается порядковый номер элемента [r,c] в массиве элементов матрицы. Остается умножить этот номер на размер элемента, чтобы получить смещение в байтах, и добавить 8, чтобы пропустить первые 8 байтов, занимаемые данными о размере матрицы (4 + 4).
Для изменения размера или очистки матрицы следует использовать сервисную функцию rdsResizeVarArray. Она позволяет изменять размеры матриц без потери текущих данных: при указании размера, большего текущего, новые строки и столбцы дописываются в конец матрицы и заполняются значением по умолчанию, указанным для матрицы в редакторе переменных (см. рис. 9), при уменьшении размера матрица обрезается справа и снизу. Если же вызвать эту функцию, указав размер 0×0, матрица будет очищена.
Массив в RDS – это матрица с числом строк, равным 1, поэтому данные массивов хранятся в памяти точно так же, как и данные матриц.
Для работы с массивами и матрицами модель блока может использовать два способа: один из них более простой, другой – более быстрый. Здесь будут рассмотрены оба.
Чтобы упростить написание функции модели можно использовать сервисную функцию rdsGetVarArrayAccessData, которая записывает всю информацию о матрице в специальную структуру RDS_ARRAYACCESSDATA. Эта структура имеет следующие поля:
- BOOL Exists – принимает значение TRUE, если матрица содержит элементы, и FALSE, если она пуста;
- int Rows – число строк в матрице;
- int Cols – число столбцов в матрице;
- int ItemSize – размер элемента матрицы в байтах;
- LPVOID Data – указатель на первый элемент матрицы.
Вместе с этой структурой можно использовать макрос RDS_ARRAYITEM, с помощью которого можно обращаться к элементу по двум индексам. Например, чтобы обратиться к элементу [r,c] переменной типа «матрица double», параметры которой считаны в структуру с именем str, можно написать RDS_ARRAYITEM(double,&str,r,c).
В качестве примера рассмотрим блок, который умножает каждый элемент входной матрицы вещественных чисел X на константу k и выдает результат на выход Y. С учетом обязательных сигналов Start и Ready структура переменных блока будет выглядеть следующим образом:
| Смещение | Имя | Тип | Размер | Вход/выход |
|---|---|---|---|---|
| 0 | Start | Сигнал | 1 | Вход |
| 1 | Ready | Сигнал | 1 | Выход |
| 2 | X | Матрица double | 16 | Вход |
| 18 | k | double | 8 | Вход |
| 26 | Y | Матрица double | 16 | Выход |
Функция модели блока будет выглядеть так:
extern "C" __declspec(dllexport) int RDSCALL TestMatr(int CallMode, RDS_PBLOCKDATA BlockData, LPVOID ExtParam) { // Макроопределения для статических переменных #define pStart ((char *)(BlockData->VarTreeData)) #define Start (*((char *)(pStart))) #define Ready (*((char *)(pStart+RDS_VSZ_S))) #define pX ((void **)(pStart+2*RDS_VSZ_S)) #define k (*((double *)(pStart+2*RDS_VSZ_S+RDS_VSZ_M))) #define pY ((void **)(pStart+2*RDS_VSZ_S+RDS_VSZ_M+RDS_VSZ_D)) // Структуры с информацией о матрицах RDS_ARRAYACCESSDATA XD,YD; switch(CallMode) { // Проверка типа переменных case RDS_BFM_VARCHECK: if(strcmp((char*)ExtParam,"{SSMDDMD}")==0) return RDS_BFR_DONE; return RDS_BFR_BADVARSMSG; // Выполнение такта моделирования case RDS_BFM_MODEL: // Считать информацию о матрице X в структуру XD rdsGetVarArrayAccessData(pX,&XD); if(XD.Exists) // Матрица X существует { // Задать размер Y и считать информацию о ней в YD rdsResizeVarArray(pY,XD.Rows,XD.Cols,FALSE,&YD); // Присвоить значения элементам Y for(int r=0;r<XD.Rows;r++) for(int c=0;c<XD.Cols;c++) RDS_ARRAYITEM(double,&YD,r,c)= k*RDS_ARRAYITEM(double,&XD,r,c); } else // Матрица X не существует // Очистить матрицу Y rdsResizeVarArray(pY,0,0,FALSE,NULL); break; } return RDS_BFR_DONE; // Отмена макроопределений #undef pY #undef k #undef pX #undef Ready #undef Start #undef pStart } //=========================================
Макроопределения для этой модели отличаются от предыдущих примеров. Для матриц X и Y вместо определений для доступа к самим переменным вводятся определения для указателей pX и pY. В языке C нет стандартного типа, описывающего конструкцию, аналогичную матрице RDS, поэтому создать макроопределение для самой переменной-матрицы невозможно (при желании, можно создать класс в C++, который будет описывать матрицу RDS и облегчать обращение к ее элементам при помощи своих функций-членов). Сервисные функции RDS, обслуживающие матрицы и массивы, работают именно с указателями, поэтому такое описание вполне оправдано. В модели также используются локальные переменные-структуры типа RDS_ARRAYACCESSDATA, в которые будет записываться информация о матрицах.
При вызове этой модели с параметром RDS_BFM_VARCHECK производится сравнение переданной строки типа переменных блока со строкой «{SSMDDMD}». Первые две буквы «S» соответствуют двум обязательным сигналам, «MD» – первой матрице double (X), «D» – вещественной переменной k, и завершающие буквы «MD» – матрице Y. Сравнив эти строки, модель возвращает RDS соответствующую константу: RDS_BFR_DONE, если строки совпали (переменные имеют правильный тип), и RDS_BFR_BADVARSMSG, если они отличаются (пользователю будет выведено сообщение о недопустимой структуре переменных блока).
При вызове модели с параметром RDS_BFM_MODEL сначала вызывается сервисная функция rdsGetVarArrayAccessData, записывающая информацию о матрице X в локальную структуру XD. Если матрица X существует (XD.Exists имеет значение TRUE), формируется выходная матрица Y. Для этого сначала размер Y делается равным размеру X при помощи вызова сервисной функции rdsResizeVarArray. Кроме указателя на изменяемую матрицу (pY) и нового числа строк и столбцов, этой функции передается значение FALSE, указывающее на то, что старые данные матрицы Y можно не сохранять, и указатель на локальную структуру YD, в которую будет записана информация об измененной матрице Y. В данном примере функция rdsResizeVarArray будет вызываться даже тогда, когда размеры матриц Y и X совпадают. Это не приведет к дополнительным задержкам, поскольку эта функция изменяет размер матрицы только тогда, когда это необходимо. Можно было бы вызывать ее только при несовпадении размеров, но это потребовало бы дополнительного вызова rdsGetVarArrayAccessData для выяснения размера Y и не дало бы выигрыша в быстродействии.
После того, как размер матрицы Y установлен, каждому ее элементу присваивается значение произведения переменной k и соответствующего элемента матрицы X. Для перебора элементов матриц служат два цикла, вложенные один в другой: внешний цикл по переменной r (номер строки), изменяющейся от 0 до XD.Rows-1, и внутренний цикл по переменной c (номер столбца), изменяющейся от 0 до XD.Cols-1. Для обращения к элементам матриц служит макрос RDS_ARRAYITEM, которое используется и в левой, и в правой части выражения. В этом макросе вычисляется смещение к заданному элементу матрицы с использованием данных вспомогательной структуры RDS_ARRAYACCESSDATA. Например, текст
RDS_ARRAYITEM(double,&XD,r,c)
эквивалентен выражению
* (double*) ( ((char*)XD.Data) + (r*XD.Cols + c) * XD.ItemSize )
К указателю на первый элемент матрицы XD.Data, приведенному к указателю на однобайтовый тип char, добавляется вычисленное смещение к элементу [r,c] в байтах, после чего указатель приводится к типу double*, чтобы можно было работать с вещественными переменными.
Все указанные действия выполняются только тогда, когда матрица X существует, то есть имеет ненулевые размеры. Если же она не существует (XD.Exists имеет значение FALSE), матрица Y очищается вызовом rdsResizeVarArray с указанием нулевого числа строк и столбцов.
При создании блоков, работающих с массивами и матрицами в режиме расчета, следует помнить, что операции с матрицами могут занимать достаточно продолжительное время, особенно при больших размерах матриц. Поэтому для таких блоков крайне нежелательно устанавливать флаг срабатывания в каждом такте расчета. Вместо этого следует вызывать модель таких блоков только при срабатывании входных связей. Для описанного блока, например, следует на вкладке «» окна параметров (рис. 5) включить запуск по сигналу, после чего в окне редактирования переменных (рис. 9) задать для переменной Start начальное значение 1 и установить флаг «» для входов X и k. Таким образом, модель блока будет вызвана при первом запуске расчета для вычисления начального значения выхода (сигнал запуска Start в этот момент будет иметь значение 1, после чего автоматически сбросится) и при каждом поступлении на входы X и k новых значений. Во всех остальных случаях она вызываться не будет, не тратя тем самым процессорное время на повторные вычисления значения выхода при неизменных входах.
Использование структур RDS_ARRAYACCESSDATA делает текст программы более читаемым, но это не самый быстрый способ работы с матрицами. Хотя вызов rdsGetVarArrayAccessData выполняется достаточно быстро, всю информацию о матрице можно получить и без него. Изменим приведенную выше модель блока, убрав из нее локальные переменные XD и YD. Реакция модели на выполнение одного такта моделирования теперь будет выглядеть следующим образом:
// Выполнение такта моделирования case RDS_BFM_MODEL: if(RDS_ARRAYEXISTS(pX)) // Матрица X существует { // Вспомогательные переменные int xr,xc; double *ydata,*xdata; // Получить размеры матрицы X xr=RDS_ARRAYROWS(pX); xc=RDS_ARRAYCOLS(pX); // Задать размер Y равным размеру X rdsResizeVarArray(pY,xr,xc,FALSE,NULL); // Получить указатель на первый элемент X xdata=(double*)RDS_ARRAYDATA(pX); // Получить указатель на первый элемент Y ydata=(double*)RDS_ARRAYDATA(pY); // Присвоить значения элементам Y for(int r=0;r<xr;r++) for(int c=0;c<xc;c++) ydata[r*xc+c]=k*xdata[r*xc+c]; } else // Матрица X пуста rdsResizeVarArray(pY,0,0,FALSE,NULL); break;
Как и в предыдущем варианте примера, сначала необходимо проверить, существует ли матрица X. Для этого используется макрос RDS_ARRAYEXISTS, который возвращает FALSE, если указатель на область данных матрицы равен NULL, то есть если в матрице нет элементов (поскольку в данном примере в определении pX указатель уже приведен к типу void**, вместо RDS_ARRAYEXISTS(pX) можно было просто написать *pX!=NULL). Далее, если матрица существует, число ее строк присваивается вспомогательной переменной xr при помощи макроса RDS_ARRAYROWS, а число столбцов – переменной xc при помощи RDS_ARRAYCOLS. Оба этих макроса нельзя использовать для пустых матриц, поскольку они считывают размеры из области данных, которая у пустых матриц отсутствует. Например, текст
RDS_ARRAYROWS(pX)
преобразуется в
*( *( (RDSINT32**)pX ) )
В этом выражении указатель pX приводится к типу «указатель на указатель на RDSINT32». Данные, на которые ссылается указатель pX, в свою очередь также являются указателем, указывающим на область данных матрицы, первые 4 байта которой представляют собой целое число (RDSINT32) – число строк матрицы (см. рис. 20). Если бы матрица была пуста, выполнение операции *((RDSINT32**)pX) дало бы значение NULL, и попытка получить данные по этому указателю привела бы к возникновению ошибки.
После того, как размеры матрицы X считаны, размеры Y делаются такими же при помощи уже описанного вызова rdsResizeVarArray, только в данном случае вместо указателя на структуру RDS_ARRAYACCESSDATA функции передается значение NULL – вспомогательные структуры теперь не используются. Далее указатели на первый элемент матриц X и Y присваиваются локальным переменным xdata и ydata при помощи макроса RDS_ARRAYDATA. Поскольку элементы обеих матриц – вещественные числа двойной точности (double), оба указателя приводятся к типу double*, что позволяет использовать адресную арифметику и обращаться к элементам матриц как к элементам одномерных массивов xdata и ydata. В результате всех этих присвоений вспомогательные переменные будут ссылаться на матрицу X так, как показано на рис. 21.
Рис. 21. Матрица X и вспомогательные переменные модели
Переменная ydata ссылается на первый элемент матрицы Y аналогичным образом. Вспомогательные переменные для размеров Y не вводятся, т.к. после вызова rdsResizeVarArray они должны быть равны xr и xc.
Далее, как и в первом варианте модели, в двух циклах производится вычисление элементов матрицы Y. Для получения индекса элемента в одномерных массивах xdata и ydata, соответствующего элементу матриц [r,c], номер строки r умножается на число элементов в строке xc (что дает индекс первого элемента строки r), после чего к нему добавляется номер столбца c.
Можно заметить, что в обоих вариантах этого примера все элементы матрицы X обрабатываются одинаково – каждый из них умножается на одно и то же число k независимо от номера элемента. Поскольку все элементы матриц хранятся в памяти последовательно, в данном случае можно упростить модель, заменив два цикла по номеру строки и столбца одним, перебирающим все элементы общего массива элементов размером xr × xc:
// Присвоить значения элементам Y for(int i=0;i<xr*xc;i++) ydata[i]=k*xdata[i];
Если бы номера строки и столбца были важны для вычисления элементов матрицы Y (например, если после умножения на k необходимо было бы транспонировать результат), замена двух циклов на один была бы невозможна.
Для проверки этой модели можно подключить к созданному блоку три стандартных блока: поле ввода к входу k, редактор матриц к входу X и блок отображения матриц к выходу Y (рис. 22). При запущенном расчете любые изменения, внесенные в окно редактора матриц или в поле ввода, должны немедленно отражаться на выходной матрице.
Рис. 22. Тестирование блока умножения матрицы на константу
Чтобы не загромождать этот пример, в тексте модели мы не проверяем логическое значение, возвращаемое функцией rdsResizeVarArray. В настоящих моделях блоков такая проверка необходима. Если размер матрицы изменить не удалось (например, из-за нехватки памяти), функция возвращает FALSE. Если при этом модель, не проверив возвращенное значение, будет обращаться к элементам матрицы, под которые не удалось отвести память, вероятнее всего произойдет ошибка общей защиты. Также в этом примере не производится сравнение k и элементов матрицы X со специальным значением, возвращаемым функцией rdsGetHugeDouble, которое используется для сигнализации об ошибке. В модели, оперирующей вещественными числами двойной точности, такая проверка позволяет избежать возникновения исключений при выполнении арифметических операций.