Информационные технологии Вестник Нижегородского университета им». Н.И. Лобачевского, 2010, 3(1), с. 207-214
УДК 004.432:004.4'422
МАТЕМАТИЧЕСКОЕ РАСШИРЕНИЕ ЯЗЫКА ПРОГРАММИРОВАНИЯ ZONNON
© 2010 г. Н.А. Гонова \ Н.Ю. Золотых \ Р.О. Митин 2
1 Нижегородский госуниверситет им. Н.И. Лобачевского 2 Швейцарская высшая техническая школа Цюриха
nina.gonova@gmail.com
Поступила в редакцию 16.11.2009
Описано расширение объектно-ориентированного языка конструкциями для разработки математических приложений: операциями и функциями над многомерными матрицами, индексацией при помощи диапазонов и векторов. Это приводит к более наглядному, естественному и компактному коду в приложениях, реализующих алгоритмы линейной алгебры. Обсуждается практический опыт реализации математических конструкций для языка Zonnon на платформе .NET.
Ключевые слова: Zonnon, язык программирования, математическое расширение, сложная индексация, .NET, CCI, компилятор, оптимизация.
Введение
Расширенная поддержка векторно-матричных вычислений имеется в целом ряде специализированных пакетов, таких как MATLAB,
Mathematica и R [1-3], а также в языках общего назначения, например в Fortran 90, ZPL, APL и Python (NumPy) [4]. Подобные идеи математических расширений были предложены в работе [5] и реализованы для языка программирования Oberon.
Реализация математических операций над многомерными массивами на уровне языка расширяет возможности компилятора для оптимизации. В случае Fortran и Oberon в первую очередь речь идет о векторизации матричных вычислений [6]. Возможности оптимизации напрямую зависят от ограничений, накладываемых на модель программирования (к примеру, отсутствие указателей), и от целевой архитектуры (наличие векторных инструкций в целевой машине).
Zonnon - объектно-ориентированный язык программирования общего назначения из семейства языков Pascal, Modula-2 и Oberon. Существующая версия Zonnon [7] реализована для платформы Microsoft .NET и по отношению к вышеупомянутым языкам обладает важным преимуществом поддержки интероперабельности [8] с другими .NET-языками. Кроме того, Zonnon - понятный и выразительный язык, что, в совокупности, делает его очень привлекательным для реализации математических расширений.
В данной работе описывается опыт разработки и реализации математического расшире-
ния языка Zonnon на платформе .NET, которое совмещало бы в себе удобство программирования в векторно-матричном стиле и производительность компилятора. Также обсуждаются потенциальные оптимизации компилятора, специфичные для математических расширений языка.
Математические расширения
На надежность программы влияет как легкость ее чтения, так и легкость создания. При разработке математического расширения языка мы стремимся к согласованной, ясной и выдержанной концепции, которая не делает язык избыточным и позволяет уменьшить количество ловушек в программировании. Прозрачная консистентная операционная семантика в совокупности со строгой типизацией и статическими проверками компилятора призваны оградить программиста от ошибок, типично возникающих при использовании математических библиотек.
В целом можно выделить три группы введенных нами математических конструкций в Zonnon: индексы, операторы и функции. Расширения применяются к переменным специально введенного типа математических массивов.
1. Математические массивы
Главное семантическое отличие математических массивов от обычных в том, что присваивание для обычных массивов осуществляется по ссылке, в то время как в случае математических
Таблица 1
Операция Обычный массив или объект ссылочного типа Математический массив Объект-значение, элементарный тип
Присваивание По ссылке По значению По значению
Передача в подпрограмму с модификатором ¥аг Ссылка на ссылку Ссылка на ссылку Ссылка
Передача в подпрограмму без модификатора var Ссылка Ссылка на константный массив Значение
Таблица 2
Унарные операторы
Оператор Операнд Результат Значение
+ , - Массив чисел Массив чисел Поэлементная унарная операция
Логический массив Логический массив Поэлементная операция обращения
Таблица 3
Бинарные операторы
Оператор Операнды Результат Значение
+, -, *, /, div, mod Массив чисел, скаляр Массив чисел Поэлементная операция со скаляром
+, -,.*,./, div, mod Массив чисел, массив чисел Массив чисел Поэлементная бинарная операция
+ *1 Массив чисел, массив чисел Скаляр (Псевдо)скалярное произведение
V . < II . V #V < , Массив чисел, скаляр Логический массив Поэлементное сравнение со скаляром
іГ V іГ ^ A 11= V ' A Массив чисел, массив чисел Логический массив Поэлементное сравнение двух массивов
11= 11= V if V V A> Массив чисел, скаляр Вoolean Является ли результат поэлементного сравнения массивом только из истинных элементов
if if V if V V A Массив чисел, массив чисел Вoolean
Таблица 4
Оператор приведения типа
Оператор Операнд Результат Значение
<типА> Массив Массив типа <типA> Поэлементное приведение типа
1 Скалярное произведение двух одномерных массивов определяется следующим образом: х + * у = х[] * у[г]. (Псевдо)скалярное произведение двух массивов размерности п определено как
По-ЕНо Л*,.-»]
x + * у =
массивов семантически естественно присваивание по значению.
Синтаксически для обозначения математических массивов используется модификатор {math}, примененный к обычному массиву. К примеру тип, определяющий вектор из трех элементов, может быть задан так: type Vector = array {math} 3 of integer;.
Таблица 1 дает сравнение семантики операций обычных массивов и объектов ссылочного
типа, математических массивов и объектов-значений.
2. Индексы
Если a - ^мерный массив, то его срез может быть задан как a[index0, ..., index(n-1) ], где indexi может быть простым индексом, описанным в отчете по языку [9], или одним из расширенных типов индексации: диапазоном, численным векторным индексом или логическим векторным индексом.
Таблица 5
Унарные операторы
Оператор Операнд Результат Значение
! Двумерный массив чисел Двумерный массив чисел Транспонирование матрицы
Таблица 6
Бинарные операторы
Оператор Операнды Результат Значение
* Два численных массива размерности 2 или 1 Массив чисел соответствующей размерности или скаляр Матричное, матричновекторное, векторноматричное, векторновекторное произведение
/ Два численных массива размерности 2 или 1 Массив чисел Решение СЛУ (или правое деление матрицы)
\ Два численных массива размерности 2 или 1 Массив чисел Решение СЛУ (или левое деление матрицы)
Таблица 7
Функция Параметр(ы) Результат Значение
abs Массив чисел Массив чисел Поэлементная унарная операция
min, max Массив чисел Скаляр Минимальный/максимальный элемент
min, max Массив чисел, скаляр Скаляр Минимальный/максимальный элемент в соотв. размерности
sum Массив чисел Скаляр Сумма элементов
sum Массив чисел, скаляр Скаляр Сумма элементов в соотв. размерности
all Логический массив Boolean Все ли элементы истинны
all Логический массив, скаляр Boolean Все ли элементы истинны в соотв. размерности
any Логический массив Boolean Есть ли хотя бы 1 истинный элемент
any Логический массив, скаляр Boolean Есть ли хотя бы 1 истинный элемент в соотв. размерности
a) Диапазон (range) обозначается выражением вида [a]..[b] [by c], где a, b и c должны быть целочисленными константами или переменными. Диапазон a..b by c соответствует множеству
{a + i • c : i e N, 0 < i • c < b - a} .
b) Численный векторный индекс представляет собой одномерный массив любой положительной длины с целочисленными элементами. Элементы индексируемого массива (в данном измерении) с индексами из численного индексного вектора отбираются и конкатенируются в том же порядке, в котором они стояли в индексном векторе.
c) Логический векторный индекс - это одномерный массив с элементами типа boolean, длина которого равна длине индексируемого массива в соответствующем измерении. Результат содержит значения, которые соответствуют элементам, равным true, и его длина равна количеству истинных элементов.
3. Операторы
Общее правило для определения типа результата конкретной операции с массивами заключается в том, что результирующий тип со-
ответствует типу результата этой или аналогичной операции со скалярами.
Общие операторы для математических массивов приведены в таблицах 2-4.
Матричные операторы для двумерных математических массивов приведены в табл. 5, 6.
4. Дополнительные функции перечислены в табл. 7.
Примеры с использованием Zonnon
Пример 1. Приведем пример программы на языке Zonnon, которая строит фазовую траекторию динамической системы, описываемой системой дифференциальных уравнений
х = р{у - х)
'У = x(r - z) - y z = xy-bz
(аттрактор Лоренца, см., например, [10]). Интегрирование проводится методом Рунге-Кутты четвертого порядка с фиксированным шагом по времени:
type Vector = array {math} 3 of integer;
procedure {public} Process (x0: A22 = A [v, v];
Vector; tMax, dt: real); B11 = B [u, u] ;
var x, x1, k1, СП k k k4 : Vector; B12 = B [u, v];
t : real; B21 = B [v, u] ;
begin B22 = B [v, v];
x О о = t := x0 ; D1 : = Strassen(A11 +
B11 +
while t <= tMax do
k1 := f(t, x); B22) ;
k2 := f(t + dt/2 x + dt/2 * k1) ; D2 := Strassen(A12 - A22, B21
k3 := f(t + dt/2 x + dt/2 * k2); B22) ;
k4 := f(t + dt 3 k * t d + x D3 := Strassen(A11 - A21, B11
x1 := x + dt/3 * (1/2 * k1 + k2 B12) ;
+ k3 + 1/2 * k4); D4 : = Strassen(A11 + A12, B22) ;
Connect(x, x1) D5 : = Strassen(A21 + A22, B11) ;
1 x = x D6 : = Strassen(A11, B12 - B22) ;
t; d + t = t D7 : = Strassen(A22, B21 - B11) ;
end C11 := D1 + D2 - D4 + D7;
end Process; C12 := D4 + D6;
C21 := D5 + D7;
procedure {public} f (t: real, x : C22 := D1 - D3 - D5 + D6;
Vector) : Vector; C : new Matrix(n, n);
var Res : Vector; C [u, u] := C11;
begin C [u, v] := C12;
Res := [ -x[0]*p + p*x[1], C [v, u] := C21;
-x[0]*x[2] + r*x[0] - x[1], C [v, v] := C22;
x [0]*x[1] - b*x[2] ] ; end;
return Res; return C;
end f; end Strassen;
В данном примере сначала определяется тип Vector, задающий одномерный массив из трех элементов. В цикле функции Process происходит поиск приближенных координат состояния системы в очередной момент времени. Функция Connect отображает найденную точку в фазовом пространстве и соединяет ее с предыдущей.
Пример 2. Рассмотрим пример программы на языках Zonnon и MATLAB, реализующей умножение матриц методом Штрассена:
(* ZONNON *) type Matrix = array {math} *, * of real;
procedure Strassen(A : Matrix; B: Ma-
trix)
: Matrix; var C, C11, C12, C21, C22: Matrix;
A11, A12, A21, A22,
B11, B12, B21, B22: Matrix;
D1, D2, D3, D4, D5, D6, D7: Matrix; n: integer; u, v: range; begin
(* Мы предполагаем, что A и B -
матрицы размера nxn и n = 2Ak *) n : = len(A, 0) ;
if n = 1 then A * B
C
else
u
(n div 2
v : = A11 A12 A21
n div 2 = A [u, u = A[u, v = A [v, u
- 1); 1;
% MATLAB
function C = strassen(A, B)
% Мы предполагаем, что A и B -% матрицы размера nxn и n = 2Ak
n = size(A, 1) ;
if n == 1
C = A*B;
else
u = 1:(n/2);
v = (n/2 + 1) : n
A11 = A(u, u) ;
A12 = A(u, v);
A21 = A(v, u) ;
A22 = A(v, v);
B11 = B(u, u) ;
B12 = B(u, v);
B21 = B(v, u) ;
B22 = B(v, v);
D1 =
D2 = D3 = D4 = D5 = D6 = D7 = C11 C12 C21 C22 C =
strassen(A11 + A22, B11 +
B22)
A22, B21 +B22) A21, B11 +B12) B22)
B11)
B22)
B11)
strassen(A12 strassen(A11 strassen(A11 + A12, strassen(A21 + A22, strassen(A11, B12 -strassen(A22, B21 -
= D1 + D2 - D4 + D7;
= D4 + D6;
= D5 + D7;
= D1 - D3 - D5 + D6;
[C11, C12; C21, C22];
end
В примере на языке Zonnon вначале определяется тип Matrix, который задает двумерный
+
+
n
массив действительных чисел. Далее происходит разбиение матриц на блоки, рекурсивное вычисление произведений и вычисление частей искомой матрицы C.
Компилятор Zonnon
Компилятор Zonnon написан на языке C# и генерирует общепринятые сборки .NET, содержащие промежуточный код и метаданные. Библиотека Common Compiler Infrastructure (CCI) [11], предоставляемая Microsoft, используется как утилита для генерации кода и платформа для интеграции.
Библиотека CCI обеспечивает поддержку для разработки компиляторов для .NET на трех уровнях [12]:
• инфраструктура высокого уровня (структуры для построения деревьев программ и методы для осуществления семантической проверки деревьев);
• поддержка низкого уровня (генерация IL-кода и метаданных);
• сервис интеграции с Microsoft Visual Studio.
CCI не обеспечивает полной поддержки для разработки компиляторов. К примеру, лексический и синтаксический анализаторы оставлены для реализации пользователю.
В целом компилятор организован довольно традиционно: сканер трансформирует исходный текст в последовательность лексем, которые принимаются синтаксическим анализатором. Далее строится Zonnon-ориентированное программное дерево, которое затем трансформируется в дерево с узлами, представляющими собой сущность класса промежуточного представления CCI. Конечным результатом трансформаций является сборка .NET. Процесс компиляции схематично представлен на рисунке 1. Основная причина построения Zonnon-ориентированного программного дерева - разделение части компилятора, ориентированной на язык, и системно-ориентированной части компилятора.
Отображение математических расширений в .NET происходит во время конвертации дерева Zonnon в дерево CCI.
Отображение в .NET
Важным условием реализуемости в .NET для любого языка является существование отображения его конструкций в Common Language Runtime (CLR) [13]. В зависимости от парадигмы и модели, которые представляет язык, нахождение такого отображения может быть действительно сложным.
По сути нам требуется осуществить отображение математических расширений на соответствующие конструкции CLR. Возможны два основных варианта отображений:
1. Отображение осуществляется после того, как построено дерево Zonnon, и до того, как осуществляется конвертация в дерево CCI посредством детализации дерева Zonnon.
При этом отсутствует зависимость от платформы .NET, так как используются только структуры дерева Zonnon. Главный недостаток подхода - отображения ограничены возможностями платформы, представленными в языке Zonnon. Сама детализация дерева Zonnon также нарушает идею двух уровней абстракции в компиляторе, представленных ориентированным на язык деревом Zonnon и ориентированным на платформу деревом CCI.
2. Отображение происходит во время конвертации дерева Zonnon в дерево CCI с построением результата в дереве CCI. При этом подходе отображения зависят от целевой платформы, но позволяют использовать всю ее функциональность.
С учетом про- и контраргументов для отображения математических расширений был выбран второй способ.
В общем случае для реализации математической операции компилятор, используя структуры CCI, формирует соответствующую функцию, выполняющую эту операцию, и вставляет
Рис. 1. Процесс компиляции
ее вызов в нужное место в дереве программы.
К примеру, следующую конструкцию на Zonnon
a := b[m, n1..n2 by step] * k;, где a — вектор, b — матрица, k — константа, компилятор отобразит в структуру CCI, аналогичную данному предложению на C#:
a = ElementWiseSRArrayScalarMult2d<type of b><type of k>
(b, k, m, n1, n2, step);
При этом будет сгенерирована функция, аналогичная следующей функции на C#:
public static <type of result>[]
ElementWiseSRArrayScalarMult2d<type of b><type of k>
(<type of b> [,] left, <type of k> right,
Int32 n s0, Int32 n r0 from, Int32 n_r0_to, Byte n_r0_by)
{ _
Int32 i0 = new Int32(), n0 =
new Int32 ( );
n0 = ((n r0 to-n r0 from) /
n r0 by) + 1;
<type of result>[] res = new Int32[n0];
for ( i0 = 0; i0 < n0; i0 += 1)
res[i0] = left[n_s0,
n r0 from + (i0*n r0 by)] * right;
return res;
}
Отображение данной конструкции на Zonnon в структуры CCI схематично изображено на рисунке 2.
Генерируемые компилятором функции имеют уникальные имена в зависимости от типов элементов массивов и их размерности, выполняемой операции, а также дополнительных индексов. При конвертации очередной математической конструкции компилятор вначале осуществляет поиск соответствующей функции; если требуемая функция не была создана ранее, она будет сгенерирована компилятором.
Оптимизация
Одним из важных преимуществ реализации математических расширений на уровне языка, а не библиотеки, является возможность применения специфичных для математических выражений оптимизаций. Можно выделить следующие группы оптимизаций:
1. Типичные оптимизации для арифметических выражений, примененные к математическим массивам: продвижение и свертка кон-
стант, поиск общих подвыржаний, вынос инвариантов цикла.
Пример 1.
Рассмотрим следующий фрагмент кода:
x0 := A*b + c; x1 := A*b + d;
Вынос общего подвыражения A*b для базовых типов является стандартной оптимизацией для любого компилятора:
t0 := A*b; x0 := t0 + c; x1 := t0 + d;
Если же a - матрица, x0, xl, b, c, d - векторы, а операторы сложения и умножения перегружены, то компилятор в общем случае не имеет права на такую оптимизацию. Таким образом, необходима дополнительная поддержка осуществления подобных оптимизаций для математических массивов.
2. Генерация эффективного кода для составных матричных выражений: объединение операций (склеивание циклов).
Пример 2.
Присваивание x : = A*b + c, где A - матрица, x, b, c - векторы, обыч-ным преобразованием отображается в цепочку вызовов функций, аналогичную Math.Assign(x,Math.Add(Math.Multiply(A,
b), c)). В результате последовательного выполнения операций умножения и сложения будет создано два дополнительных массива, последний из которых будет присвоен в вектор x.
В свою очередь, для выражения x := A*b + c компилятор может сгенерировать более специализированную функцию, в которой не понадобятся промежуточные переменные и дополнительные циклы:
for(int i=0; i<x.Length; i++)
{
int tmp = c[i];
for(int j=0; j<b.Length; j++)
{
tmp += A[i,j]*b[j];
}
x[i] = temp;
}
3. Оптимизации использования памяти. Семантика математических массивов диктует осуществление операции присваивания по значению, что решает проблему алиасинга (здесь алиасинг означает ситуацию, при которой несколько переменных с различными символьными именами указывают на одно место в памяти). Тем не менее осуществление каждый раз глубокого копирования на практике может оказаться неэффективным. Возможные пути этого избежать включают в себя контроль владения массивом и создание копии при попытке
Дереео Zonnon
Дерек CCI
Ь Ев, Til. .п2 by step] ' /;
а" > " Ëleraeütiif ï sëSBAi ïaÿSÜi L&ËHÜItïï ' ; itype of ЬХ type a-f fc>
ibr k, iuj ni, n2f st*p) ;
Рис. 2. Дерево Zonnon и дерево CCI при отображении математических расширений
изменения. При этом контроль владения может быть выполнен статически.
Пример 3.
Пусть A, B, C - матрицы размером 100^100, объявленные как локальные переменные. Рассмотрим следующий фрагмент кода:
B := A;
D := A;
D[7,9] := B[5,7];
(* Далее в коде A и D используются для чтения, B не используется *)
Самая простая реализация присваивания по значению создала бы для B и D копии A. В свою очередь, анализ потока данных показывает, что массив B после копирования в него массива A используется только для чтения и A до окончания использования B не меняется. Таким образом, нет необходимости в B := A создавать глубокую копию, а можно осуществить присваивание по ссылке.
4. Оптимальный выбор метода в матричных операциях.
Известно, что при осуществлении матричновекторного умножения для матриц различных размеров используются разные алгоритмы. Так, для маленьких матриц (например, размером 2^2, 3x3, 4x4) даже затраты на вызов функции будут слишком высокими и в данном случае имеет смысл развернуть все вычисления в последовательность присваиваний без циклов (на .NET инструкции SSE напрямую недоступны, но мы можем надеяться, что если JIT компилятор увидит простую последовательность инструкций в базовом блоке, которые могут быть векторизованы, то он это сделает, если не сейчас, то в будущих версиях платформы .NET). В то время как для больших матриц важно использовать блочные алгоритмы с блоками, размеры которых равны длине кэш-линеек.
Зная размер матриц в вычислениях, компилятор способен выбирать наиболее оптимальный метод, а в случае если размер неизвестен, делать предположение или динамически вставлять проверку размера и соответственно осуществлять динамический выбор метода.
Заключение
Главная цель проекта Zonnon заключается в том, чтобы открыть поле для экспериментов с эволюционными языковыми концепциями [14], а также исследовать потенциал платформы .NET и технологии Microsoft CCI для интеграции компиляторов. Компилятор ETH Zonnon является первым компилятором, разработанным за пределами Microsoft, который полностью интегрирован в Visual Studio, а математическое расширение языка Zonnon, как нам известно, является первым расширением подобного рода для платформы .NET.
В ходе работы были проанализированы концепции программирования в векторно-матричном стиле, присутствующие в современных языках и пакетах, после чего на основе данного анализа синтаксис языка Zonnon был расширен новыми конструкциями. Описанные математические расширения реализованы в компиляторе ETH Zonnon для платформы .NET. В дальнейшем планируется реализовать оптимизации, описанные в соответствующем разделе, а также продемонстрировать полученный за счет этого прирост производительности.
Авторы благодарят проф. Ю. Гуткнехта и проф. В.П. Гергеля за предоставленную возможность заниматься этой темой, а также С.С. Лялина и Ф. Фридриха за многочисленные комментарии и идеи.
Работа выполнена при поддержке ETH Zürich, Швейцария.
Список литературы
1. Интернет-сайт MATLAB. URL: http://www. mathworks.com/products/matlab/
2. Venables B., Smith D. An introduction to R. URL: http://cran.r-project.org/doc/manuals/R-intro.html
3. Исследовательские лекции университета Калифорнии «Scientific Computing with FORTRAN 95». URL: http://exodus.physics.ucla.edu/Fortran95/ PSTIRe-searchLecSeries1.html
4. Array programming languages. URL: http:// en.wikipedia.org/wiki/Category:Array_programming_la nguages
5. Friedrich F., Gutknecht J., Morozov O., Hunziker P. A Mathematical Programming Language Extension for Multilinear Algebra // Timmendorfer Strand: Proc. Kolloqium über Programmiersprachen und Grundlagen der Programmierung, 2007.
6. Allen R., Kennedy K. Automatic Translation of FORTRAN Programs to Vector Form // ACM Transac-
tions on Programming Languages and Systems. 1987. Vol. 9, No. 4.
7. Компилятор Zonnon. URL: http://zonnon.ethz.ch/ compiler/download.html
8. Lowy J. Programming .NET components. O'Reilly Media, Inc., 2005.
9. Gutknecht J. Zonnon Language Report. Zurich, 2009.
10. Неймарк Ю.И., Ланда П.С. Стохастические и хаотические колебания. М.: Наука, 1987
11. Библиотека Common Compiler Infrastructure (CCI). URL: http://ccimetadata.codeplex.com/
12. Zueff E. Common Compiler Infrastructure from a Compiler Writer’s Perspective. Видеолекция Microsoft Corporation. США, Редмонд, 2003.
13. Box D., Sells C. Essential .NET, Volume I: The Common Language Runtime. Addison-Wesley Professional, 2002.
14. Gutknecht J., Mitin R., Zueff E. Project Zonnon: а .NET Language Platform Challenge // Proceedings of the Conference on Innovative Views of .NET Technologies (IVNET'06), 2006.
A MATHEMATICAL EXTENSION OF ZONNON PROGRAMMING LANGUAGE N.A. Gonova, N.Yu. Zolotykh, R.O. Mitin
An extension has been described of an object-oriented programming language with mathematical constructs: operations and functions on multidimensional arrays, indexing by ranges and vectors. It leads to a more readable, natural and compact code in applications implementing linear algebra algorithms. Practical experience of implementing mathematical constructs for the Zonnon programming language on .NET framework is discussed.
Keywords: Zonnon, programming language, mathematical extension, complex indexing, .NET, CCI, compiler, optimization.