Данная справка рассказывает про подводные камни при работе с неуправляемыми .dll,
+которые можно встретить при работе с модулями OpenGL
и OpenCL
, входящеми в состав стандартных модулей языка PascalABC.NET.
Справки по соответствующим библиотекам:
+(Далее "исходные библиотеки" относится сразу к обеим этим библиотекам)
+Отдельных справок по модулям OpenGL
и OpenCL
(далее просто "н.у. модули", что значит "низко-уровневые")
+нет, потому что они сделаны по
+
+общим принципам
+
+.
Н.у. модули созданы как промежуточная точка между исходными библиотеками и модулями
+OpenGLABC
+и
+OpenCLABC
,
+на случай если вы хотите использовать OpenGL и/или OpenCL, но хотите написать свою высоко-уровневую оболочку.
+Возможно, ради опыта. Возможно, ради особых оптимизаций. И т.п.
Если вы будете использовать только модули OpenGLABC
и OpenCLABC
- данная справка вам не нужна,
+потому что они специально созданы так, чтобы отстранить программиста от всех сложностей
+работы с неуправляемыми .dll и предоставить привычный ООП интерфейс со всевозможными удобствами.
Если:
+В н.у. модулях или данной справке найдена ошибка или чего-либо не хватает;
+Вы наткнулись на какую то особенностью работы с неуправляемыми .dll, которая тут не (или недо-) описана:
+Пишите в issue.
+Лучше прочитайте оффициальный документ от microsoft, если хотите точной и подробной информации.
+Если в кратце:
+Есть платформа .Net, объединяющая много языков программирования. В том числе C# и PascalABC.NET.
+.exe и .dll созданные такими языками содержат код в особом виде, позволяющем легко подключать
+.dll (и теоретически .exe, но это плохо) созданные на 1 .Net языке к программе на любом другом .Net языке.
+(в паскале это делается с помощью $reference
)
Такие .exe и .dll называются управляемыми. .exe и .dll созданные на любом другом (то есть не .Net) языке называются неуправляемыми.
+OpenCL.dll и OpenGL.dll созданы на- и для языков C/C++, поэтому являются неуправляемыми.
+Большинство функций из н.у. модулей требует какой-либо чистки. В основном потому что
+при переводе с C++ тип int*
может оказаться и указателем, и var
-параметром, и массивом.
При автоматическом переводе кода с C++ на паскаль - создаются все возможные перегрузки. +А те перегрузки, которые не подходят конкретным подпрограммам - надо убирать вручную. +В этом и состоит чистка подпрограмм.
+Все разом функции не почистишь, в одном только OpenGL
их >3000. Но сделать это, всё же, надо.
+Если хотите помочь - можно писать и в issue, но лучше (и проще для вас, когда разберётесь) создать fork репозитория,
+сделать там нужные изменения и послать pull-request. Могу объяснить подробнее в vk или по sunserega2@gmail.com
.
Чтобы чистить подпрограммы было проще - я написал несколько инструментов.
+Вся упаковка н.у. модулей находится тут.
+Откройте папку OpenCL
или OpenGL
. Они устроены одинаково, поэтому объяснять можно на любой из них.
Все файлы (кроме .exe, .pdb и .pcu, которые создаёт паскаль) в +этих папках открываются любым текстовым редактором.
+Контроль содержимого модуля находится в папке Fixers
. В этой папке есть следующие подпапки:
Funcs
- контроль подпрограмм;Enums
- контроль записей-перечислений;Structs
- контроль просто записей (записей-контейнеров, если хотите).Во всех папках находится файл ReadMe.md
, объясняющий синтаксис.
MiscInput
содержит другие данные, используемые при создании модулей, которые имеют разные значения для OpenCL и OpenGL.
Синтаксис у всех предельно прост. Не вижу смысла объяснять подробнее.
+Log
содержит логи последней сборки соответствующего модуля.
Они в основном используются чтобы было проще увидеть на что именно +повлияли ваши изменения и как (используя проверку изменений git'а).
+Но файл FinalFuncOverloads.log
так же особо полезен перед
+началом чистки, чтобы увидеть какие перегрузки уже есть.
Чтобы применить фиксеры и посмотреть на что они влияют - вызывайте Pack Most.pas
.
Ну а чтобы полностью собрать модули - вызывайте PackAll.exe
в корне репозитория.
+(или .bat
файлы там же, для сборки отдельных компонентов)
Если нужна высоко-уровневость - используйте соответственно модули +OpenGLABC +и +OpenCLABC +.
+В исходных библиотеках обращение ко всем объектам идёт по "именам" (их так же можно назвать дескрипторами или id этих объектов).
+Имена объектов - это числа (в OpenGL обычно на 32 бита, в OpenCL - зависит от битности системы).
+Чтобы в подпрограмму, принимающую имена объектов определённого типа нельзя было передать имя +объекта неправильного типа - в н.у. модулях для каждого типа объектов описана подобная запись:
+ gl_buffer = record
+ public val: UInt32;
+ public constructor(val: UInt32) := self.val := val;
+ public static property Zero: gl_buffer read default(gl_buffer);
+ public static property Size: integer read Marshal.SizeOf&<UInt32>;
+ public function ToString: string; override := $'gl_buffer[{val}]';
+ end;
+
+Такой подход не замедляет готовую программу, но позволяет отловить некоторые ошибки на этапе компиляции.
+Поле .val
и конструктор публичны только на случай ошибки в перегрузках,
+то есть если подпрограмма принемает неправильный тип имени.
В обычной ситуации - вы будете взаимодействовать с именами только 3 способами:
+Объявление:
+var name: gl_buffer;
+var names: array of gl_buffer := new gl_buffer[5];
+
+Передача исходным библиотекам:
+gl.CreateBuffers(1, name);
+gl.CreateBuffers(names.Length, names);
+
+Использование статического свойства .Zero
:
procedure MyProc(buff: gl_buffer);
+begin
+ ...
+end;
+...
+MyProc(gl_buffer.Zero);
+// То же самое, но с лишними скобками:
+MyProc(default(gl_buffer));
+
+У настоящих объектов имя никогда не будет нулевым.
+Но бывает не мало случаев когда исходные библиотеки могут принимать нулевое имя.
+К примеру, привязка шейдера с нулевым именем в OpenGL отменяет предыдущую привязку шейдера.
Многие параметры подпрограмм в исходных библиотеках принимают перечисления (enum'ы).
+Перечисления, как и имена, это числа. Но в отличии от имён - перечисления принимают заданные заранее константные значения.
+В качестве примера:
+procedure gl.BeginQuery(target: QueryTarget; id: gl_query)
+
+Параметр target
принимает одно из значений, сгрупированных в записи QueryTarget
:
QueryTarget = record
+ public val: UInt32;
+ public constructor(val: UInt32) := self.val := val;
+
+ public static property TRANSFORM_FEEDBACK_OVERFLOW: QueryTarget read new QueryTarget($82EC);
+ public static property VERTICES_SUBMITTED: QueryTarget read new QueryTarget($82EE);
+ public static property PRIMITIVES_SUBMITTED: QueryTarget read new QueryTarget($82EF);
+ public static property VERTEX_SHADER_INVOCATIONS: QueryTarget read new QueryTarget($82F0);
+ public static property TIME_ELAPSED: QueryTarget read new QueryTarget($88BF);
+ public static property SAMPLES_PASSED: QueryTarget read new QueryTarget($8914);
+ public static property ANY_SAMPLES_PASSED: QueryTarget read new QueryTarget($8C2F);
+ public static property PRIMITIVES_GENERATED: QueryTarget read new QueryTarget($8C87);
+ public static property TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN: QueryTarget read new QueryTarget($8C88);
+ public static property ANY_SAMPLES_PASSED_CONSERVATIVE: QueryTarget read new QueryTarget($8D6A);
+
+ ...
+
+ end;
+
+То есть вызов может выглядеть так:
+var name: gl_query;
+...
+gl.BeginQuery(QueryTarget.VERTICES_SUBMITTED, name);
+
+Чтобы увидеть доступные значения - достаточно написать QueryTarget
и поставить точку,
+после этого анализатор кода покажет список доступных имён.
Так же бывают особые перечисления - битовые маски. Они могут принимать сразу несколько значений:
+procedure gl.Clear(mask: ClearBufferMask);
+
+gl.Clear(
+ ClearBufferMask.COLOR_BUFFER_BIT + // Очистка поверхности рисования одним цветом
+ ClearBufferMask.DEPTH_BUFFER_BIT // Очистка буфера глубины - нужна при рисовании 3D
+);
+
+Все подпрограммы исходных библиотек можно разделить на подпрограммы ядра + множество мелких расширений.
+Все подпрограммы ядра находятся в классе gl
/cl
для OpenGL/OpenCL соответственно.
То есть если вы хотите вызвать функцию clCreateBuffer
надо писать:
cl.CreateBuffer(...);
+
+По нажатию точки после cl
вам так же покажет список всех функций в ядре OpenCL.
С OpenGL немного сложнее:
+В модуле OpenGL
это реализовано так:
//ToDo Создание контекста и привязка его к текущему потоку
+
+// Все адреса и экземпляры делегатов создаются в конструкторе
+// В "<>" указывается имя платформы
+// Можете написать "Pl" и нажать Ctrl+пробел, чтобы получить список платформ
+var gl := new OpenGL.gl<PlWin>;
+
+while true do
+begin
+ // Локальные переменные имеют бОльший приоритет чем классы
+ // Поэтому тут "gl" это не класс, а экземпляр, который создали выше
+ gl.Clear( ClearBufferMask.COLOR_BUFFER_BIT );
+
+ //ToDo само рисование
+
+end;
+
+У каждого расширения есть свой класс. К примеру, так используется расширение GL_AMD_debug_output
:
//ToDo Опять же, сначала контекст
+
+var glDebugOutputAMD := new OpenGL.glDebugOutputAMD<PlWin>;
+...
+glDebugOutputAMD.DebugMessageEnableAMD(...);
+
+В модуле OpenGL
так же есть особые классы, wgl
, gdi
и glx
:
gdi
содержит несколько методов библиотеки gdi32.dll
. На этой библиотеке основано всё в System.Windows.Forms
.
+Подпрограммы включённые в класс gdi
- это то, что может понадобиться вам, чтобы настроить и подготовить форму для рисования на ней с помощью OpenGL.
wgl
содержит методы для подключения OpenGL к окну Windows.
glx
содержит методы для подключения OpenGL к окну XWindow.
Все эти классы работают как класс cl
, то есть им не надо создавать экземпляр.
И последнее что вам надо знать об этих классах:
+Если вы получаете NullReferenceException
,
+при попытке вызова функции из инициализируемых классов, как gl
:
Скорее всего вы попытались вызвать подпрограмму, которой нет в реализации библиотеки на вашем компьютере.
+Проверьте версию библиотеки, или, если это подпрограмма из расширения - проверьте существование у вас этого расширения.
Так же, возможно, проблема в н.у. модуле. Если вы уверены что п.1. вас не касается - напишите в issue.
+Библиотека OpenGL.dll
имеет несколько функций, принимающих
+вектора и матрицы (в основном для передачи значений в шейдеры).
В модуле OpenGL
для каждого типа вектора и матрицы описана отдельная запись.
+Они особенны тем, что поддерживают некоторые математические операции, которые можно считать
+высокоуровневыми, а значит противоречущими основным принципам н.у. модулей.
Но реализовывать их все в качестве extensionmethod
'ов было бы сложно, не красиво,
+а в случае статических методов и свойств - ещё и невозможно.
ToDo
сейчас все индексные свойства кроме .ColPtr
(.val
, .Row
и .Col
) убраны из релиза,
+потому что я не знаю как безопасно и эффективно их реализовывать. Постараюсь в ближайшее
+время придумать, что можно сделать.
Все типы векторов можно описать разом как Vec[1,2,3,4][ b,ub, s,us, i,ui, i64,ui64, f,d ]
.
Каждый тип вектора берёт по 1 из значений, перечисленных в []
, через запятую.
К примеру, есть типы Vec2d
и Vec4ui64
.
Число в первых скобках - значит кол-во измерений вектора.
+Буква (буквы) в следующих скобках - значат тип координат вектора:
+b=shortint
, s=smallint
, i=integer
, i64=int64
: Все 4 типа - целые числа, имеющие бит знака (±) и занимающие 1, 2, 4 и 8 байт соответственно;
Они же но с приставкой u
- целые числа без знака. К примеру ui
значит целое на 4 байта без знака, то есть longword
(он же cardinal
);
f=single
и d=real
- числа с плавающей запятой, на 4 и 8 байт соответственно.
Таким образом Vec2d
хранит 2 числа типа real
, а Vec4ui64
хранит 4 числа типа uint64
.
У векторов есть только индексное свойство val
. Оно принимает индекс, считаемый от 0,
+и возвращает или задаёт значение вектора для соответствующего измерения.
К примеру:
+var v: Vec4d;
+v[0] := 123.456; // Записываем 123.456 по индексу 0
+v[1].Println; // Читаем и выводим значение по индексу 1
+v.val[2] := 1; // Можно так же писать и имя свойства
+
+Но использование этого свойства не рекомендуется. Прямое обращение +к полю всегда будет быстрее. То есть аналогично предыдущему коду:
+var v: Vec4d;
+v.val0 := 123.456;
+v.val1.Println;
+v.val2 := 1;
+
+Используйте свойство val
только тогда, когда индекс это НЕ константа.
var v0: Vec3d;
+...
+// v1 будет иметь ту же длину, но
+// противоположное v0 направление
+var v1 := -v0;
+// А унарный + не делает ничего, он только
+// для красоты. То есть v2=v0 тут
+var v2 := +v0;
+
+var v1: Vec3d;
+var v2: Vec3i;
+...
+// Выведет вектор, имеющий то же
+// направление что v1, но в 2 раза длиннее
+(v1*2).Println;
+
+// Выведет вектор, имеющий то же
+// направление что v1, но в 2 раза короче
+(v1/2).Println;
+
+// К целочисленным векторам вместо
+// обычного деления надо применять div
+(v2 div 2).Println;
+
+var v1, v2: Vec3d;
+...
+// Скалярное произведение векторов
+(v1*v2).Println;
+
+// Сумма векторов, складывает
+// отдельно каждый элемент вектора
+(v1+v2).Println;
+
+// Разность векторов, тоже работает
+// отдельно на каждый элемент вектора
+(v1-v2).Println;
+
+Чтобы применить 1 из этих операций к 2 векторам - их типы должны быть одинаковые.
+Если это не так - 1 из них (или оба) надо явно преобразовать, так чтобы типы были одинаковые:
var v1: Vec3d;
+var v2: Vec2i;
+...
+( v1 + Vec3d(v2) ).Println;
+
+Метод .SqrLength
возвращает квадрат длины (то есть модуля) вектора.
+Возвращаемый тип .SqrLength
совпадает с типом элементов вектора.
+Каким образом находить корень полученного значения - дело программиста.
var v1: Vec3d;
+...
+v1.SqrLength.Println; // Квадрат длины
+v1.SqrLength.Sqrt.Println; // Сама длина
+
+Метод .Normalized
возвращает нормализированную (с длиной =1) версию вектора.
+Так как эта операция требует деления (на длину вектора), она применима только
+к векторам с элементами типов single
или real
(f
или d
).
var v1 := new Vec3d(1,1,1);
+v1.Println;
+v1.SqrLength.Sqrt.Println;
+var v2 := v1.Normalized;
+v2.Println;
+v2.SqrLength.Sqrt.Println; // Обязательно будет 1
+
+Статичные методы .Cross[CW,CCW]
возвращают векторное произведение двух
+3-х мерных векторов ("Cross product", не путать со скалярным произведением).
+Векторное произведение - это вектор, перпендикулярный обоим входным векторам и имеющий длину,
+равную площади параллелограмма, образованного входными векторами.
Не работает для векторов с элементами-беззнаковыми_целыми, потому что даёт переполнение +на практически любых входных значениях. Если найдёте нормальное применение - напишите в issue.
+В математике произведение векторов может вернуть один из двух противоположных друг-другу векторов,
+в зависимости от ориентации системы координат. В модуле OpenGL
это решено следующим образом:
CW (Clockwise - по часовой стрелке):
+Vec3d.CrossCW(new Vec3d(1,0,0), new Vec3d(0,1,0)) = new Vec3d(0,0,1)
.
CСW (Counter-Clockwise - против часовой стрелки):
+Vec3d.CrossCCW(a,b) = -Vec3d.CrossCW(a,b) = Vec3d.CrossCW(b,a)
;
Кроме этого, статические методы .Cross[CW,CCW]
так же объявленны для 2D векторов.
+Для них результат является z-компонентом соответствующего метода для 3D векторов:
Vec2d.CrossCW(a,b) = new Vec3d(0,0,1) * Vec3d.CrossCW(Vec3d(a),Vec3d(b))
+
+С другом стороны, 2D векторное произведение это определитель матрицы (не важно, по строкам или столбцам):
+Vec2d.CrossCW(a,b) = Mtr2d.FromCols(a,b).Determinant
+
+Статичный метод .Random
создаёт новый вектор из случайных значений в заданном диапазоне:
// Вектор будет иметь значения из [0;1)
+Vec2d.Random(0,1).Println;
+
+Статичные методы Read
и Readln
создают новый вектор из элементов, прочитанных из стандартного ввода:
// Прочитать 2 числа из ввода
+Vec2d.Read('Введите 2 координаты:').Println;
+
+// Прочитать 2 числа из ввода
+// и затем пропустить всё до конца строки
+Vec2d.Readln.Println;
+
+var v1: Vec4d;
+...
+v1.Println; // Вывод вектора
+// s присвоит ту же строку, что выводит .Println
+var s := v1.ToString;
+
+Методы .ToString
и .Println
должны быть использованы
+только для чего то вроде дебага или красивого вывода,
+потому что операции со строками это в целом медленно.
Все типы матриц можно описать разом как Mtr[2,3,4]x[2,3,4][f,d]
.
У каждой квадратной матрицы есть короткий синоним.
+К примеру вместо Mtr3x3d
можно писать Mtr3d
.
Так же стоит заметить - конструктор матрицы принимает элементы по строкам, +но в самой матрице элементы хранятся в транспонированном виде.
+Это потому, что в OpenGL.dll
в шейдерах матрицы хранятся по столбцам.
+Но если создавать матрицу конструктором - элементы удобнее передавать по строкам, вот так:
var m := new Mtr3d(
+ 1,2,3, // (1;2;3) станет нулевой строкой матрицы
+ 4,5,6,
+ 7,8,9
+);
+
+Как и у векторов, у матриц есть свойство val
:
var m: Mtr4d;
+m[0,0] := 123.456;
+m[1,2].Println;
+m.val[3,1] := 1;
+
+И как и у векторов - val
всегда медленнее прямого обращения к полям:
var m: Mtr4d;
+m.val00 := 123.456;
+m.val12.Println;
+m.val31 := 1;
+
+Но у матриц так же есть свойства для столбцов и строк:
+var m: Mtr3d;
+...
+m.Row0.Println; // Вывод нулевой строчки в виде вектора
+m.Row1 := new Vec3d(1,2,3);
+m.Col2.Println;
+
+И в качестве аналога val
- строку и стобец тоже можно
+получать по динамическому индексу (но, опять же, это медленнее):
var m: Mtr3d;
+...
+m.Row[0].Println;
+m.Row[1] := new Vec3d(1,2,3);
+m.Col[2].Println;
+
+Для столбцов так же есть особые свойства, возвращающие не столбец, а его адрес в памяти:
+var m: Mtr3d;
+...
+var ptr1 := m.ColPtr0;
+var ptr2 := m.ColPtr[3];
+
+Использовать это свойство + +не всегда безопасно.
+Оно должно быть использовано только для записей, +хранящихся на стеке или в неуправляемой памяти.
+Для более безопасной альтернативы - можно использовать методы .UseColPtr*
.
Это тоже свойство, но статическое и применение совершенно другое:
+Identity
возвращает новую единичную матрицу. То есть матрицу, у
+которой главная диагональ заполнена 1, а всё останое заполнено 0.
Mtr3d.Identity.Println;
+// Работает и для не_квадратных матриц
+Mtr2x3d.Identity.Println;
+
+Методы .UseColPtr*
принимают подпрограмму, принимающую
+адрес определённого столбца в виде var
-параметра.
В отличии от свойств .ColPtr*
, методы .UseColPtr*
безопасны для
+матриц, хранящихся в экземплярах классов и статических полях:
uses OpenGL;
+
+procedure p1(var v: Vec3d);
+begin
+ Writeln(v);
+end;
+
+function f1(var v: Vec3d): string :=
+v.ToString;
+
+begin
+ var o := new class(
+ m := Mtr3d.Identity
+ );
+ o.m.UseColPtr0(p1);
+ o.m.UseColPtr1(f1).Println;
+end.
+
+Статичный метод .Scale
возвращает матрицу, при
+умножении на которую вектор маштабируется в k раз.
var m := Mtr3d.Scale(2);
+var v := new Vec3d(1,2,3);
+(m*v).Println; // (2;4;6)
+
+Статичный метод .Translate
возвращает матрицу, при
+умножении на которую к вектору добавляется заданное значение.
var m := Mtr4d.Translate(1,2,3);
+
+// Последний элемент должен быть 1,
+// чтобы матрица из .Translate правильно работала
+var v := new Vec4d(0,0,0,1);
+
+(m*v).Println; // (1;2;3)
+
+Так же есть статический метод .TraslateTransposed
. Он возвращает
+ту же матрицу что .Translate
, но в транспонированном виде.
Группа статических методов .Rotate[XY,YZ,ZX][cw,ccw]
возвращает матрицу вращения в определённой плоскости.
Первые скобки определяют плоскость.
+(Но у 2x2 матриц есть только XY вариант)
Вторые скобки определяют направление вращения:
+Группа статических методов .Rotate3D[cw,ccw]
возвращает матрицу
+вращения вокруг нормализованного 3-х мерного вектора.
+(разумеется, не существует для матриц 2x2,2x3 и 3x2)
Метод .Det
возвращает определитель матрицы. Существует только для квадратных матриц.
Метод .Transpose
возвращает транспонированную версию матрицы:
var m := new Mtr2x3d(
+ 1,2,3,
+ 4,5,6
+);
+m.Transpose.Println; // Выводит:
+// 1 4
+// 2 5
+// 3 6
+
+m*v
- это обычное математическое умножение матрицы m
и вектора v
,
+возвращающее результат после применения преобразования из m
к v
.
Но так же как в шейдерах - поддерживается и обратная запись:
+v*m
это то же самое что m.Transpose*v
.
m1*m2
- это математическое умножение матриц m1
и m2
.
Статичный метод .Random
создаёт новую матрицу из случайных значений в заданном диапазоне:
// Матрица будет иметь значения из [0;1)
+Mtr3d.Random(0,1).Println;
+
+Статичные методы Read[,ln][Rows,Cols]
создают новую матрицу из элементов, прочитанных из стандартного ввода:
// Прочитать 3*4=12 элементов из ввода
+// и сохранить в новую матрицу по строкам
+Mtr3x4d.ReadRows('Введите 12 элементов матрицы:').Println;
+
+// Прочитать 4 элемета из ввода, переходя на
+// следущую строку ввода после чтения каждого столбца
+Mtr2d.ReadlnCols(
+ 'Введите столбцы матрицы...',
+ col -> $'Столбец #{col}:'
+).Println;
+
+Как и у векторов - матрицы можно выводить и превращать в строку
+var m: Mtr4d;
+...
+m.Println; // Вывод матрицы
+// s присвоит ту же строку, что выводит .Println
+var s := m.ToString;
+
+Для того чтобы матрица выведенная 1 из этих методов выглядела красиво надо +использовать моноширный шрифт и поддерживать юникод (потому что для матриц +используются символы псевдографики).
+Обычно это не проблема для .Println
, потому что и консоль, и окно вывода в IDE имеют моноширный шрифт и поддерживают юникод.
Но если выводить на форму, то придётся специально поставить моноширный шрифт.
+А если выводить в файл, надо выбрать кодировку файла - юникод (UTF).
Большинство проблем при использовании неуправляемых .dll вытекают из следующих 2 различий:
+В .Net строки и массивы это стандартные типы, доступные для всех .Net языков.
+А в C++ строки и массивы не только описаны не так же как в .Net . +У них так же есть множество разных стандартов. +Благо, исходные библиотеки придерживаются одного общего стандарта.
+В управляемом коде оперативная память (дальше просто память) обычно управляется сборщиком мусора. +Поэтому если создать массив - когда он стал не нужен о нём можно просто забыть.
+А в C++ память управляется только программистом, поэтому неуправляемую память нужно всегда освобождать после использования.
+Забытая и не_освобождённая неуправляемая память называется утечкой памяти. И это один из самых сложно-ловимых багов,
+потому что у него нет явно видимых симптомов, вроде вызова ошибки, пока память окончательно не закончится.
В .Net так же можно выделять и освобождать неуправляемую память, статическими методами класса Marshal
:
+.AllocHGlobal
и .FreeHGlobal
соответственно. Обычно это надо для п.1., для преобразований между
+управляемыми и неуправляемыми типами.
Ещё одно отличие неуправляемой памяти - она не очищается нулями при выделении, а +значит содержит мусорные данные. И память GPU (к примеру, содержимое буферов) тоже.
+Полное имя класса Marshal
это System.Runtime.InteropServices.Marshal
.
+Чтобы не писать его целиком - можно написать в начале файла uses System.Runtime.InteropServices
, и дальше писать только Marshal
.
Стоит так же заметить, что в н.у. модулях все подпрограммы, напрямую вызывающие подпрограммы из неуправляемых +.dll принимают параметрами как неуправляемые, так и управляемые типы массивов и строк, по мере необходимости.
+Если ваша необходимость не удовлетворена (то есть не хватает перегрузки с определённым типом) - это особо хороший повод написать в issue.
+var
-параметры Кроме удаления неиспользуемых экземпляров классов, сборщик мусора так же может произвольно +перемещать используемые объекты, более плотно упаковая их в памяти.
+И он прекрасно справляется с тем, чтобы сделать эти перемещения незаметными, в обычных ситуациях. +Но как только речь находит об указателях и неуправляемом коде - начинаются проблемы. +Чтобы избежать их, надо очень хорошо понимать как работает сборщик мусора.
+uses System;
+uses System.Runtime.InteropServices;
+// Отдельный разбор:
+// http://forum.mmcs.sfedu.ru/t/testy-raboty-sborshhika-musora
+
+function get_addr(a: array of integer) := '$'+Marshal.UnsafeAddrOfPinnedArrayElement(a,0).ToString('X');
+
+// Заполняем память кучей разных мусорных объектов,
+// которые никогда не удалятся, чтобы
+// сборщик мусора засуетился когда его тыкнут
+// Не знаю почему надо именно массив массивов + массив их адресов
+var ___a := ArrGen(10000, i->|0|);
+var ___p := ___a.ConvertAll(get_addr);
+
+type
+ punch_gc_callback = procedure(ptr: pointer);
+
+function ptr_adr<T>(var a: T) := new IntPtr(@a);
+
+{$region Safe}
+
+function copy_arr(var a: integer; punch_gc: punch_gc_callback): IntPtr;
+external 'Native\x64\Release\Native.dll';
+function copy_arr(var a: byte; punch_gc: punch_gc_callback): IntPtr;
+external 'Native\x64\Release\Native.dll';
+
+function copy_arr([MarshalAs(UnmanagedType.LPArray)] a: array of integer; punch_gc: punch_gc_callback): IntPtr;
+external 'Native\x64\Release\Native.dll';
+
+function copy_arr_recall2(a: array of integer; punch_gc: punch_gc_callback): IntPtr :=
+copy_arr(a, punch_gc);
+
+function copy_arr_recall3(var a: integer; punch_gc: punch_gc_callback): IntPtr :=
+copy_arr(a, punch_gc);
+
+function copy_arr_recall5(var a: integer; punch_gc: punch_gc_callback): IntPtr :=
+copy_arr(PByte(pointer(@a))^, punch_gc);
+
+{$endregion Safe}
+
+{$region Unsafe}
+
+function copy_arr(a: IntPtr; punch_gc: punch_gc_callback): IntPtr;
+external 'Native\x64\Release\Native.dll';
+function copy_arr(a: ^integer; punch_gc: punch_gc_callback): IntPtr;
+external 'Native\x64\Release\Native.dll';
+
+function copy_arr_recall1(var a: integer; punch_gc: punch_gc_callback): IntPtr :=
+copy_arr(@a, punch_gc);
+
+function copy_arr_recall4_helper(a: ^integer; punch_gc: punch_gc_callback): IntPtr;
+begin
+ punch_gc(a);
+ Result := copy_arr(a^, ptr->begin end); // Второй раз вызывать punch_gc и вывод - ни к чему, всё ломается уже на предыдущей строчке
+end;
+function copy_arr_recall4(var a: integer; punch_gc: punch_gc_callback): IntPtr :=
+copy_arr_recall4_helper(@a, punch_gc);
+
+function get_int(punch_gc: punch_gc_callback; var a: integer): integer;
+begin
+ punch_gc(@a);
+ Result := 4;
+end;
+function copy_arr_recall6(var a: integer; punch_gc: punch_gc_callback): IntPtr :=
+copy_arr(PByte(pointer(IntPtr(pointer(@a))+get_int(punch_gc, a)))^, ptr->begin end);
+
+{$endregion Unsafe}
+
+procedure punch_gc := GC.Collect;
+
+begin
+ var a := Arr(1,2,3,4,5,6);
+ var b := Arr(1,2,3,4,5);
+ Console.WriteLine('begin');
+ Console.WriteLine(get_addr(a));
+ Console.WriteLine(get_addr(b));
+
+ // punch_gc работает только 1 раз, эти строчки только чтобы протестировать, работает ли он у вас вообще
+// punch_gc;
+// Console.WriteLine('after first gc');
+// Console.WriteLine(get_addr(a));
+// Console.WriteLine(get_addr(b));
+
+ {$region заголовки вызова copy_arr}
+
+ // Безопасно
+// var ptr := copy_arr(a, // Передавать как массив безопасно
+// var ptr := copy_arr(a[0], // Передавать элемент массива var-параметром безопасно
+// var ptr := copy_arr(a[1], // И это касается не только элемента [0]
+// var ptr := copy_arr_recall2(a, // Безопасно, потому что с точки зрения copy_arr_recall2 ситуация та же что "copy_arr(a,"
+// var ptr := copy_arr_recall3(a[0], // И var-параметры тоже безопасны через промежуточные подпрограммы
+// var ptr := copy_arr_recall5(a[0], // Тут указатели не попадают в готовый .exe, они только чтобы успокоить компилятор, поэтому безопасно
+
+ // НЕ безопасно
+// var ptr := copy_arr(Marshal.UnsafeAddrOfPinnedArrayElement(a,0), // GC не следит за содержимым IntPtr
+// var ptr := copy_arr(ptr_adr(a[0]), // И за другими формами указателей тоже
+// var ptr := copy_arr_recall1(a[0], // Проблема не в передаче адреса возвращаемым значением из ptr_adr в copy_arr
+// var ptr := copy_arr_recall4(a[0], // Кроме того, проблема вообще не в неуправляемом коде, в управляемом тоже воспроизводится
+// var ptr := copy_arr_recall6(a[0], // В отличии от recall5 - тут указатели попадают в готовый .exe, поэтому небезопасно
+
+ {$endregion заголовки вызова copy_arr}
+
+ ptr->
+ begin
+ Console.WriteLine('before gc');
+ Console.WriteLine(get_addr(a));
+ Console.WriteLine('$'+IntPtr(ptr).ToString('X'));
+ Console.WriteLine(get_addr(b));
+
+ // "b" используется только чтобы видеть когда punch_gc успешно сработал
+ // (хотя всегда может переместить только "a", или только "b", поэтому не 100% показатель)
+ // Но главное тут - "ptr" это то, что хранит неуправляемый код, а "get_addr(a)" показывает текущий адрес "a"
+ // Если "get_addr(a)" изменился после вызова "punch_gc", а "ptr" остался тем же - значит "a" не заблокирован
+ punch_gc;
+ Console.WriteLine('after gc');
+ Console.WriteLine(get_addr(a));
+ Console.WriteLine('$'+IntPtr(ptr).ToString('X'));
+ Console.WriteLine(get_addr(b));
+ end);
+
+ Console.WriteLine('end');
+ Console.WriteLine(get_addr(a));
+ Console.WriteLine(get_addr(b));
+
+// punch_gc;
+// Console.WriteLine('after last gc');
+// Console.WriteLine(get_addr(a));
+// Console.WriteLine(get_addr(b));
+
+ // Показывает эффекты НЕ безопасного вызова
+ // Точнее если неуправляемый код потеряет адрес массива, то тут
+ // может быть мусор или ошибка доступа, правда я их ни разу не получил
+ var res := new byte[20];
+ Marshal.Copy(ptr,res,0,20);
+ res.Println;
+end.
+
+Dll1.dll
должна быть неуправляемой библиотекой, содержащей следующую функцию (это C++):
extern "C" __declspec(dllexport) BYTE* copy_arr(int* a, void (*punch_gc)(void*))
+{
+ BYTE* res = new BYTE[20]; // Выделяем 20 байт неуправляемой памяти
+ punch_gc(a); // Вызываем ту подпрограмму, чей адрес сюда передали
+ memcpy(res, a, 20); // Копируем 20 байт из "a" в "res"
+ return res; // Плохо что неуправляемая память не освобождается, но в этом тесте не важно
+}
+
+Подробнее о параметрах:
+a
принимает указатель на integer
, что в C++ так же может являеться массивом с элементами типа integer
;
punch_gc
принемает адрес подпрограммы, принемающей void*
(безтиповый указатель)
+и возвращающей void
(ничего не возвращающей, то есть это процедура);
Ну и возвращаемое значение - BYTE*
. Так же как a
, вообще указатель, но в данном случае массив.
Почитать более подробный разбор и скачать архив с упакованной библиотекой можно тут.
+Пожалуйста, попробуйте поэксперементировать с этим кодом сами. И если найдёте +что-то интересное - обязательно напишите в issue. В этом деле много тестов не бывает.
+Вся безопасность зависит только от объявления параметра external
-подпрограммы,
+через который передаётся управляемый объект. Если параметр принимает:
Любой размерный тип (то есть запись):
+При передаче в подпрограмму размерное значение копируется.
+Сборщик мусора тут не при чём. Поэтому передача записи всегда безопасна.
Массив или var
-параметр:
+Пока вызов неуправляемой подпрограммы не завершится - сборщик мусора НЕ будет передвигать объект в памяти.
+Но обратите внимание, есть неуправляемые подпрограммы, использующие переданные вами данные после завершения вызова external
-подпрограммы.
+К примеру gl.Uniform1iv
скопирует данные из вашего массива в память GPU, поэтому он безопасен.
+Но cl.CreateBuffer
в который передали MemFlags.MEM_USE_HOST_PTR
создаст буфер, который
+будет ссылаться на ваш массив. В таком случае надо использовать GCHandle
с GCHandleType.Pinned
, чтобы массив не двигался в памяти.
+Поэтому очень внимательно читайте документацию того, что вызываете.
Указатель в любом виде (типизированный, безтиповый или даже обёрнутый в запись вроде IntPtr
):
+Передавать адрес содержимого класса НЕ безопасно.
+Можно передавать только адрес локальной переменной, которая не была захвачена лямбдой.
+Ну или адрес заведомо неуправляемого куска памяти, к примеру выделенного с помощью Marshal.AllocHGlobal
.
Так же протестировал mono
(версия .Net для линукса) на arch-linux и Windows10.
+Среда выполнения mono
блокирует управляемые объекта не зависимо от типа параметра,
+даже если external
подпрограмма принимает указатель или IntPtr
.
То есть при запуске из под mono
волноваться о блокировке объектов надо только в случае,
+если неуправляемый код будет использовать данные этого объекта после выхода из вызова external
-подпрограммы.
Ну и нельзя забывать о случае, когда данные объекта имеют сложный тип, требующий маршлинга +перед передачей в неуправляемый код. Подробнее на странице ниже.
+В .Net var
-параметры реализованы через указатели. То есть эти 2 кода:
procedure p1(var i: integer);
+begin
+ i.Println;
+ i := 5;
+end;
+
+begin
+ var i := 3;
+ p1(i);
+ i.Println;
+end.
+
+procedure p1(i: ^integer);
+begin
+ i^.Println;
+ i^ := 5;
+end;
+
+begin
+ var i := 3;
+ p1(@i);
+ i.Println;
+end.
+
+Генерируют практически одинаковые .exe .
+Отличие состоит в том, что передавать содержимое класса (в том числе массива) указателем +опасно - сборщик мусора может в любой момент переместить память, ломая указатель.
+В то же время если передавать это же значение var
-параметром - сборщик мусора
+поправит адрес при перемещении памяти.
Статические поля так же могут быть перемещены, как и содержимое экземпляров, даже если они имеют размерный тип.
+И глобальные переменные это, на самом деле, тоже статические поля.
+А захваченные лямбдой локальные переменные превращаются в поля специального анонимного класса.
В .Net массивы хранят не только содержимое, но и данные о своём размере.
+А в C++ вместо обычных массивов используется безформенная область памяти.
+При её выделении - в переменную записывается указатель [0]
элемента.
+А о том чтобы сохранить данные о размере этой области - должен позаботится программист.
+(на самом деле обычно в C++ используют обёртки, хранящие длину так же как .Net массивы. Но OpenGL.dll и OpenCL.dll это не касается)
Если вы видели старые коды с использованием OpenGL из какого то-из паскалей - наверняка видели что то такое:
+glИмяПодпрограммы(@a[0]);
+
+Но в PascalABC.Net так делать нельзя! Получение указателя на элемент массива моментально создаёт утечку памяти,
+потому что компилятор, на всякий случай, вставляет полную блокировку массива в памяти, используя GCHandle
с GCHandleType.Pinned
.
Такая блокировка нужна, потому что иначе полученный указатель может в любой момент стать устаревшим.
+Обычно GCHandle
освобождают методом .Free
. Но если позволить компилятору использовать
+GCHandle
- освобождение никогда не произойдёт, потому что компилятор не знает когда указатель станет не нужен.
Из очевидных вариантов - использовать GCHandle
самостоятельно:
## uses System.Runtime.InteropServices;
+var a := |1,2,3|;
+
+var gc_hnd := GCHandle.Alloc(a, GCHandleType.Pinned);
+try
+ var ptr: ^integer := gc_hnd.AddrOfPinnedObject.ToPointer;
+ Writeln(ptr);
+ Writeln(ptr^);
+finally
+ // Освобождение в finally, чтобы оно произошло не зависимо ошибок при выполнении
+ gc_hnd.Free;
+end;
+
+Но на него наложенны некоторые ограничения, связанные с тем как среда .Net работает с памятью:
+uses System;
+
+type
+ r1 = record
+ b1: byte;
+ i: integer;
+ b2: byte;
+// s: string[5];
+ end;
+
+function Offset(p1,p2: pointer) := int64(p2)-int64(p1);
+
+begin
+ var a: r1;
+ Offset(@a,@a.b1).Println;
+ Offset(@a,@a.i ).Println;
+ Offset(@a,@a.b2).Println;
+// Offset(@a,@a.s).Println;
+end.
+
+Пока запись содержит только размерные поля - их хранит в памяти в том порядке, которые описали вы.
+Ну и в данном случае если бы i
хранилось сразу за b1
- его адрес не делился бы поровну на sizeof(integer)=4
,
+а значит процессору было бы неудобно читать и записывать i
, поэтому его отодвигает на 3 байта от b1
.
Но если раскомментировать объявление поля s
, являющегося строкой (то есть имеющего ссылочный тип),
+среда .Net решает что эта запись не может быть передана в неуправляемый код как есть,
+(потому что каждая реализация .Net может по-своему решать как хранить содержимое классов)
+а значит можно спокойно перетасовать поля так, чтобы плотнее их упаковать.
Эта запись всё ещё может быть передана параметром в неуправляемый код, даже var
-параметром.
+Но в таком случае данные из неё копирует в отдельную область памяти, так чтобы в этой области
+поля были в том порядке, в котором их описали в записи. Это копирование можно симулировать вручную:
uses System;
+uses System.Runtime.InteropServices;
+
+type
+ // Чтобы класс был форматированным, то есть совместимым
+ // с неуправляемым кодом, надо или
+ // LayoutKind.Sequential или LayoutKind.Explicit
+ [StructLayout(LayoutKind.Sequential)]
+ MyFormattedClass = class
+ b1: byte := $BA;
+ i: integer := $87654321;
+ b2: byte := $DC;
+ end;
+
+ // LayoutKind.Sequential влияет на StructureToPtr,
+ // но не на то как хранятся данные в переменной "a"
+ // CharSet.Unicode указывает как должен работать UnmanagedType.ByValTStr
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ r1 = record
+ b1: byte := $AB;
+ i: integer := $12345678;
+ b2: byte := $CD;
+ o := new MyFormattedClass;
+
+ // Явно указываем как неуправляемый код
+ // должен видеть массив и строку:
+
+ // Из массива берём первые 2 значения и сохраняем в саму запись
+ // В массиве должно быть хотя бы 2 элемента при вызове StructureToPtr
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst=2)]
+ arr := new byte[]($EF,$FE);
+ // А из строки берём максимум 2 первых символа
+ // То есть не больше двух, в строке может быть и меньше
+ // В конце всегда добавляется нулевой символ, чтобы
+ // указать неуправляемому коду где строка заканчивается
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst=2+1)]
+ s := char($CC11)+char($CC22);
+
+ end;
+
+function Offset(p1,p2: pointer) := int64(p2)-int64(p1);
+
+begin
+ var a: r1;
+
+ 'Содержимое r1:'.Println;
+ Writeln('Начало: ',@a);
+ Offset(@a,@a.b1).Println;
+ Offset(@a,@a.i ).Println;
+ Offset(@a,@a.b2).Println;
+ Offset(@a,@a.o).Println;
+
+ 'Содержимое класса:'.Println;
+ // Будем надеятся что содержимое класса не переместит пока выводим
+ // Конечно, в реальной ситуации такая надежда не годится
+ var po := PPointer(pointer(@a.o))^;
+ Writeln('Начало: ',po);
+ Offset(po,@a.o.b1).Println;
+ Offset(po,@a.o.i ).Println;
+ Offset(po,@a.o.b2).Println;
+
+ var sz := Marshal.SizeOf&<r1>;
+ var ptr := Marshal.AllocHGlobal(sz);
+ // Делаем неуправляемую версию этой записи
+ Marshal.StructureToPtr(a, ptr, false);
+
+ 'Содержимое неуправляемой копии:'.Println;
+ for var i := 1 to sz do
+ begin
+ // X2 указывает 16-ричный вывод, по 2 символа на каждое значение
+ PByte((ptr+(i-1)).ToPointer)^.ToString('X2').Print;
+ if i.Divs(8) then Println;
+ end;
+
+end.
+
+Поля записи r1
опять перемешиваются, даже с явным указанием LayoutKind.Sequential
,
+потому что эта запись всё равно не может использоваться неуправляемым кодом как-есть.
В то же время поля класса оказались в том порядке, как мы указали. Потому что, на самом деле,
+этот класс можно передавать в неуправляемый код отдельно от r1
. Такое объявление:
procedure p1(o: MyFormattedClass); external 'some_lib.dll';
+
+Будет эквивалентно:
+procedure p1(var o: SomeRecord); external 'some_lib.dll';
+
+Где SomeRecord
- запись с таким же полями. Но есть одно но: первое объявление p1
будет блокировать
+содержимое MyFormattedClass
с помощью GCHandle
, тратя на это немного времени процессора.
Но если бы в MyFormattedClass
были ссылочные поля - все его поля перемешались бы так же, как и поля r1
.
Далее, вызывается StructureToPtr
, копирующее содержимое записи в область неуправляемой памяти.
+И в данном случае, то что сохраняется в неуправляемую память это не копия содержимого переменной a
,
+а совершенно новое значение, в которое влили копии содержимого MyFormattedClass
, массива и строки,
+не смотря на то что оригиналы этих значений хранились в разных частях оперативной памяти.
Кроме того, в данном примере не показано, но есть некоторые типы записей, как boolean
, char
и DateTime
,
+которые можно по-разному интерпретировать при передаче в неуправляемый код.
+Их тоже преобразовывает при копировании в неуправляемую память.
Возвращаясь к GCHandle
: Он принципиально используется не для копирования, а для закрепления в памяти существующего содержимого.
+Поэтому когда он встречает запись, которую надо преобразовывать при передаче в неуправляемый код,
+GCHandle
наотрез отказывается загреплять её с GCHandleType.Pinned
.
+(но в качестве исключения разрешает закреплять некоторые особые случаи, как массив символов)
И это хорошо - это защита от дурака, потому что случаи когда запись, +поля которой среда .Net могла перетасовать, передают в неуправляемый код как есть - заведомо неправильны.
+Но с другой стороны - эта проверка типов происходит во время выполнения программы.
+А если надо делать много быстрых неуправляемых вызовов - это плохо.
+Поэтому н.у модули используют другой способ:
Как видно в тестах на странице выше - массив можно заблокировать в памяти без GCHandle
,
+если передавать его параметром-массивом или var
-параметром.
И, в отличии от GCHandle
, такой способ будет работать с массивами с любым размерным типом элементов.
+Это значит что следить за адекватностью содержимого записи должны вы сами.
+Но в то же время это значит что вы сами решаете, какие проверки делать.
К примеру, если имеем процедуру p1
из неуправляемой .dll, принимающую массив из двух чисел типа integer
:
var a := new integer[5](...);
+p1(a); // Передача массива целиком
+p1(a[3]); // Передача [3] элемента var-параметром
+
+Из первого вызова p1
возьмёт только элементы a[0]
и a[1]
,
+потому что p1
по условию требует только два элемента.
Из второго вызова p1
возьмёт a[3]
и a[4]
, потому что в неуправляемом коде
+нет разницы между указателем на один из элементов и указателем на начало массива.
Но, обратите внимание, на случай, когда тип требует особого преобразования.
+Для примера возьмём параметр, который объявили как var b: boolean
и передали в него элемент массива:
boolean
занимает 1 байт в .Net, но по-умолчанию преобразовывается в BOOL
из C++, занимающий 4 байта.
+Поэтому, в данном случае, среда .Net выделит новую неуправляемую область памяти в 4 байта
+и передаст её неуправляемому коду. А остальные элементы массива неуправляемому коду не достанутся.
Обычнно эти два способа передать массив в неуправляемый код - всё что вам понадобится.
+Но, допустим, вы хотите написать подпрограмму для создания OpenGL буфера из массива векторов. +Можно сделать перегрузку для каждого типа вектора, но тогда получится очень много дублей кода. +Этого довольно просто избежать, используя шаблоны:
+// Это не настоящая подпрограмма, а только пример
+procedure FillBuffer(var data: byte);
+external 'some.dll';
+
+// external подпрограммы не могут быть шаблонными, поэтому нужна ещё одна промежуточная перегрузка
+// "where T: record;" делает так, что FillBuffer будет можно вызвать только для размерных типов T
+procedure FillBuffer<T>(var data: T); where T: record;
+begin
+ // Компилятор развернёт это в "FillBuffer(data)"
+ // То есть никакие преобразования в .exe не попадут
+ // Но указатели всё равно нужны, чтобы компилятор не ругался на несовместимость типов
+ FillBuffer(PByte(pointer(@data))^);
+end;
+
+procedure FillBuffer<T>(data: array of T); where T: record;
+begin
+ // В неуправляемом коде нет разницы между массивом и адресом начала его содержимого
+ // Поэтому можно передавать массив в виде [0] элемента-var-параметра.
+ FillBuffer(data[0]);
+end;
+
+Но это для одномерных массивов. А что насчёт многомерных?
+Сделать перегрузку для заданного кол-ва измерений не сложно:
+procedure FillBuffer<T>(data: array[,] of T); where T: record;
+begin
+ // Многомерные массивы расположены в памяти как одномерные,
+ // Но обращение к элементам идёт по нескольким индексам
+ // Элемент [0,0,...] в любом случае будет в самом начале,
+ // Поэтому код одинаковый для любого кол-ва измерений
+ FillBuffer(data[0,0]);
+end;
+
+Но, опять же, получается так, что для каждой размерности - приходится добавлять перегрузку.
+И, к сожалению, в данном случае я не знаю красивого способа обхода.
+Лучшее что я могу придумать - создать Dictionary<integer, Action<System.Array>>
,
+где ключи - размерности массивов, а значения - делегаты, работающие с соответствующей размерностью.
+Когда происходит вызов с массивом определённой размерности - создавать новый делегат
+в виде динамичного метода, с помощью System.Reflection.Emit
, если его ещё нет в словаре.
Как и массивы - неуправляемые строки это указатель на первый символ строки.
+Но со строками ещё сложнее - исходные библиотеки хранят строки в кодировке ANSI (1 байт на символ).
+А управляемые строки - хранят символы в кодировке Unicode (2 байта на символ).
Кроме того, у неуправляемых строк принятно добавлять в конце строки символ #0
.
+Это позволяет вообще не хранить длину строки. Вместо этого конец строки считается там, где первый раз встретится символ #0
.
Благо, для перевода между этими типами уже есть Marshal.StringToHGlobalAnsi
и Marshal.PtrToStringAnsi
.
Но будьте осторожны - Marshal.StringToHGlobalAnsi
выделяет неуправляемую память для хранения неуправляемого варианта строки.
+Когда неуправляемая память стала не нужна - её надо обязательно удалить методом Marshal.FreeHGlobal
, иначе получите утечку памяти.
В отличии от массивов - пытаться передавать строки в виде символа-var
-параметра безсмысленно, из за разницы форматов содержимого.
Более того, передача символа строки var
-параметром, в отличии от массивов, всё равно вызывает копирование строки,
+на всякий случай, потому что в .Net строки неизменяемы, а компилятор не может знать,
+будет неуправляемый код только читать, или ещё и перезаписывать строку.
Единственный способ не выполнять лишних копирований - написать свою обёртку неуправляемых строк. Обычно оно того не стоит.
+Но если вы, к примеру, создаёте много OpenGL шейдеров из исходников - можно перед компиляцией программы:
+Marshal.StringToHGlobalAnsi
чтобы получить неуправляемые строки;$resource
, читать как массив байт и его уже передавать неуправляемому коду вместо строки.Делегат - это адрес подпрограммы:
+procedure p1(i: integer);
+begin
+ Writeln(i);
+end;
+
+begin
+
+ var d: System.Delegate := p1; // Это не вызов, а получение адреса p1
+ d.DynamicInvoke(5); // Вообще .DynamicInvoke это очень медленно
+
+ var p: integer->();
+ // Такое же объявление как на предыдущей строчке, но в другом стиле
+// var p: Action<integer>;
+ // И ещё один стиль. Этот особенный, потому что
+ // Он неявно создаёт новый тип делегата
+// var p: procedure(i: integer);
+ p := p1;
+
+ // Типизированные делегаты можно вызывать быстрее и проще,
+ // так же как обычные подпрограммы
+ p(5);
+
+end.
+
+Так же как обычные подпрограммы - подпрограммы из неуправляемых .dll могут принимать делегаты параметром.
+Далее всё будет рассматриваться на примере cl.SetEventCallback
из модуля OpenCL
, потому что с ним есть особые проблемы.
Объявление cl.SetEventCallback
:
static function SetEventCallback(&event: cl_event; command_exec_callback_type: CommandExecutionStatus; pfn_notify: EventCallback; user_data: IntPtr): ErrorCode;
+
+Объявление EventCallback
:
EventCallback = procedure(&event: cl_event; event_command_status: CommandExecutionStatus; user_data: IntPtr);
+
+Рассмотрим следующий пример:
+uses System;
+uses OpenCL;
+
+begin
+
+ var cb: EventCallback := (ev,st,data)->
+ begin
+ Writeln($'Активация обработчика ивента {ev}, зарегестрированного для состояния {st}');
+ end;
+
+ var ev: cl_event; //ToDo := ...
+
+ cl.SetEventCallback(ev, CommandExecutionStatus.COMPLETE, cb, IntPtr.Zero).RaiseIfError;
+end.
+
+Этот код может время от времени вылетать, потому что:
+cl.SetEventCallback
вызывает свой коллбек тогда, когда посчитает нужным (но обычно после того как вызов cl.SetEventCallback
завершился);
Делегаты - это классы. А сборщик мусора распоряжается памятью классов и удаляет их, тоже когда посчитает нужным.
+Раз после вызова cl.SetEventCallback
делегат cb
больше нигде не используется - сборщик мусора может в любой момент
+решить удалить его. Но, опять же, это редко случается сразу после вызова cl.SetEventCallback
,
+поэтому ошибки связанные с этим удалением могут быть плавающие.
Если сборщик мусора удалит делегат, а затем .dll попытается его вызвать - +это приведёт или к ошибке доступа, или к моментальному беззвучному вылету.
+Чтобы запретить сборщику мусора удалять делегать - нужно создать GCHandle
, привязанный к нему.
+Но в отличии от массивов - GCHandleType.Pinned
не нужно, потому что сборщик мусора
+не может перемещать адрес исполняемого кода (а он единственное что передаётся в .dll).
+Это потому, что этот адрес хранится в виде указателя на неуправляемый код.
uses System.Runtime.InteropServices;
+uses System;
+uses OpenCL;
+
+begin
+
+ var gc_hnd: GCHandle;
+ var cb: EventCallback := (ev,st,data)->
+ begin
+ // В данном случае делегат вызывается одноразово
+ // И пока он выполняется - сборщик мусора его не удалит
+ // Поэтому освобождение GCHandle можно поставить в любой части самого обработчика
+ gc_hnd.Free;
+
+ Writeln($'Активация обработчика ивента {ev}, зарегестрированного для состояния {st}');
+
+ end;
+ gc_hnd := GCHandle.Alloc(cb);
+
+ var ev: cl_event; //ToDo := ...
+
+ cl.SetEventCallback(ev, CommandExecutionStatus.COMPLETE, cb, IntPtr.Zero).RaiseIfError;
+end.
+
+Данная справка относится к модулю OpenCLABC
, входящему в состав стандартных модулей языка PascalABC.NET
.
Модуль OpenCLABC
это высокоуровневая обёртка модуля OpenCL
.
+Это значит, что с OpenCLABC
можно писать гораздо меньше кода в больших и сложных программах,
+однако такой же уровень микроконтроля как с модулем OpenCL
недоступен.
+Например, напрямую управлять cl_event
'ами в OpenCLABC
невозможно.
+Вместо этого надо использовать операции с очередями (например, сложение и умножение очередей)
Справка модуля OpenCL
отсутствует. Вместо неё смотрите:
OpenGL
и OpenCL
OpenCL
, на которой основан модуль OpenCL
Если в справке или модуле найдена ошибка, или чего-либо не хватает - пишите в issue.
+CPU — Центральное Процессорное Устройство (процессор);
+GPU — Графическое Процессорное Устройство (видеокарта);
+RAM — Оперативная память;
+Команда — запрос на выполнение чего-либо. К примеру:
+Называть процедуры и функции командами ошибочно!
+Подпрограмма — процедура или функция;
+Метод — особая подпрограмма, вызываемая через экземпляр:
+Context.SyncInvoke
выглядит в коде как cont.SyncInvoke(...)
, где cont
— переменная типа Context
;Остальные непонятные термины можно найти в справке PascalABC.NET
или в интернете.
Сам модуль OpenCLABC
- обёртка модуля OpenCL
. Это значит, что внутри он использует
+содержимое OpenCL
, но при подключении - показывает только свой личный функционал.
Так же множество типов из OpenCLABC
являются обёртками типов из OpenCL
.
Тип CommandQueue
использует тип cl_command_queue
, но предоставляет очень много не связанного с cl_command_queue
функционала.
"Простые" типы-обёртки модуля OpenCLABC
предоставляют только функционал соответствующего
+типа из модуля OpenCL
... в более презентабельном виде. Из общего - у таких типов есть:
Свойство .Native
, возвращающее внутренний неуправляемый объект из модуля OpenCL
.
+Если вам не пришлось, по какой-либо причине, использовать OpenCLABC
и OpenCL
вместе - это свойство может понадобится только для дебага.
Свойство .Properties
, возвращающее объект свойств внутреннего объекта.
+Свойства неуправляемого объекта никак не обрабатываются и не имеют описаний.
+Но типы этих свойств всё равно преобразуются в управляемые (особо заметно на строках и массивах).
В списке устройств контекста могут быть только совместимые друг с другом устройства.
+Коллекция совместимых устройств называется платформой и хранится в объектах типа Platform
.
Обычно платформы получают из статического свойства Platform.All
:
## uses OpenCLABC;
+var pls := Platform.All;
+pls.PrintLines;
+
+Обычно устройства получают статическим методом Device.GetAllFor
:
## uses OpenCLABC;
+
+foreach var pl in Platform.All do
+begin
+ Writeln(pl);
+ var dvcs := Device.GetAllFor(pl, DeviceType.DEVICE_TYPE_ALL);
+ if dvcs<>nil then dvcs.PrintLines;
+ Writeln('='*30);
+end;
+
+И в большинстве случаев - это всё что вам понадобится.
+Но если где то нужен более тонкий контроль - можно создать несколько виртуальных
+под-устройств, каждому из которых даётся часть ядер изначального устройства.
+Для этого используются методы .Split*
:
## uses OpenCLABC;
+
+var dvc := Context.Default.MainDevice;
+
+Writeln('Поддерживаемые типы .Spilt-ов:');
+var partition_properties := dvc.Properties.PartitionProperties;
+if (partition_properties.Length=0) or (partition_properties[0].val = System.IntPtr.Zero) then
+begin
+ Writeln('Ничего не поддерживается...');
+ exit;
+end else
+ partition_properties.PrintLines;
+Writeln('='*30);
+
+Writeln('Виртуальные устройства, по 1 ядру каждое:');
+if dvc.CanSplitEqually then
+ // Если упадёт потому что слишком много
+ // устройств - пожалуйста, напишите в issue
+ dvc.SplitEqually(1).PrintLines else
+ Writeln('Не поддерживается...');
+Writeln('='*30);
+
+Writeln('Два устройства, 1 и 2 ядра соответственно:');
+if dvc.CanSplitByCounts then
+ dvc.SplitByCounts(1,2).PrintLines else
+ Writeln('Не поддерживается...');
+Writeln('='*30);
+
+
+Контекст (объект типа Context
) содержит информацию об устройствах,
+на которых надо выделяет области памяти OpenCL
и выполнять код для GPU.
Создать контекст можно конструктором (new Context(...)
).
+Контекст можно и не создавать, используя везде свойство Context.Default
.
Если в операции, требующей контекст (как выделение памяти GPU),
+его не указать - будет автоматически выбран Context.Default
.
+Неявные очереди всегда используют Context.Default
.
Изначально этому свойству присваивается контекст, использующий GPU, если оно имеется, +или любое другое устройство, поддерживающее OpenCL, если GPU отсутствует.
+Если устройств поддерживающих OpenCL
нет - Context.Default
будет nil
.
+Однако такая ситуация на практике невозможна, потому что OpenCL поддерживается
+практически всеми современными устройствами, занимающимися выводом изображения на экран.
+Если Context.Default = nil
- переустановите графические драйверы.
Context.Default
можно перезаписывать.
+Используйте эту возможность только если во всей программе нужен общий контекст, но не стандартный.
Если ваша программа достаточно сложная чтобы нуждаться в нескольких контекстах - лучше не использовать
+Context.Default
. И присвоить ему nil
, чтобы не использовать случайно (к примеру, неявной очередью).
Объект типа Kernel
представляет одну подпрограмму в OpenCL-C коде,
+объявленную с ключевым словом __kernel
.
Обычно Kernel
создаётся через индексное свойтсво ProgramCode
:
var code: ProgramCode;
+...
+var k := code['KernelName'];
+
+Тут 'KernelName'
— имя подпрограммы-kernel'а в исходном коде (регистр важен!).
Так же можно получить список всех kernel'ов объекта ProgramCode
, методом ProgramCode.GetAllKernels
:
var code: ProgramCode;
+...
+var ks := code.GetAllKernels;
+ks.PrintLines;
+
+Обычные программы невозможно запустить на GPU. Для этого надо писать особые программы.
+В контексте OpenCL - эти программы обычно пишутся на языке "OpenCL C" (основанном на языке "C").
Язык OpenCL-C это часть библиотеки OpenCL, поэтому его справку можно найти там же, где и справку OpenCL.
+В OpenCLABC
OpenCL-C код хранится в объектах типа ProgramCode
.
+Объекты этого типа используются только как контейнеры.
+Один объект ProgramCode может содержать любое количествово подпрограмм-kernel'ов.
Самый простой способ создать ProgramCode
- конструктором:
var code := new ProgramCode(
+ ReadAllText('file with code 1.cl'),
+ ReadAllText('file with code 2.cl')
+);
+
+Внимание! Этот конструктор принимает именно тексты исходников, не имена файлов.
+Если надо передать текст из файла - его надо сначала явно прочитать.
Так же как исходники паскаля хранят в .pas файлах, исходники OpenCL-C кода хранят в .cl файлах.
+На самом деле это не обязательно, потому что код даже не обязан быть в файле:
+var code_text := '__kernel void k() {}';
+var code := new ProgramCode(code_text);
+
+Так как конструктор ProgramCode
принимает текст - исходники программы
+на языке OpenCL-C можно хранить даже в строке в .pas программе.
Тем не менее, хранить исходники OpenCL-C кода в .cl файлах обычно удобнее всего.
+После создания объекта типа ProgramCode
из исходников можно вызвать
+метод ProgramCode.SerializeTo
, чтобы сохранить код в бинарном и прекомпилированном виде.
Затем, объект ProgramCode
можно пере-создать статический метод ProgramCode.DeserializeFrom
.
Пример можно найти в папке примеров Прекомпиляция ProgramCode
или тут.
Типы-простые_обёртки памяти OpenCL (cl_mem
) рассмотрены в
+этой папке.
Методы, запускающие Kernel
принимают специальные аргументы типа KernelArg
, которые передаются в OpenCL-C код.
+Экземпляр KernelArg
может быть создан из нескольких типов значений, а точнее:
## uses OpenCLABC;
+
+var k: Kernel;
+var val1 := 3;
+var val2 := 5;
+var val3 := new NativeValue<byte>(7);
+var a: array of byte;
+
+k.Exec1(1,
+ // Передавать можно:
+
+ // Области памяти
+ new CLArray<byte>(1),
+ new MemorySegment(1),
+ // Очереди возвращающие область памяти
+ HFQ(()->new MemorySegment(1)),
+ // В том числе CLArrayCCQ и т.п.
+ CLArray&<byte>.Create(1).NewQueue,
+
+ // Указатель на размерное значение
+ // (в kernel попадёт само значение, не указатель)
+ @val2,
+ // Так нельзя, потому что val1 была захвачена лямбдой из HFQ ниже
+// @val1,
+ // Расширенный набор параметров для передачи адреса
+ KernelArg.FromData(new System.IntPtr(@val2), new System.UIntPtr(sizeof(integer))),
+
+ // Размерное значение
+ val1,
+ HFQ(()->val1),
+
+ // В том числе неуправляемое
+ val3,
+ HFQ(()->val3),
+
+ // Массив размерных значений
+ a,
+ HFQ(()->a)
+
+);
+
+Обратите внимание, KernelArg
из указателя на val2
будет немного эффективнее
+чем KernelArg
из самого значения val2
. Но эту возможность стоит использовать
+только как тонкую оптимизацию, потому что много чего может пойти не так.
+Если передавать @val2
в качестве KernelArg
- надо знать все тонкости.
Если @x
передали вкачестве KernelArg
:
x
не может быть глобальной переменной или полем класса:
uses OpenCLABC;
+
+type
+ t1 = class
+ val1 := 1; // Не подходит, потому что поле класса
+ static val2 := 2; // И статических полей это тоже касается
+ end;
+
+var
+ val3 := 3; // Глобальные переменные - тоже статические поля
+ k: Kernel; // k не важно где объявлена
+
+procedure p1;
+// Теоретически подходит, но вообще это плохой стиль кода
+var val4 := 4;
+begin
+
+ // Однозначно подходит, потому что объявлена не только в той же
+ // подпрограмме, но и прямо перед использованием в k.Exec*
+ var val5 := 5;
+
+ k.Exec1(1,
+
+ // Это единственные 2 переменные, которые можно передавать адресом
+ @val4,
+ @val5
+
+ );
+
+end;
+
+begin end.
+
+x
не должно быть захвачего лямбдой.
+Хотя указатель на x
уже можно захватывать:
## uses OpenCLABC;
+
+var k: Kernel;
+var val1 := 3;
+
+// На val1 всё ещё накладываются все ограничения,
+// когда val1_ptr использована в качестве KernelArg
+// Но к самой val1_ptr эти ограничения не применяются
+var val1_ptr := @val1;
+
+k.Exec1(1,
+
+ val1,
+ HFQ(()->val1_ptr^), // Захватили переменную val1_ptr, а не val1
+
+ // val1 нигде не захвачена, поэтому теперь так можно
+ @val1,
+ val1_ptr // То же самое
+
+);
+
+Выходить из подпрограммы, где объявили x
нельзя, пока .Exec
не закончит выполнятся.
+Это так же значит, что возвращать очередь, содержащую KernelArg
из @x
обычно нельзя.
+Но это можно обойти, если объявлять переменную в другой подпрограмме:
uses OpenCLABC;
+
+var k: Kernel; // Вообще лучше передавать параметром в p2
+
+function p2(var val: integer): CommandQueueBase;
+begin
+
+ Result := k.NewQueue.AddExec1(1,
+
+ @val
+
+ );
+
+end;
+
+procedure p1;
+begin
+ var val: integer;
+
+ var q := p2(val);
+ // Опять же, q не должна продолжать выпоняться
+ // после выхода из p1, потому что тут объявлена val
+ Context.Default.SyncInvoke(q);
+
+end;
+
+begin
+ p1;
+end.
+
+Тут val
объявлена в p1
. При этом val
в p2
является синонимом val
из p1
,
+потому что объявлена var
-параметром - передачей по ссылке.
+Если передать без var
перед параметром - val
из p2
будет копией,
+а значит перестанет существовать после выхода из p2
.
Обратите внимание: Компилятор не заставит вас следовать описанным тут ограничениям.
+И показанные на этой странице коды могут работать не смотря на эти ограничения,
+потому что саму область памяти, на которую ссылается созданный KernelArg
, никто никогда не уничтожает.
Но в то же время эту же область памяти может использовать под другие данные, таким образом +заменяя значение которое вы пытались передать в GPU на мусорные данные.
+Это плохо в первую очередь потому, что на перезапись этих данных может повлиять совершенно не связанная +часть кода, или различие архитектур компьтеров, таким образом усложняя поиск источника ошибки.
+Поэтому, ещё раз, используйте передачу адреса в качестве KernelArg
только как тонкую оптимизацию и только когда понимаете что делаете.
OpenCL это неуправляемая библиотека. Обычно можно заставить управляеммые типы данных работать с ней.
+Но часто это приводит к дополнительным затратам производительности.
+И обычно не значит всегда - MemorySegment.ReadValue
, принимающее запись var
-параметром не может быть безопасным из за сборщика мусора
+(и поэтому отсутствует).
Более прямым будет передача неуправляемых типов - указателей - без преобразований в подпрограммы модуля OpenCL
.
+И эта возможность тоже существует, к примеру в виде MemorySegment.WriteData
. Но такие указатели ещё более не_безопасны:
+Как минимум они требуют освобождения в try-finally
чтобы избежать утечек памяти.
+И защиты от дурака, не возволяющей записать значение типа real
туда, где хранится int64
- не существует.
Как что-то среднее между этими двумя вариантами - существует NativeValue<T>
:
+Этот класс является обёрткой указателя на область памяти RAM.
## uses OpenCLABC;
+
+// В конструктор необходимо передавать значение,
+// потому что иначе неуправляемая память будет содержать мусор
+// Но можно передать default, чтобы заполнить выделяемую память нулями
+var nv := new NativeValue<integer>(default(integer));
+
+nv.Value := 5; // Перезаписываем значение,
+nv.Value.Println; // И читаем назад
+
+// Напрямую получать доступ к области памяти,
+// через свойство Pointer, не рекомендуется
+Writeln(nv.Pointer);
+// Кроме как передавать nv.Pointer^ var-параметром
+
+nv.Dispose; // Освобождение памяти - вызывается и само при сборке мусора
+
+Всё преимущество GPU над CPU состоит в выполнении одинаковой программы на рое из процессорных ядер.
+Каждое из этих ядер значительно медленнее ядер CPU, но используя весь рой вместе можно выполнять вычисления значительно быстрее.
+Но раз все ядра GPU выполняют одну и ту же программу - им обычно приходится передавать массив из однотипных данных.
Объект типа CLArray<T>
(где T
- почти любая запись) является массивом, содержимое которого хранится в памяти GPU, для более быстрого доступа из кода на GPU.
CLArray<T>
создаётся конструктором:
## uses OpenCLABC;
+
+// Массив для 10 элементов типа integer
+var a1 := new CLArray<integer>(Context.Default, 10);
+// Заполняем нулями, потому что при создании CLArray может содержать мусорные данные
+a1.FillValue(0);
+a1.GetArray.Println;
+
+// Массив, в который при создании копируются данные из переданного массива
+var a2 := new CLArray<byte>(Context.Default, new byte[](1,2,3,4,5,6,7));
+a2.GetArray.Println;
+
+// Массив, в который при создании копируется часть данных из переданного массива
+// Точнее, копируется 2 элемента, начиная с элемента с индексом 3
+var cpu_array := |0,1,2,3,4,5,6,7|;
+var a3 := new CLArray<integer>(Context.Default, 3, 2, cpu_array);
+a3.GetArray.Println;
+
+Кроме того, объект контекста можно не передавать, тогда для выделения памяти будет автоматически выбран Context.Default
.
Элементы этого массива, так же как у обычных массивов, можно читать через индексное свойство:
+## uses OpenCLABC;
+var a := new CLArray<integer>(Context.Default, |0,1,4,9,16|);
+a[3].Println;
+a[4] := 5;
+a[4].Println;
+
+Но имейте в виду - каждое обращение к индексному свойству - это новое выполнение неявной очереди, поэтому заполнять или читать такой массив по 1 элементу не эффективно.
+Лучше всего использовать полноценные очереди. Но если вы выполняете какое-то одноразовое заполнение - можно, хотя бы, использовать свойство .Section
:
## uses OpenCLABC;
+var a := new CLArray<integer>(Context.Default, |0,1,4,9,16|);
+// Заполняем элементы с индексами от 2 до 3 массивом |-1,-2|
+a.Section[2..3] := |-1,-2|;
+// Читаем и выводим элементы с 0 по 2 индексы как массив
+a.Section[0..2].Println;
+
+Некоторые данные, передаваемые в программы на GPU, могут быть общими (для всех ядер) или просто не однотипными.
+Такие данные можно хранить в типе MemorySegment
, представляющем область памяти GPU, для которой не определено ничего кроме размера.
Область памяти MemorySegment
создаётся конструктором (new MemorySegment(...)
).
+Основные конструкторы принимают размер области памяти и, опционально, контекст. Если контекст не указывается - память выделяется на Context.Default
.
Далее, в неё можно записывать и читать данные практически любого типа записи, в любой точке:
+## uses OpenCLABC;
+
+var s := new MemorySegment(sizeof(int64));
+// $ Это запись числа в 16-ичной СС
+s.WriteValue&<int64>($FEDCBA9876543210);
+// .ToString('X') преобразовывает число в строку в 16-ичной СС
+s.GetValue&<int64>.ToString('X').Println;
+// Можно читать кусок данных одного типа, как данные другого типа
+s.GetValue&<integer>.ToString('X').Println;
+// Читаем начиная с четвёртого байта
+// Я сделал 16-ичный вывод чтобы было видно, изначальное число разрезало пополам
+s.GetValue&<integer>(4).ToString('X').Println;
+// Ну и никто не запретит прочитать как совершенно не совместимый тип данных
+// Но в ответ вы получите мусорные данные
+s.GetValue&<real>.Println;
+
+То есть, вам самим придётся заботится о типе внутренних данных, работая с MemorySegment
, но взамен вы полностью контролируете содержимое области памяти.
Память на GPU можно моментально освободить, вызвав метод MemorySegment.Dispose
.
+Но вызывать его не обязательно, потому что этот метод вызывается автоматически во время сборки мусора.
Но кроме выделения памяти на GPU - OpenCL так же позволяет выделять память внутри другой области памяти.
+Для этого используется тип MemorySubSegment
:
## uses OpenCLABC;
+
+var c := Context.Default;
+
+// Не обязательно MainDevice, можно взять любое устройство из контекста
+var align := c.MainDevice.Properties.MemBaseAddrAlign;
+
+var s := new MemorySegment(align*2, c);
+// size может быть любым, но origin
+// должно быть align*N, где N - целое
+var s1 := new MemorySubSegment(s, 0, Min(123,align));
+var s2 := new MemorySubSegment(s, align, align);
+
+Writeln(s1);
+Writeln(s2);
+
+Передавать команды для GPU по одной не эффективно. +Гораздо эффективнее передавать несколько команд сразу.
+Для этого существуют очереди (типы, наследующие от CommandQueueBase
).
+Они хранят произвольное количество команд для GPU.
+А при необходимости также и части кода, выполняемые на CPU.
Все типы очередей наследует от CommandQueueBase
. Это значит что любую очередь можно сохранить в переменную типа CommandQueueBase
.
+Но о значении типа CommandQueueBase
известно не на много больше чем о значении типа object
.
Так же, все очереди наследуют от одного из двух типов:
+CommandQueueNil
- очередь возващающая nil
(именно нулевую ссылку, не пустое значение любого типа).CommandQueue<T>
(где T
- любой тип) - очередь возвращающая значение типа T
;После выполнения очереди CommandQueue<T>
метод Context.SyncInvoke
возвращает то, что вернула очередь.
+А если использовать метод Context.BeginInvoke
- возвращаемое значение можно получить с помощью метода CLTask<T>.WaitRes
.
Результат других типов очередей нельзя получить, но их можно преобразовать к CommandQueue<T>
с произвольным T
с помощью .Cast
:
## uses OpenCLABC;
+
+// Q объявлена как CommandQueueBase,
+// а значит в неё можно сохранить любую очередь
+var Q: CommandQueueBase;
+
+// В данном случае сохраняем CommandQueueNil
+Q := HPQ(()->Writeln('Q выполнилась'));
+
+// Преобразовывать nil можно в любой ссылочный тип
+Writeln(Context.Default.SyncInvoke( Q.Cast&<object> ));
+// Exception тоже класс - поэтому можно и в него
+// Но в результате всё равно получится nil
+Writeln(Context.Default.SyncInvoke( Q.Cast&<Exception> ));
+
+Sleep(1000); // Чтобы было видно предыдущий вывод
+//Ошибка времени выполнения: .Cast не может преобразовывать nil в integer
+// Ошибка кидается ещё в момент создания .Cast очереди
+Writeln(Context.Default.SyncInvoke( Q.Cast&<integer> ));
+
+Подробнее тут.
+В то же время, если результат выполнения очереди не нужен,
+от него можно избавится с помощью .DiscardResult
.
Это может понадобится если вы хотите:
+CommandQueueNil
;## uses OpenCLABC;
+
+var Q := HFQQ(()->
+begin
+ Writeln('Функция выполнилась');
+ Result := 5;
+end).DiscardResult;
+
+var t := Context.Default.BeginInvoke(Q as CommandQueueBase);
+$'Has result: {not (t is CLTaskNil)}'.Println;
+
+t.Wait;
+
+Без .DiscardResult
- BeginInvoke
создаст CLTask<integer>
,
+не смотря на то, что в него передают CommandQueueBase
.
А с .DiscardResult
- HFQQ
выполняется как будто это HPQQ
.
Обратите внимание, если результат очереди выкидывается, без +какого-либо использования - ресурсы на него не выделяются. К примеру:
+## uses OpenCLABC;
+
+Context.Default.SyncInvoke(
+ HFQQ(()->1) +
+ HFQQ(()->2)
+);
+
+Первая HFQQ
выполнится как будто это HPQQ
, потому что
+из SyncInvoke
можно получить только результат второй HFQQ
.
Очереди, созданные из областей памяти OpenCL или kernel'ов возващают свои области памяти/Kernel
'ы соответственно, из которых были созданы;
+Очереди, созданные с HF[Q]Q
- значение, которое вернёт переданная функция;
+Очереди, созданные с HP[Q]Q
являются CommandQueueNil
.
Демонстрация:
+## uses OpenCLABC;
+
+/// Вывод типа и значения объекта
+procedure OtpObject(o: object) :=
+$'{if o=nil then nil else TypeName(o)}[{_ObjectToString(o)}]'.Println;
+// _ObjectToString это функция, которую используют
+// Writeln и Println для форматирования значений
+
+procedure Test(q: CommandQueueBase) :=
+OtpObject(Context.Default.SyncInvoke(
+ // Преобразовываем результат к object, чтобы его вернула SyncInvoke
+ q.Cast&<object>
+));
+
+var s := new MemorySegment(1);
+
+// Тип - MemorySegment, потому что очередь создали из него
+Test( s.NewQueue );
+
+// Тип - integer, потому что это тип по умолчанию для выражения (5)
+Test( HFQQ(()->5) );
+
+// Тип - string, по той же причине
+Test( HFQQ(()->'abc') );
+
+// Тип отсутствует, потому что HP[Q]Q возвращает nil
+Test( HPQQ(()->Print('Выполнилась HPQQ:')) );
+
+Проверить что очередь ничего не возвращает очень просто:
+var Q: CommandQueueBase;
+...
+if Q is CommandQueueNil(var cqn) then
+ p1(cqn) else
+ p2(Q);
+
+Но для типа CommandQueue<T>
надо указать конкретный тип, чтобы вызвать is
.
+Другими словами, с помощью is
можно проверять только по одному типу возвращаемого значения за раз:
var Q: CommandQueueBase;
+...
+if Q is CommandQueueNil(var cqn) then
+ p1(cqn) else
+if Q is CommandQueue<byte>(var cq) then
+ p2&<byte>(cq) else
+if Q is CommandQueue<word>(var cq) then
+ p2&<word>(cq) else
+ // Не должно происходить
+ raise new System.NotSupportedException;
+
+Если надо вызвать p2
для очереди с любым возвращаемым значением - используется .UseTyped
:
uses OpenCLABC;
+
+procedure p1(cq: CommandQueueNil) := Writeln('nil');
+procedure p2<T>(cq: CommandQueue<T>) := Writeln($'<{TypeToTypeName(typeof(T))}>');
+
+type
+ // Не обязательно запись
+ TypedUser = record(ITypedCQUser)
+
+ public procedure UseNil(cq: CommandQueueNil) := p1(cq);
+ public procedure Use<T>(cq: CommandQueue<T>) := p2(cq);
+
+ end;
+
+procedure Test(Q: CommandQueueBase) :=
+Q.UseTyped(new TypedUser);
+
+begin
+ Test(HPQ(()->begin end));
+ Test(HFQ(()->0));
+ Test(HFQ(()->0.0));
+end.
+
+Объявлять дополнительный тип (TypedUser
в этом коде) необходимо потому, что иначе
+передать подпрограмму Use<T>
, не указывая её <T>
, в UseTyped
не получится.
Так же, если нужно не только использовать очередь, но и что-то вернуть - используется .ConvertTyped
:
uses OpenCLABC;
+
+type
+ // Получает имя типа результата очереди, или nil если он отсутствует
+ QueueConverterResTName = record(ITypedCQConverter<string>)
+
+ public function ConvertNil(cq: CommandQueueNil): string := nil;
+ public function Convert<T>(cq: CommandQueue<T>): string := TypeToTypeName(typeof(T));
+
+ end;
+
+procedure Test(Q: CommandQueueBase) :=
+Writeln( Q.ConvertTyped(new QueueConverterResTName) );
+
+begin
+ Test(HPQ(()->begin end));
+ Test(HFQ(()->0));
+ Test(HFQ(()->0.0));
+end.
+
+И .UseTyped
и .ConvertTyped
гарантируют что обязательно будет вызван ровно один
+из двух методов - либо принимающий CommandQueueNil
, либо принимающий CommandQueue<T>
.
Самый простой способ выполнить очередь - вызвать метод Context.SyncInvoke
.
У него есть три перегрузки, для CommandQueueBase
, CommandQueueNil
и CommandQueue<T>
.
+Только последняя возвращает результат.
Но если надо выполнить очередь асинхронно - лучше использовать метод Context.BeginInvoke
,
+потому что его всё равно вызывает Context.SyncInvoke
.
Context.BeginInvoke
запускает асинхронное выполнение очереди.
+Как только очередь была полностью запущена он возвращает объект типа
+CLTaskBase
, CLTaskNil
или CLTask<T>
для соответствующих типов очередей.
Так же как в случае очередей, CLTaskNil
и CLTask<T>
наследуют от CLTaskBase
.
+У всех CLTask
-ов есть:
.OrgContext
и .OrgQueue
, возвращающие контекст выполнения и выполняемую очередь соответственно..Wait
для ожидания окончания выполнения очереди.У CLTask<T>
так же есть метод .WaitRes
, вызывающий .Wait
и затем возвращающий результат очереди.
При выполнении очереди может произойти несколько исключений, поэтому, чтобы получить можно было все, их упаковывает в System.AggregateException
:
## uses OpenCLABC;
+
+try
+
+ // Context.SyncInvoke или CLTask.Wait
+ Context.Default.SyncInvoke(Q);
+
+except
+ on e: System.AggregateException do
+ e.InnerExceptions.PrintLines;
+end;
+
+Но для более тонкого контроля лучше использовать
+.Handle*
методы очередей.
OpenCL используется для передачи комманд в GPU.
+Поэтому в первую очередь стоит поговорить об очередях OpenCLABC
, содержащих комманды для GPU.
Такие очереди создаются из некоторых типов-простых_обёрток методом .NewQueue
и имеют тип с припиской CCQ
.
+("Command Container Queue", то есть очередь-контейнер для коман GPU)
+К примеру, метод CLArray<byte>.NewQueue
вернёт очередь типа CLArrayCCQ<byte>
, наследующего от CommandQueue< CLArray<byte> >
.
Чтобы создать саму комманду - надо вызвать соответствующий ей метод CCQ
объекта. К примеру:
## uses OpenCLABC;
+
+// Массив на 3 элемента типа integer
+var a := new CLArray<integer>(3);
+
+// Создаём очередь
+var q: CLArrayCCQ<integer> := a.NewQueue;
+
+Context.Default.SyncInvoke(q
+ .ThenWriteValue(1, 0)
+ // Записывать по 1 элементу не эффективно
+ // Лучше сначала создать массив в RAM
+ // А затем послать его целиком
+ .ThenWriteArray(|5,7|, 1, 2,0)
+ // .ThenGet методы особенные, потому что не возвращают CCQ объект
+ // В данном случае эта комманда читает весь CLArray как обычный массив в RAM
+ .ThenGetArray
+).Println;
+
+Также, CCQ
очереди можно создавать из очередей, возвращающих объект с командами. Для этого используется конструктор:
var q0: CommandQueue<MemorySegment>;
+...
+var q := new MemorySegmentCCQ(q0);
+
+Команды объектов, представляющих память на GPU, можно разделить на группы.
+По направлению передачи:
+Write
и Fill
: Из RAM в память GPU;Read
и Get
: Из памяти GPU в RAM;Copy
: Между двумя областями памяти GPU.И по типу данных на стороне RAM:
+Data
: Используются данные, находящиеся в RAM по указанному адресу;Value
: Используется размерное значение;Array
: Используется содержимое указанного массива размерных значений.Но при этом отсутствуют некоторые комбинации.
+В первую очередь, в случае Copy
нет понятия типа данных на стороне RAM, потому что RAM в принципе не задействуется.
И нет GetData
, потому что в случае ошибок будут утечки памяти.
+Вместо него выделяйте память явно и передавайте её в ReadData
.
Так же, WriteValue
может принимать размерное значение и NativeValue
, но ReadValue
принимает только NativeValue
.
+Это потому, что принимать размерное значение в ReadValue
var
-параметром не безопасно,
+как и в случае передачи адреса в качестве KernelArg
.
+Если вы понимаете что делаете - используйте ReadData
, явно передавая в него адрес вашего значения (то есть указатель).
+Но обычно лучше использовать GetValue
, создающее и возвращающее новое размерное значение, либо ReadValue
принимающее NativeValue
.
Кроме таких объектов, методы-команды для GPU есть только у Kernel
. И все они представляют запуск kernel'а.
Комманда .ThenGet
не выполняется, если её результат выкидывается.
+Подробнее тут.
Это оптимизация, которую вы можете заметить только в одном случае: +Если параметры этой команды были неправильные - ошибка не возникнет.
+Константы в любом контексте позволяют проводить особые оптимизации.
+В случае OpenCLABC
- константные очереди являются константами только на время выполнения очереди.
## uses OpenCLABC;
+
+var cq := new ConstQueue<integer>(1);
+Context.Default.SyncInvoke(cq).Println;
+
+cq.Value := 2;
+Context.Default.SyncInvoke(cq).Println;
+
+Обратите внимание, .Value
нельзя изменять пока .BeginInvoke
не досоздаст CLTask
. Иначе поведение неопределено.
Так же, константную очередь можно создать присвоив значение результата туда, где ожидается очередь:
+var q: CommandQueue<integer> := 1;
+
+Для этого тип значения (1
) и результата очереди <integer>
должны полностью совпадать. К примеру так сделать не позволит:
//Ошибка: Нельзя преобразовать тип integer к CommandQueue<object>
+var q: CommandQueue<object> := 1;
+
+Чтобы можно было присвоить - значение надо явно преобразовать к object
:
var q: CommandQueue<object> := object(1);
+
+Этот способ создания константной очереди обычно используется подпрограммами, принимающими очереди:
+## uses OpenCLABC;
+
+procedure p1(q: CommandQueue<integer>) :=
+Context.Default.SyncInvoke(q).Println;
+
+p1(1);
+p1(HFQQ(()->2));
+
+В OpenCLABC
есть множество подпрограмм, принимающий очередь вместо значения.
+Подробнее.
Если нужно сохранить константную очередь в переменную, можно написать:
+var q1 := new ConstQueue<integer>(1);
+var q2: CommandQueue<integer> := 3;
+
+Но оба способа объёмны и, что важнее, требуют указать тип значения явно.
+Поэтому в OpenCLABC
есть вспомогательная подпрограмма:
var q3 := CQ(3);
+
+У очередей есть возвращаемое значение, но чтобы передавать в них данные при запуске - можно +или создавать новую очередь на каждое выполнение (что очень не эффективно), или выкручиваться так:
+uses OpenCLABC;
+
+type
+ MyQueueContainer = sealed class
+ private Q: CommandQueue<integer>;
+ private par1 := new ConstQueue<integer>(-1);
+ private par2 := new ConstQueue<string>(nil);
+
+ // Эта очередь ничего полезного не делает, но это только пример
+ public constructor := self.Q :=
+ MemorySegment.Create(sizeof(integer)).NewQueue
+ .ThenWriteValue( self.par1 )
+ .ThenQueue( self.par2.ThenQuickUse(x->Writeln(x)) )
+ .ThenGetValue&<integer>;
+
+ public function Invoke(par1: integer; par2: string): integer;
+ begin
+ var tsk: CLTask<integer>;
+ // Нужна блокировка, чтобы если метод Invoke будет выполнен
+ // в нескольких потоках одновременно, .Value параметров
+ // не могло поменяться пока Context.BeginInvoke создаёт CLTask
+ lock self do
+ begin
+ self.par1.Value := par1;
+ self.par2.Value := par2;
+ tsk := Context.Default.BeginInvoke(Q);
+ end;
+
+ Result := tsk.WaitRes;
+ end;
+
+ end;
+
+begin
+ var cont := new MyQueueContainer;
+
+ cont.Invoke(1, 'abc').Println;
+ cont.Invoke(2, 'def').Println;
+
+end.
+
+Но это не очень красиво. Можно сделать красивее, используя специальную очередь-параметр.
+ParameterQueue<T>
тоже является константной во время выполнения очереди, но используется по-другому:
uses OpenCLABC;
+
+type
+ MyQueueContainer = sealed class
+ private Q: CommandQueue<integer>;
+ private par1 := new ParameterQueue<integer>('par1');
+ private par2 := new ParameterQueue<string>('par2');
+
+ // Эта очередь ничего полезного не делает, но это только пример
+ public constructor := self.Q :=
+ MemorySegment.Create(sizeof(integer)).NewQueue
+ .ThenWriteValue( self.par1 )
+ .ThenQueue( self.par2.ThenQuickUse(x->Writeln(x)) )
+ .ThenGetValue&<integer>;
+
+ public function Invoke(par1: integer; par2: string) :=
+ Context.Default.SyncInvoke(self.Q,
+ self.par1.NewSetter(par1),
+ self.par2.NewSetter(par2)
+ );
+
+ end;
+
+begin
+ var cont := new MyQueueContainer;
+
+ cont.Invoke(1, 'abc').Println;
+ cont.Invoke(2, 'def').Println;
+
+end.
+
+Таким образом код получается значительно чище.
+Кроме того, при запуске очереди проводятся проверки, чтобы очереди-параметры
+не оказывались не_определены или установлены дважды.
Иногда между командами для GPU надо вставить выполнение обычного кода на CPU. +А разрывать для этого очередь на две части - плохо, потому что +одна целая очередь всегда выполнится быстрее двух её частей.
+Поэтому существует множество типов очередей, хранящих обычный код для CPU.
+Чтобы создать самую простую такую очередь используются глобальные подпрограммы HFQ
и HPQ
:
HFQ — Host Function Queue
+HPQ — Host Procedure Queue
+(Хост в контексте OpenCL - это CPU, потому что с него посылаются команды для GPU)
Они возвращают очередь, выполняющую код (функцию/процедуру соотвественно) на CPU.
+Пример применения приведён на странице выше.
Так же бывает нужно использовать результат предыдущей очереди в коде на CPU.
+Для этого используются методы .ThenUse
и .ThenConvert
:
## uses OpenCLABC;
+var Q := HFQ(()->5);
+
+Context.Default.SyncInvoke(Q
+ .ThenUse(x->Println($'x*2 = {x*2}'))
+ .ThenConvert(x->$'x^2 = {x**2}')
+).Println;
+
+.ThenUse
дублирует возвращаемое значение предыдущей очереди (Q
в примере).
+А .ThenConvert
возвращает результат выполнения переданной функции, как и HFQ
.
OpenCLABC
очереди существуют чтобы можно было удобно описывать параллельные процедуры.
+Поэтому, с расчётом на параллельность, обычные очереди с кодом для CPU создают себе по одному потоку выполнения (Thread
) при запуске.
Этот поток выполнения запускается до выхода из Context.BeginInvoke
и остаётся в режиме ожидая.
+Но даже если игнорировать затраты на запуск потока, выход из режима ожидания это не моментальня операция.
Если надо выполнить очень простое действие, как в последнем примере выше, эти затраты неоправданны.
+Для таких случаев используется Quick
версии очередей:
## uses OpenCLABC;
+var Q := HFQQ(()->5);
+
+Context.Default.SyncInvoke(Q
+ .ThenQuickUse(x->Println($'x*2 = {x*2}'))
+ .ThenQuickConvert(x->$'x^2 = {x**2}')
+).Println;
+
+В отличии от предыдущего примера, в данном будет создан только один поток выполнения (его всегда создаёт Context.BeginInvoke
).
В общем случае Quick
очереди стараются выполняться на одном из уже существующих потоков выполнения, но так чтобы не нарушать порядок выполнения очередей.
В случае HFQ
и HPQ
, их Quick
варианты это HFQQ
и HPQQ
соответственно.
Если вам необходимо быстро преобразовать тип возвращаемого значения очереди - можно использовать .ThenQuickConvert
:
## uses OpenCLABC;
+var Q := HFQQ(()->1 as object);
+
+Context.Default.SyncInvoke(
+ Q.ThenQuickConvert(x->integer(x))
+).Println;
+
+Но в OpenCLABC
для случая простого преобразования существует особо-оптимизированный метод .Cast
.
+Он ограничен примерно так же, как метод последовательностей .Cast
. То есть:
uses OpenCLABC;
+
+type t1 = class end;
+type t2 = class(t1) end;
+
+begin
+ var Q1: CommandQueue<integer> := 5;
+ var Q2: CommandQueueBase := Q1;
+ var Q3: CommandQueue<t1> := (new t2) as t1;
+ var Q4: CommandQueue<t1> := new t1;
+ var Q5: CommandQueue<t2> := new t2;
+
+ // Можно, потому что к object можно преобразовать всё
+ Context.Default.SyncInvoke( Q1.Cast&<object> );
+
+ //Ошибка: .Cast не может преобразовывать integer в byte
+ // Преобразование записей меняет представление данных в памяти
+ // Можно преобразовывать только object в запись и назад
+// Context.Default.SyncInvoke( Q1.Cast&<byte> );
+
+ // Можно, Q2 и так имеет тип CommandQueue<integer>,
+ // а значит тут Cast вернёт (Q2 as CommandQueue<integer>)
+ Context.Default.SyncInvoke( Q2.Cast&<integer> );
+
+ // Можно, потому что Q3 возвращает t2
+ Context.Default.SyncInvoke( Q3.Cast&<t2> );
+
+ //Ошибка: Не удалось привести тип объекта "t1" к типу "t2".
+ // Q4 возвращает не t2 а именно t1
+// Context.Default.SyncInvoke( Q4.Cast&<t2>.HandleDefaultRes(e->e.Message.Println<>nil, new t2) );
+
+ // Можно, потому что t2 наследует от t1
+ Context.Default.SyncInvoke( Q5.Cast&<t1> );
+
+end.
+
+Кроме того, .Cast
можно применять к очередям без типа результата:
## uses OpenCLABC;
+
+var Q1 := HPQQ(()->begin end);
+var Q2 := HFQQ(()->5) as CommandQueueBase;
+
+// .Cast применимо к CommandQueueNil, но результат будет всегда nil
+Writeln( Context.Default.SyncInvoke(Q1.Cast&<object>) );
+// .Cast применимо и к CommandQueueBase
+Writeln( Context.Default.SyncInvoke(Q2.Cast&<object>) );
+
+Ближайшей альтернативой к вызову CommandQueueBase.Cast
будет вызов .ConvertTyped
- но для этого надо на много больше кода.
Основная оптимизация .Cast
состоит в том, что преобразование не выполняется, если возможно.
+Но будьте осторожны, в некоторых случаях такая оптимизация приглушит ошибку:
uses OpenCLABC;
+
+type t1 = class end;
+type t2 = class(t1) end;
+
+begin
+ Context.Default.SyncInvoke( HFQ(()->new t1).Cast&<t2>.Cast&<t1> );
+end.
+
+Преобразование в t2
не выполняется из-за следующего .Cast
, поэтому
+проверяется только преобразование defalt(t1)
к t2
(во время создания очереди) - а оно допустимое.
Если сложить две очереди A и B (var C := A+B
) — получится очередь C, в которой сначала выполнится A, а затем B.
+Очередь C будет считаться выполненной тогда, когда выполнится очередь B.
Если умножить две очереди A и B (var C := A*B
) — получится очередь C, в которой одновременно начнут выполняться A и B.
+Очередь C будет считаться выполненной тогда, когда обе очереди (A и B) выполнятся.
Как и в математике, умножение имеет бОльший приоритет чем сложение.
+В обоих случаях очередь C будет возвращать то, что вернула очередь B. То есть если складывать и умножать много очередей - результат будет всегда возвращать то, что вернула самая последняя очередь.
+Простейший пример:
+## uses OpenCLABC;
+
+var q1 := HPQ(()->
+begin
+ // lock необходим чтобы при параллельном выполнении два потока
+ // не пытались использовать вывод одновременно. Иначе выведет кашу
+ lock output do Writeln('Очередь 1 начала выполняться');
+ Sleep(500);
+ lock output do Writeln('Очередь 1 закончила выполняться');
+end);
+var q2 := HPQ(()->
+begin
+ lock output do Writeln('Очередь 2 начала выполняться');
+ Sleep(500);
+ lock output do Writeln('Очередь 2 закончила выполняться');
+end);
+
+Writeln('Последовательное выполнение:');
+Context.Default.SyncInvoke( q1 + q2 );
+
+Writeln;
+Writeln('Параллельное выполнение:');
+Context.Default.SyncInvoke( q1 * q2 );
+
+Операторы += и *= также применимы к очередям.
+И как и для чисел - A += B
работает как A := A+B
(и аналогично с *=).
+А значит, возвращаемые типы очередей A и B должны быть одинаковыми, чтобы к ним можно было применить +=/*=.
Если надо сложить/умножить много очередей - лучше применять CombineSyncQueue
/CombineAsyncQueue
соответственно.
+Точнее A+B+C
это то же самое что CombineSyncQueue(CombineSyncQueue(A, B), C)
.
+Поэтому CombineSyncQueue(A, B, C)
создаст очередь немного быстрее чем A+B+C
.
+Но скорость выполнения очереди будет абсолютно одинаковой в этих двух случаях.
Чтобы складывать/умножать много CommandQueueBase
и CommandQueueNil
очередей
+используются Combine*QueueBase
и Combine*QueueNil
соответственно.
Используя CombineConv*Queue
можно указать функцию преобразования, принимающую результаты всех скомбинированных очередей:
## uses OpenCLABC;
+
+var q1 := HFQ( ()->1 );
+var q2 := HFQ( ()->2 );
+
+// Выводит 2, то есть только результат последней очереди
+// Так сделано из за вопросов производительности
+Context.Default.SyncInvoke( q1+q2 ).Println;
+// Но бывает так, что нужны результаты всех сложенных/умноженных очередей
+
+// В таком случае надо использовать CombineConv*Queue
+Context.Default.SyncInvoke(
+ CombineConvSyncQueue(
+ results->results.JoinToString, // Функция преобразования
+ q1, q2
+ )
+).Println;
+// Выводит строку "1 2". Это то же самое, что вернёт "Arr(1,2).JoinToString"
+
+И так же как с другими очередями с кодом на CPU - есть Quick
версия:
## uses OpenCLABC;
+
+var q1 := HFQQ( ()->1 );
+var q2 := HFQQ( ()->2 );
+
+Context.Default.SyncInvoke( q1+q2 ).Println;
+
+Context.Default.SyncInvoke(
+ CombineQuickConvSyncQueue(
+ results->results.JoinToString,
+ q1, q2
+ )
+).Println;
+
+Если надо скомбинировать несколько очередей с разными типами результатов, но при этом использовать все результаты, можно использовать CombineConv*QueueN*
:
## uses OpenCLABC;
+
+var q1 := HFQQ( ()->1 );
+var q2 := HFQQ( ()->'abc' );
+
+Context.Default.SyncInvoke( q1+q2 ).Println;
+
+Context.Default.SyncInvoke(
+ CombineConvSyncQueueN2(
+ (r1,r2)->$'integer[{r1}]+string[{r2}]',
+ q1, q2
+ )
+).Println;
+
+У каждой из CombineConv*QueueN*
так же есть CombineQuickConv*QueueN*
версия.
В данный момент всё ещё не работает... Но уже совсем скоро, правда-правда!
Одну и ту же очередь можно использовать несколько раз, в том числе одновременно:
+## uses OpenCLABC;
+var Q := HPQQ(()->lock output do Writeln('Q выполнилась'));
+
+var t1 := Context.Default.BeginInvoke(Q);
+var t2 := Context.Default.BeginInvoke(Q*Q);
+
+t1.Wait;
+t2.Wait;
+
+Но эта программа выведет "Q выполнилась" три раза, потому что при каждом упоминании - Q запускается ещё раз.
+Это не всегда хорошо. К примеру, может быть так что Q
содержит какой то затратный алгоритм. Или ввод значения с клавиатуры:
## uses OpenCLABC;
+
+var Q := HFQ(()->
+begin
+ lock input do
+ Result := ReadInteger;
+end);
+
+Context.Default.SyncInvoke(CombineConvAsyncQueue(res->res,
+ Q,
+ Q.ThenQuickConvert(i->i*i),
+ Q.ThenQuickConvert(i->i*i*i)
+)).Println;
+
+Эта программа запросит три разных значения, что не всегда то что надо.
+Чтобы использовать результат одной очереди несколько раз применяется .Multiusable
:
## uses OpenCLABC;
+
+var Q := HFQ(()->
+begin
+ lock input do
+ Result := ReadInteger;
+end);
+var Qs := Q.Multiusable;
+
+// Теперь нет смысла в Async, потому что
+Context.Default.SyncInvoke(CombineConvAsyncQueue(res->res,
+ Qs(),
+ Qs().ThenQuickConvert(i->i*i),
+ Qs().ThenQuickConvert(i->i*i*i)
+)).Println;
+
+.Multiusable
создаёт новую функцию, вызывая которую можно получить любое количество очередей,
+у которых будет общий результат.
Каждый вызов .Multiusable
создаёт именно новую функцию.
+Это значит, что если использовать результаты двух вызовов .Multiusable
- исходная очередь выполнится два раза:
## uses OpenCLABC;
+
+var Q := HFQ(()->
+begin
+ lock input do
+ Result := ReadInteger;
+end);
+var Q1s := Q.Multiusable;
+var Q2s := Q.Multiusable;
+
+Context.Default.SyncInvoke(CombineConvAsyncQueue(res->res,
+ Q1s(),
+ Q1s().ThenQuickConvert(i->i*i),
+ Q2s().ThenQuickConvert(i->i*i*i)
+)).Println;
+
+Очередь для которой вызвали .Multiusable
начинает выполняться не ожидая другие очереди:
## uses OpenCLABC;
+
+var Q0 := HPQ(()->
+begin
+ Sleep(1000);
+ Writeln('Q0');
+end);
+
+var Q1s := HPQ(()->
+begin
+ Writeln('Q1');
+end).Multiusable;
+var Q1 := Q1s();
+
+// Q1 выполнит первым
+Context.Default.SyncInvoke( Q0 + Q1 );
+
+Чтобы Q1
выполнялась после Q0
- они должны быть сумированны под .Multiusable
:
## uses OpenCLABC;
+
+var Q0 := HPQ(()->
+begin
+ Sleep(1000);
+ Writeln('Q0');
+end);
+
+var Q1 := HPQ(()->
+begin
+ Writeln('Q1');
+end);
+
+var Qs := (Q0+Q1).Multiusable;
+Context.Default.SyncInvoke( Qs() );
+
+.Multiusable
не работает между вызовами Context.BeginInvoke
:
## uses OpenCLABC;
+
+var Q := HFQ(()->
+begin
+ lock input do
+ Result := ReadInteger;
+end);
+var Qs := Q.Multiusable;
+
+Context.Default.SyncInvoke( Qs() ).Println;
+Context.Default.SyncInvoke( Qs().ThenQuickConvert(i->i*i) ).Println;
+
+Эта программа запросит ввод два раза.
+Если контекст у двух очередей общий - лучше объединить вызовы Context.BeginInvoke
.
+Так не только .Multiusable
будет работать, но и выполнение будет в целом быстрее.
А если контекст разный - надо сохранять результат в переменную и использовать Wait
очереди
+(подробнее на странице ниже).
Между командами в CCQ
очередях бывает надо вставить выполнение другой очереди или кода для CPU.
Это можно сделать, используя несколько .NewQueue
:
var s: MemorySegment;
+var q0: CommandQueueBase;
+...
+var q :=
+ s.NewQueue.ThenWriteValue(...) +
+
+ q0 +
+ HPQ(...) +
+ HPQQ(...) +
+
+ s.NewQueue.ThenWriteValue(...)
+;
+
+Однако можно сделать и красивее:
+var s: MemorySegment;
+var q0: CommandQueueBase;
+...
+var q := s.NewQueue
+ .ThenWriteValue(...)
+
+ .ThenQueue(q0)
+ .ThenProc(...)
+ .ThenQuickProc(...)
+
+ .ThenWriteValue(...)
+;
+
+(На самом деле процедура передаваемая в .ThenProc
'а принимает исходную простую обёртку параметром, что делает его более похожим на .ThenUse
, чем HPQ
)
Эти методы не имеют незаменимых применений, но позволяют сделать код значительно читабельнее.
+Большинство деревьев выполнения очередей можно реализовать используя только сложение и умножение очередей. Но есть несколько проблем:
+Большинство но не все. Пример дерева которое нельзя реализовать через сложение и умножение можно найти тут или в файле:
+C:\PABCWork.NET\Samples\OpenGL и OpenCL\OpenCLABC\Wait очереди\1.pas
Даже если дерево реализуется сложением+умножением - такая реализация может выглядеть запутано и не похоже на график дерева выполнения, к примеру:
+C:\PABCWork.NET\Samples\OpenGL и OpenCL\OpenCLABC\Wait очереди\2.pas
Очереди, выполняемые на разных контекстах не могут эффективно взаимодействовать.
+Эти проблемы решают Wait
-очереди:
## uses OpenCLABC;
+
+var M := WaitMarker.Create;
+
+var t := Context.Default.BeginInvoke(
+ HPQ(()->Writeln('Начало .BeginInvoke')) +
+ WaitFor(M) +
+ HPQ(()->Writeln('Конец .BeginInvoke'))
+);
+
+Sleep(2000);
+Writeln('Посылаем сигнал маркера M');
+M.SendSignal;
+
+t.Wait;
+
+WaitFor(M)
создаёт очень простую Wait
-очередь.
+Внутри вызова .BeginInvoke
(до того как он вернёт CLTask
)
+эта очередь входит в режим ожидания сигналов от маркера M
.
+В этом режиме она не тратит время процессора, но готова возобновить выполнение в любой момент.
Далее, вызов M.SendSignal
посылает по 1 сигналу всем .BeginInvoke
, имеющим Wait
-очереди в ожидании этого сигнала.
WaitMarker
не является очередью, но может быть преобразован к типу CommandQueueBase
, в своеобразную выполняемую форму.
Преобразование к выполняемой форме обычно происходит автоматически, если складывать/умножать его с другими очередями,
+либо передавать в подпрограмму принимающую CommandQueueBase
, как .BeginInvoke
.
+Так же его можно вызвать явно, написав CommandQueueBase(M)
, где M
- маркер.
Выполняемая форма маркера вызывает метод .SendSignal
своего макера.
+То есть CommandQueueBase(M)
обычно равноценна, но немного эффективнее чем HPQQ(M.SendSignal)
.
+Но не всегда - потому что HPQ
возвращает nil
, а выполняемая форма маркера может возвращать другие результаты (об этом ниже).
И стоит так же сказать, прямой вызов M.SendSignal
всё равно всегда эффективнее чем Context.Default.SyncInvoke(M)
.
+Используйте выполнение маркеров внутри .BeginInvoke
только если вам надо активировать его сразу после других очередей:
var M := WaitMarker.Create;
+
+Context.Default.SyncInvoke(
+ HFQ(()->5) + M
+);
+
+Но в этом же коде видно ещё одну проблему - сложение очередей возвращает последний результат,
+то есть результат маркера (который для простого макера из WaitMarker.Create
- nil
).
Если нужно иметь сразу и маркер и возвращаемое значение предыдущей очереди,
+можно создать оторванный сигнал маркера методом .ThenMarkerSignal
:
## uses OpenCLABC;
+var Q := HFQ(()->5).ThenMarkerSignal;
+
+var t := Context.Default.BeginInvoke(
+ WaitFor(Q) + HPQ(()->Writeln('Получен сигнал от Q'))
+);
+var res := Context.Default.SyncInvoke( Q );
+
+t.Wait;
+res.Println;
+
+Тут выполнение Q
сначала выполнит HFQ
, затем пошлёт сигнал в
+Wait
-очередь WaitFor(Q)
и в конце вернёт то, что вернула HFQ
.
В общем случае, оторванный сигнал макера является очередью, и имеет то же возвращаемое значение, что очередь из которой он был создан.
+Но в то же время его можно преобразовать типу WaitMarker
, так же как WaitMarker
преобразовывается в CommandQueueBase
.
+Более того, выполняемая форма полученного маркера - это оторванный сигнал, из которого создали этот маркер.
+Другими словами даже после преобразования в WaitMarker
- оторванный сигнал можно выполнить и получить его результат:
## uses OpenCLABC;
+var Q := HFQ(()->5).ThenMarkerSignal;
+var M: WaitMarker := Q;
+
+Context.Default.SyncInvoke(
+ CommandQueueBase(M)
+ // CommandQueueBase надо всё равно как то преобразовать,
+ // чтобы SyncInvoke вернуло результат Q
+ .Cast&<integer>
+).Println;
+
+Того же эффекта можно добится используя .Multiusable
:
## uses OpenCLABC;
+
+var Qs := HFQ(()->5).Multiusable;
+var M := WaitMarker.Create;
+
+var t := Context.Default.BeginInvoke(
+ WaitFor(M) + HPQ(()->Writeln('Получен сигнал от M'))
+);
+var res := Context.Default.SyncInvoke( Qs()+M+Qs() );
+
+t.Wait;
+res.Println;
+
+То есть .ThenMarkerSignal
не имеет незаменимых применений, но делает код читабельнее.
Есть всего три подпрограммы, создающие Wait
-очереди:
Глобальная, WaitFor
:
+Ничего не делает сама, но блокирует выполнение, ожидая сигнала указанного маркера.
Ещё один общий метод CCQ объектов: .ThenWait
:
+Как и .ThenQueue
и .ThenProc
, .ThenWait(...)
это всего лишь аналог
+.ThenQueue(WaitFor(...))
, существующий только ради читабельности кода.
Метод очереди с результатом, .ThenWaitFor
:
+Как и .ThenMarkerSignal
, сначала выполняет исходную очередь, потом ожидает и в конце возвращает результат исходной очереди.
Маркеры можно комбинировать:
+## uses OpenCLABC;
+
+var M1 := WaitMarker.Create;
+var M2 := WaitMarker.Create;
+
+var Q1 := WaitFor(M1 and M2); // Ожидание обоих маркеров
+var Q2 := WaitFor(M1 or M2); // Ожидание любого из маркеров
+
+При этом если надо применить and
или or
>2 маркерам - лучше использовать WaitAll
/WaitAny
соответственно:
## uses OpenCLABC;
+
+var M1 := WaitMarker.Create;
+var M2 := WaitMarker.Create;
+var M3 := WaitMarker.Create;
+
+var Q1 := WaitFor(WaitAll(M1,M2,M3)); // Ожидание всех маркеров
+var Q2 := WaitFor(WaitAny(M1,M2,M3)); // Ожидание любого из маркеров
+
+Wait
-очереди работают даже между вызовами Context.BeginInvoke
, в отличии от всего остального в OpenCLABC
.
Это не всегда безопасно:
+Context.Default.BeginInvoke(M);
+Context.Default.BeginInvoke(WaitFor(M) + Q);
+
+Проблема этого кода в том, что M
может послать сигнал ещё до того как WaitFor(M)
начнёт ждать.
Чтобы такое не происходило - надо всегда запускать Wait
-очередь раньше маркера:
Context.Default.BeginInvoke(WaitFor(M) + Q);
+Context.Default.BeginInvoke(M);
+
+Но, как всегда, лучше объединять вызовы Context.BeginInvoke
:
Context.Default.BeginInvoke(
+ ( M ) *
+ ( WaitFor(M) + Q )
+);
+
+Все Wait
-очереди начинают ждать в самом начале вызова Context.BeginInvoke
, перед началом выполнения очереди.
+Поэтому если Wait
-очередь и вызов её маркера находятся в общем Context.BeginInvoke
- использовать их можно в любом порядке.
Все Wait
-очереди в одном Context.BeginInvoke
, ожидающие один и тот же маркер, образуют общую Wait
-группу.
+Когда ожидаемый этой группой маркер активируется - он удаляет из Wait
-группы одну из Wait
-очередей, посылая ей сигнал.
## uses OpenCLABC;
+
+var Q1 := HPQ(()->
+begin
+ Sleep(10);
+ lock output do Writeln('Выполнилась Q1');
+end).ThenMarkerSignal;
+
+var Q2 := HPQ(()->lock output do Writeln('Выполнилась Q2'));
+var Q3 := HPQ(()->lock output do Writeln('Выполнилась Q3'));
+
+var t1 := Context.Default.BeginInvoke(
+ (WaitFor(Q1)+Q2) *
+ (WaitFor(Q1)+Q3)
+);
+Context.Default.SyncInvoke(Q1+Q1);
+
+// Все оставшиеся CLTask лучше ожидать, чтобы
+// вывести ошибки, если возникнут при выполнении очереди
+t1.Wait;
+
+Тут Q1
посылает 2 сигнала, сначала в первый WaitFor(Q1)
, затем во второй.
+В данный момент не рекомендуется расчитывать на порядок Wait
-очередей в Wait
-группе.
Ну и, конечно, лучше совместить вызовы Context.BeginInvoke
, раз контекст общий:
## uses OpenCLABC;
+
+var Q1 := HPQ(()->
+begin
+ Sleep(10);
+ lock output do Writeln('Выполнилась Q1');
+end).ThenMarkerSignal;
+
+var Q2 := HPQ(()->lock output do Writeln('Выполнилась Q2'));
+var Q3 := HPQ(()->lock output do Writeln('Выполнилась Q3'));
+
+Context.Default.SyncInvoke(
+ (Q1+Q1) *
+ (WaitFor(Q1)+Q2) *
+ (WaitFor(Q1)+Q3)
+);
+
+Будьте осторожны, лишняя Wait
-очередь вызовет зависание:
## uses OpenCLABC;
+
+var Q1 := HFQ(()->0).ThenMarkerSignal;
+
+var t1 := Context.Default.BeginInvoke(
+ WaitFor(Q1) +
+ // Второй запуск Q1 никогда не произойдёт,
+ // поэтому эта Wait очередь зависнет
+ WaitFor(Q1)
+);
+Context.Default.SyncInvoke(Q1);
+
+t1.Wait;
+
+Wait
-очереди ожидающие один и тот же маркер в разных Context.BeginInvoke
- образуют отдельные Wait
-группы.
+И при активации маркера - он посылает по 1 сигналу каждой (ожидающей его) Wait
-группе:
## uses OpenCLABC;
+
+var Q1 := HPQ(()->
+begin
+ Sleep(1000);
+ lock output do Writeln('Выполнилась Q1');
+end).ThenMarkerSignal;
+
+var Q2 := HPQ(()->lock output do Writeln('Выполнилась Q2'));
+var Q3 := HPQ(()->lock output do Writeln('Выполнилась Q3'));
+
+var Q4 := HPQ(()->lock output do Writeln('Выполнилась Q4'));
+var Q5 := HPQ(()->lock output do Writeln('Выполнилась Q5'));
+
+var t1 := Context.Default.BeginInvoke(
+ ( WaitFor(Q1)+Q2 ) *
+ ( WaitFor(Q1)+Q3 )
+);
+var t2 := Context.Default.BeginInvoke(
+ ( WaitFor(Q1)+Q4 ) *
+ ( WaitFor(Q1)+Q5 )
+);
+// Каждый вызов Q1 тут - активирует по одному WaitFor(Q1) в каждом .BeginInvoke
+Context.Default.SyncInvoke(Q1+Q1);
+
+t1.Wait;
+t2.Wait;
+
+Для начала немного о распространении ошибок в очередях:
+Исключения в очереди Q отменяет выполнение всех очередей, ожидающих Q, +но никак не влияет на параллельные к Q очереди:
+## uses OpenCLABC;
+
+function make_q(i: integer) :=
+HPQ(()->lock output do Writeln(i));
+
+try
+ Context.Default.SyncInvoke(
+ make_q(1) +
+ (make_q(2) * HPQ(()->raise new Exception('3')))
+ + make_q(4)
+ );
+except
+ on e: System.AggregateException do
+ Writeln(e.InnerExceptions.Single);
+end;
+
+Очередь 1 выполняется в любом случае, потому что очередь с ошибкой (3) находится позже;
+Очередь 2 выполняется, потому что она стоит параллельно к очереди 3;
+Очередь 3 выполняется и в процессе кидает исключение;
+Очередь 4 пропускается, потому что ожидает успешного выполнения очередей 2 и 3.
Может звучать тривиально - но это базовое правило, распространяющееся на более сложные случаи. К примеру:
+## uses OpenCLABC;
+
+var M := WaitMarker.Create;
+
+try
+ Context.Default.SyncInvoke(
+ (HPQ(()->raise new Exception('Special error')) + WaitFor(M))
+ *
+ (HPQ(()->Sleep(100)) + WaitFor(M))
+ *
+ M
+ );
+except
+ on e: System.AggregateException do
+ Writeln(e.InnerExceptions.Single);
+end;
+
+Из за исключения первое WaitFor(M)
пропускается. Оно начинает ожидать
+ещё до выхода из .BeginInvoke
, но затем отменяется, а значит не имеет
+шанса поглотить сигнал от M
, оставляя его для второго WaitFor(M)
.
Так же надо сказать что .Multiusable
даёт множественный доступ
+к любому результату выполнения очереди, будь то возвращаемое значение или ошибка:
## uses OpenCLABC;
+
+var Qs := HPQ(()->raise new Exception('Special error')).Multiusable;
+
+try
+ Context.Default.SyncInvoke( Qs()*Qs() );
+except
+ on e: System.AggregateException do
+ e.InnerExceptions.PrintLines;
+end;
+
+Этот код выводит ошибку дважды, потому что она пришла из 2 параллельных веток, которые обе взяли её из общего источника.
+Самый простой обработчик ошибок это .HandleWithoutRes
:
+Он применим ко всем очередям, в том числе CommandQueueBase
, потому что игнорирует любое предыдущее
+возвращаемое значение и создаёт CommandQueueNil
.
Есть два варианта вызова .HandleWithoutRes
:
on e: Exception do
:
+## uses OpenCLABC;
+
+var Q1 := HPQ(()->raise new Exception('Expected error'));
+var Q2 := HPQ(()->raise new Exception('Unexpected error'));
+
+try
+ Context.Default.SyncInvoke( (Q1*Q2)
+ // Ловим все исключения с указанным сообщением
+ // Для реального случая это плохо, но для демонстрации нормально
+ .HandleWithoutRes(e->(e.Message.Print='Expected error').Println)
+ );
+except
+ on e: System.AggregateException do
+ e.InnerExceptions.PrintLines;
+end;
+
+on e: ... do
:
+## uses OpenCLABC;
+
+var Q1 := HPQ(()->raise new System.InvalidOperationException);
+var Q2 := HPQ(()->raise new System.InvalidTimeZoneException);
+
+try
+ Context.Default.SyncInvoke( (Q1*Q2)
+ // Ловим все исключения типа InvalidOperationException
+ // Опять же, вообще плохо съедать исключения без полной диагностики
+ .HandleWithoutRes&<System.InvalidOperationException>(e->true)
+ );
+except
+ on e: System.AggregateException do
+ e.InnerExceptions.PrintLines;
+end;
+
+Далее, обработчик .HandleDefaultRes
:
Если в очереди было кинуто исключение - возвращаемое значение не существует (не nil
, а именно отсутствует).
+Поэтому чтобы вернуть очередь с определённым типом возвращаемого значения,
+необходимо заменить результат после успешной обработки исключения:
## uses OpenCLABC;
+
+// Исключение, кидаемое при не_первых выполнениях этой очереди
+var need_error := false;
+var QErr := HPQ(()->
+begin
+ if need_error then
+ raise new Exception else
+ need_error := true;
+end);
+
+loop 3 do Context.Default.SyncInvoke( (QErr+HFQ(()->5))
+ // Ловим все исключения типа Exception
+ // И заменяем результат на 0, если он отсутствует
+ .HandleDefaultRes&<Exception>(e->true, 0)
+).Println;
+
+И последний - обработчик .HandleReplaceRes
:
Не всегда значение, которым надо заменять возвращаемое значение очереди, можно посчитать до выполнения этой очереди.
+В более сложном случае это значение может вычисляться во время выполнения, скорее всего используя пойманные исключения.
В таком случае нет смысла обрабатывать исключения по одному, потому что так +можно получить несколько конфликтующих замен для возвращаемого значения:
+## uses OpenCLABC;
+var QErr := HPQ(()->raise new Exception);
+
+Context.Default.SyncInvoke(
+ (QErr*QErr + HFQ(()->-1))
+ .HandleReplaceRes(lst->
+ begin
+ Result := lst.Count; // Возвращаемое значение
+ lst.Clear; // Считаем что обработали всё
+ end)
+).Println;
+
+.HandleReplaceRes
нельзя вызывать на Base
и Nil
очередях, потому что непонятно что возвращать если ошибка не возникла.
+Но всегда можно прибавить константную очередь к обрабатываемой, таким образом указав результат по-умолчанию, на случай без ошибок:
## uses OpenCLABC;
+
+var Q1 := HPQQ(()->begin end);
+var Q2 := HPQQ(()->raise new Exception);
+
+Context.Default.SyncInvoke((Q1*Q2 + CQ(0)).HandleReplaceRes(err_lst->
+begin
+ err_lst.PrintLines;
+ err_lst.Clear;
+ Result := -1;
+end)).Println;
+
+Результат очереди из этого примера можно передавать напрямую в Halt
, если вы, к примеру, делаете инструмент для коммандной строки.
Обратите внимание, обработчики исключений всегда действую по принципу Quick
очередей.
+То есть код выполняемый на CPU, который обрабатывает исключения, не должен содержать блокирующих вызовов модуля OpenCL
.
Но использовать затратные алгоритмы, или блокирующие вызовы как ввод с клавиатуры ('Ошибка, всё равно продолжить? [Y/n]'
) вполне нормально:
+Обработчики вызываются только если было кинуто исключение. А исключения должны кидаться только в исключительных ситуациях, когда что-то пошло не так.
В паскале есть два типа блоков для обработки исключений, try-except
- собственно обработчик,
+а так же try-finally
- позволяющий выполнять код не зависимо от того, было ли кинуто исключение.
В очередях OpenCLABC
аналог последнего выглядит так:
## uses OpenCLABC;
+function QErr(i: integer) := HPQ(()->raise new Exception(i.ToString));
+
+Context.Default.SyncInvoke(
+ ( QErr(1)+QErr(2) >= QErr(3)+QErr(4) >= QErr(5)+QErr(6) )
+ .HandleWithoutRes(e->e.Message.Println<>nil)
+);
+
+В этом коде:
+>=
, работающий как +
, но игнорирующий предыдущие исключения, поэтому очередь 3 выполняется.>=
, игнорирующий исключения всех предыдущих очередей, то есть 1 и 3, а значит очередь 5 тоже выполняется.Затем стоит обработчик, который съедает все исключения, но перед этим выводит их текст, показывая что выполнились очереди 1, 3 и 5.
+Обычные .ThenMarkerSignal
и .ThenWaitFor
посылают/съедают сигнал маркера только если перед ними небыло ошибок.
+Это не всегда хорошо, потому что в случае ошибок будет сложно судить о количестве сигналов.
Как общее правило - Wait
-очереди и маркеры лучше ставить после >=
.
+Но HFQ(()->5) >= WaitFor(M)
вернёт результат последней очереди, то есть nil
.
Поэтому существуют методы .ThenFinallyMarkerSignal
и .ThenFinallyWaitFor
.
+Эти методы так же как их варианты без Finally
- возвращают то что вернула исходная очередь.
+Но в отличии от них - посылают/поглащают сигнал не зависимо от ошибок в очереди, из которой их создали.
Передавать команды по одной, когда их несколько - ужасно не эффективно!
+Но нередко бывает так, что команда всего одна. Или для отладки надо одноразово выполнить несколько команд.
Для таких случаев можно создавать очередь неявно:
+У каждого метода для создания комманды для GPU есть дублирующий метод в простой обёртке. Этот метод сам создаёт
+новую очередь, добавляет в неё одну соответствующую команду и выполняет полученную очередь в Context.Default.SyncInvoke(...)
.
Обычный код с очередями:
+## uses OpenCLABC;
+
+var a := new CLArray<integer>(3);
+
+Context.Default.SyncInvoke(a.NewQueue
+ .ThenWriteValue(1, 0)
+ .ThenWriteArray(|5,7|, 1, 2,0)
+ .ThenGetArray
+).Println;
+
+Он же, но с неявными очередями:
+## uses OpenCLABC;
+
+var a := new CLArray<integer>(3);
+
+a.WriteValue(1, 0);
+a.WriteArray(|5,7|, 1, 2,0);
+a.GetArray.Println;
+
+И в случае CLArray
можно ещё красивее:
## uses OpenCLABC;
+
+var a := new CLArray<integer>(3);
+
+a[0] := 1;
+a.Section[1..2] := |5,7|;
+a.Section[0..2].Println;
+
+Все методы создающие одну команду (*CCQ.Add*
методы и все методы неявных очередей)
+могут принимать очередь вместо значения в качестве практически любого параметра. Но в таком случае
+возвращаемый тип очереди должен совпадать с типом параметра. К примеру:
## uses OpenCLABC;
+
+var a := new CLArray<integer>(10);
+// Очищаем весь буфер нулями, чтобы не было мусора
+a.FillValue(0);
+
+// Второй параметр AddWriteItem - индекс в массиве
+// Он имеет тип integer, а значит можно передать и CommandQueue<integer>
+// Таким образом, в параметр сохраняется алгоритм, а не готовое значение
+// Поэтому 3 вызова ниже могут получится с 3 разными индексами
+var q := a.NewQueue
+ .ThenWriteValue(5, HFQQ(()-> Random(a.Length) ))
+;
+
+Context.Default.SyncInvoke(q);
+Context.Default.SyncInvoke(q);
+Context.Default.SyncInvoke(q);
+
+a.GetArray.Println;
+
+Все вложенные очереди начинают выполняться сразу при вызове метода Context.BeginInvoke
, не ожидая других очередей.
Обычно вложенные очереди особо полезны когда надо записать что-то в память GPU прямо перед вызовом kernel'а:
+k.Exec1(N, a.NewQueue.ThenFillValue(1))
+
+Вместо:
+a.NewQueue.ThenFillValue(1) +
+k.Exec1(N, a)
+
+В данной справке в нескольких местах можно встретить утверждения вроде
+++Вызовы
+Context.BeginInvoke
стоит, по возможности, объединять.
Данный раздел подпробнее объясняет устройство модуля, что делает подобные утверждения более понятными.
+Читать его не необходимо для написания работающего кода, но желательно для написания качественного кода.
+Но стоит сказать заранее - это не полное объяснение внутренностей модуля. Объясняется только то, что скорее всего окажется нужным. +Если хотите ещё более полное понимание - используйте Ctrl+клик в IDE по именам, чтобы смотреть исходный код.
+Кроме самого выполнения очередей - им так же необходима инициализация и финализация.
+Для начала, перед тем как любая из под-очередей в BeginInvoke
начнёт выполняться - необходимо инициалировать Wait
очереди.
+Иначе у ожидаемого маркера всегда будет шанс выполнится до того как ожидающая его очередь начнёт ожидать.
Инициализация Wait
очередей заключается в обходе всего дерева под-очередей.
+Для каждой Wait
-группы (очередей, ожидающих общий маркер в общем .BeginInvoke
),
+создаётся счётчик выполнений ожидаемого маркера.
Тоже заключается в обходе дерева под-очередей.
+Но в этот раз очереди по которым прошлись - уже начали выполнятся, не ожидая окончания обхода всего дерева.
+Как только этот обход закончен - метод BeginInvoke
возвращает свой CLTask
коду, вызвавшему его.
Заключается в чистке после выполнения - к примеру удалении всех созданных cl_command_queue
.
+Так же тут собирается информация о всех исключениях, пойманных во время выполнения.
Основное преимущество объединения вызовов BeginInvoke
состоит в различии следующих 2 случаев:
Context.Default.SyncInvoke(A+B);
+
+Context.Default.SyncInvoke(A);
+Context.Default.SyncInvoke(B);
+
+В первом случае пока выполнится A
- B
уже, скорее всего, окажется полностью запущено.
+А значит как только A
закончит выполнятся - ход выполнения перейдёт на B
.
А во втором случае - между окончанием выполнения A
и запуском B
- будет произведено множество проверок,
+а так же выходов/входов в объёмные (что значит JIT их не инлайнит) подпрограммы, как конструктор CLTask
.
Так же, в первом случае многие ресурсы, как объекты OpenCL.cl_command_queue
,
+выделенные при выполнении A
, будут ещё раз использованы для выполнения B
.
Да, всё это мелочи. Но нулевая задержка всегда лучше ненулевой.
+Ну а когда всё же приходится вызывать 2 отдельных BeginInvoke
, к примеру на 2 разных
+контекстах - можно использовать Wait
очереди, чтобы добится того же эффекта:
c2.BeginInvoke(WaitFor(A)+B);
+c1.BeginInvoke(A);
+
+Внутренние оптимизации OpenCLABC
делают этот код практически не отличимым по скорости, от BeginInvoke(A+B)
.
Единственное различие - время инициализации. Потому что A
не запустится, пока не закончится вызов c2.BeginInvoke
.
TODO
модуль OpenGLABC
пока что не существует. Когда то он появится, но пока что есть только это сообщение.
Если хотите понять, было ли это сообщение оставлено тут 10 лет назад и забыто - вот дата, когда автор сообщения последний раз прочитал его:
+24.06.2023
P.S. С изначального сообщения я перенёс некоторые вещи из OpenGL
в OpenGLABC
. В примерах показано использование.
+Но это не то что я собирался добавить в OpenGLABC
, поэтому когда этот модуль будет делаться - всё что сейчас существует я сотру.
+Если хотите писать как в примерах - зажимайте Ctrl и тыкайте на OpenGLABC
в uses
, чтобы открыть исходники. И копируйте оттуда что вам надо в свою программу.