From 0d1e31ebc478798e6624c6abaed424d0574c1aab Mon Sep 17 00:00:00 2001
From: Sun Serega
Если вы будете использовать только модули OpenGLABC и OpenCLABC - данная справка вам не нужна,
-потому что они специально созданы так, чтоб отстранить программиста от всех сложностей
+потому что они специально созданы так, чтобы отстранить программиста от всех сложностей
работы с неуправляемыми .dll и предоставить привычный ООП интерфейс со всевозможными удобствами.
Log содержит логи последней сборки соответствующего модуля.
Они в основном используются чтоб было проще увидеть на что именно +
Они в основном используются чтобы было проще увидеть на что именно повлияли ваши изменения и как (используя проверку изменений git'а).
Но файл FinalFuncOverloads.log так же особо полезен перед
-началом чистки, чтоб увидеть какие перегрузки уже есть.
Чтоб применить фиксеры и посмотреть на что они влияют - вызывайте Pack Most.pas.
Ну а чтоб полностью собрать модули - вызывайте PackAll.exe в корне репозитория.
+
Ну а чтобы полностью собрать модули - вызывайте PackAll.exe в корне репозитория.
(или .bat файлы там же, для сборки отдельных компонентов)
В модуле OpenGL так же есть особые классы, wgl, gdi и glx:
gdi содержит несколько методов библиотеки gdi32.dll. На этой библиотеке основано всё в System.Windows.Forms.
-Подпрограммы включённые в класс gdi - это то, что может понадобиться вам, чтоб настроить и подготовить форму для рисования на ней с помощью OpenGL.
gdi - это то, что может понадобиться вам, чтобы настроить и подготовить форму для рисования на ней с помощью OpenGL.
wgl содержит методы для подключения OpenGL к окну Windows.
Чтоб применить 1 из этих операций к 2 векторам - их типы должны быть одинаковые.
-Если это не так - 1 из них (или оба) надо явно преобразовать, так чтоб типы были одинаковые:
var v1: Vec3d;
var v2: Vec2i;
...
@@ -1475,7 +1475,7 @@ m.Println; // Вывод матрицы
// s присвоит ту же строку, что выводит .Println
var s := m.ToString;
-Для того чтоб матрица выведенная 1 из этих методов выглядела красиво надо +
Для того чтобы матрица выведенная 1 из этих методов выглядела красиво надо использовать моноширный шрифт и поддерживать юникод (потому что для матриц используются символы псевдографики).
Обычно это не проблема для .Println, потому что и консоль, и окно вывода в IDE имеют моноширный шрифт и поддерживают юникод.
Кроме удаления неиспользуемых экземпляров классов, сборщик мусора так же может произвольно перемещать используемые объекты, более плотно упаковая их в памяти.
-И он прекрасно справляется с тем, чтоб сделать эти перемещения незаметными, в обычных ситуациях. +
И он прекрасно справляется с тем, чтобы сделать эти перемещения незаметными, в обычных ситуациях. Но как только речь находит об указателях и неуправляемом коде - начинаются проблемы. Чтоб избежать их, надо очень хорошо понимать как работает сборщик мусора.
В .Net массивы хранят не только содержимое, но и данные о своём размере.
А в C++ вместо обычных массивов используется безформенная область памяти.
При её выделении - в переменную записывается указатель [0] элемента.
-А о том чтоб сохранить данные о размере этой области - должен позаботится программист.
+А о том чтобы сохранить данные о размере этой области - должен позаботится программист.
(вообще обычно в C++ используют обёртки, хранящие длину так же как .Net массивы. Но OpenGL.dll и OpenCL.dll это не касается)
Если вы видели старые коды с использованием OpenGL из какого то из паскалей - наверняка видели что то такое:
@@ -1774,7 +1774,7 @@ procedure FillBuffer<T>(var data: T); where T: record; begin // Компилятор развернёт это в "FillBuffer(data)" // То есть никакие преобразования в .exe не попадут - // Но указатели всё равно нужны, чтоб компилятор не ругался на несовместимость типов + // Но указатели всё равно нужны, чтобы компилятор не ругался на несовместимость типов FillBuffer(PByte(pointer(@data))^); end; @@ -1824,7 +1824,7 @@ end;Но если вы, к примеру, создаёте много OpenGL шейдеров из исходников - можно перед компиляцией программы:
Marshal.StringToHGlobalAnsi чтоб получить неуправляемые строки;Marshal.StringToHGlobalAnsi чтобы получить неуправляемые строки;$resource, читать как массив байт и его уже передавать неуправляемому коду вместо строки.Данная справка относится к модулю OpenCLABC, входящему в состав стандартных модулей языка PascalABC.NET.
Модуль OpenCLABC это высокоуровневая оболочка модуля OpenCL.
+
Модуль OpenCLABC это высокоуровневая обёртка модуля OpenCL.
Это значит, что с OpenCLABC можно писать гораздо меньше кода в больших и сложных программах,
однако такой же уровень микроконтроля как с модулем OpenCL недоступен.
Например, напрямую управлять cl_event'ами в OpenCLABC невозможно.
@@ -909,6 +909,8 @@ window.onload = ()=>{
GPU — Графическое Процессорное Устройство (видеокарта);
RAM — Оперативная память;
+Команда — запрос на выполнение чего-либо. К примеру:
OpenCL это неуправляемая библиотека. Обычно можно заставить управляеммые типы данных работать с ней.
+Но часто это приводит к дополнительным затратам производительности.
+И обычно не значит всегда - MemorySegment.ReadValue, принимающее запись var-параметром не может быть безопасным из за сборщика мусора
+(и поэтому отсутствует).
Более прямым будет передача неуправляемых типов - указателей - без преобразований в подпрограммы модуля OpenCL.
+И эта возможность тоже существует, к примеру в виде MemorySegment.WriteData. Но такие указатели ещё более не_безопасны:
+Как минимум они требуют освобождения в try-finally чтобы избежать утечек памяти.
+И защиты от дурака, не возволяющей записать значение типа real туда, где хранится int64 - не существует.
Как что-то среднее между этими двумя вариантами - существует NativeValue<T>:
+Этот класс является обёрткой указателя. Именно обычного указателя - не памяти GPU как все остальные простые обёртки OpenCLABC.
## uses OpenCLABC;
+
+// В конструктор необходимо передавать значение,
+// потому что иначе неуправляемая память будет содержать мусор
+// Но можно передать default, чтобы заполнить выделяемую память нулями
+var nv := new NativeValue<integer>(default(integer));
+
+nv.Value := 5; // Значение можно и читать,
+nv.Value.Println; // и перезаписывать
+
+// Напрямую получать доступ к области памяти,
+// через свойство Pointer, не рекомендуется
+Writeln(nv.Pointer);
+
+nv.Dispose; // Освобождение памяти - вызывается и само при сборке мусора
+
+Объект типа Kernel представляет одну подпрограмму в OpenCL-C коде,
объявленную с ключевым словом __kernel.
Методы, запускающие Kernel принимают специальные аргументы типа KernelArg, которые передаются в OpenCL-C код.
Экземпляр KernelArg может быть создан из нескольких типов значений, а точнее:
## uses OpenCLABC;
@@ -1173,6 +1205,7 @@ ks.PrintLines;
var k: Kernel;
var val1 := 3;
var val2 := 5;
+var val3 := new NativeValue<byte>(7);
var a: array of byte;
k.Exec1(1,
@@ -1198,6 +1231,10 @@ k.Exec1(1,
val1,
HFQ(()->val1),
+ // В том числе неуправляемое
+ val3,
+ HFQ(()->val3),
+
// Массив размерных значений
a,
HFQ(()->a)
@@ -1248,29 +1285,26 @@ begin end.
x не должно быть захвачего лямбдой.
Хотя указатель на x уже можно захватывать:
-uses OpenCLABC;
+## uses OpenCLABC;
-begin
- var k: Kernel;
- var val1 := 3;
+var k: Kernel;
+var val1 := 3;
- // На val1 всё ещё накладываются все ограничени,
- // когда val1_ptr использована в качестве KernelArg
- // Но к самой val1_ptr эти ограничения не применяются
- var val1_ptr := @val1;
+// На val1 всё ещё накладываются все ограничени,
+// когда val1_ptr использована в качестве KernelArg
+// Но к самой val1_ptr эти ограничения не применяются
+var val1_ptr := @val1;
- k.Exec1(1,
+k.Exec1(1,
- val1,
- HFQ(()->val1_ptr^), // захватили переменную val1_ptr, а не val1
+ val1,
+ HFQ(()->val1_ptr^), // захватили переменную val1_ptr, а не val1
- // val1 нигде не захвачена, поэтому теперь так можно
- @val1,
- val1_ptr // то же самое
+ // val1 нигде не захвачена, поэтому теперь так можно
+ @val1,
+ val1_ptr // то же самое
- );
-
-end.
+);
Выходить из подпрограммы, где объявили x нельзя, пока .Exec не закончит выполнятся.
@@ -1321,9 +1355,9 @@ end.
часть кода, или различие архитектур компьтеров, таким образом усложняя поиск источника ошибки.
Поэтому, ещё раз, используйте передачу адреса в качестве KernelArg только как тонкую оптимизацию и только когда понимаете что делаете.
Обычные программы невозможно запустить на GPU. Для этого надо писать особые программы.
В контексте OpenCL - эти программы обычно пишутся на языке "OpenCL C" (основанном на языке "C").
Язык OpenCL-C это часть библиотеки OpenCL, поэтому его справку можно найти там же, где и справку OpenCL.
@@ -1339,8 +1373,8 @@ end.Конструктор ProgramCode (new ProgramCode(...)) принимает тексты исходников программы на языке OpenCL-C.
Именно тексты исходников, не имена файлов!
Так же, как исходники паскаля хранят в .pas файлах, исходники OpenCL-C кода хранят в .cl файлах. @@ -1348,8 +1382,8 @@ end.
Так как конструктор ProgramCode принимает текст - исходники программы на OpenCL-C можно хранить даже в
строке в .pas программе. Тем не менее, храненить исходники OpenCL-C кода в .cl файлах обычно удобнее всего.
После создания объекта типа ProgramCode из исходников можно вызвать
метод ProgramCode.SerializeTo, чтобы сохранить код в бинарном и прекомпилированном виде.
Обычно это делается отдельной программой (не той же самой, которая будет использовать этот бинарный код).
ProgramCode.DeserializeFrom.
Пример можно найти в папке Прекомпиляция ProgramCode или тут.
Передавать команды для GPU по одной не эффективно. Гораздо эффективнее передавать несколько команд сразу.
-Для этого существуют очереди (типы, наследующие от CommandQueue<T> и CommandQueueBase).
+
Для этого существуют очереди (типы, наследующие от CommandQueueBase).
Они хранят произвольное количество команд для GPU.
А при необходимости также и части кода, выполняемые на CPU.
У каждого типа-очереди есть свой тип возвращаемого значения.
-К примеру, так объявляется переменная в которую можно будет сохранить очередь, возвращающую integer:
var Q1: CommandQueue<integer>;
+
+
+Все типы очередей наследует от 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 в System.Int32
+// Ошибка кидается ещё в момент создания .Cast очереди
+Writeln(Context.Default.SyncInvoke( Q.Cast&<integer> ));
-Очереди, созданные из областей памяти или kernel'ов возващают свои области памяти/Kernel'ы соответственно, из которых были созданы;
+
Подробнее тут.
+
+Очереди, созданные из областей памяти OpenCL или kernel'ов возващают свои области памяти/Kernel'ы соответственно, из которых были созданы;
Очереди, созданные с HFQ - значение, которое вернёт переданная функция;
-Очереди, созданные с HPQ - возвращает nil без типа.
-К примеру:
+Очереди, созданные с HPQ являются CommandQueueNil.
+Демонстрация:
## uses OpenCLABC;
/// Вывод типа и значения объекта
@@ -1398,61 +1461,115 @@ $'{o?.GetType}[{_ObjectToString(o)}]'.Println;
// то есть, берём или тип объекта, или nil если сам объект nil
// _ObjectToString это функция, которую использует Writeln для форматирования значений
+procedure Test(q: CommandQueueBase) :=
+OtpObject(Context.Default.SyncInvoke(
+ // Преобразовываем результат к object, чтобы его вернула SyncInvoke
+ q.Cast&<object>
+));
+
var s := new MemorySegment(1);
// Тип - MemorySegment, потому что очередь создали из него
-OtpObject(Context.Default.SyncInvoke( s.NewQueue ));
+Test( s.NewQueue );
// Тип - Int32 (то есть integer), потому что это тип по умолчанию для выражения (5)
-OtpObject(Context.Default.SyncInvoke( HFQ(()->5) ));
+Test( HFQ(()->5) );
// Тип - string, по той же причине
-OtpObject(Context.Default.SyncInvoke( HFQ(()->'abc') ));
+Test( HFQ(()->'abc') );
// Тип отсутствует, потому что HPQ возвращает nil
-OtpObject(Context.Default.SyncInvoke(
- HPQ(()->Print('Выполнилась HPQ:'))
- // Без преобразования к конкретному типу,
- // возвращаемое значение нельзя использовать
- .Cast&<object>
-));
-
+Test( HPQ(()->Print('Выполнилась HPQ:')) );
-После выполнения очереди метод Context.SyncInvoke возвращает то, что вернула очередь.
-А если использовать метод Context.BeginInvoke - возвращаемое значение можно получить с помощью метода CLTask.WaitRes.
-Бывает необходимо хранить несколько очередей, с разными возвращаемыми значениями, вместе. К примеру, в переменной типа List<>.
-Но в переменной типа CommandQueue<SomeT> можно хранить только очередь с конкретным типом возвращаемого значения SomeT.
-Для того, чтобы хранить очереди с разными возвращаемыми значениями в одной переменной - используется CommandQueueBase.
-CommandQueueBase это особый тип очереди, у которого не указывается возвращаемое значение.
-От него наследует CommandQueue<>, поэтому переменной типа CommandQueueBase можно присвоить любую очередь.
-Если попытаться выполнить очередь типа CommandQueueBase,
-или применять операции преобразования очередей, как .ThenConvert,
-то возвращаемое значение будет игнорироваться.
-
-
-
-
-Самый простой способ выполнить очередь - вызвать метод Context.SyncInvoke.
-Он синхронно выполняет очередь и вызвращает её результат (если таковой есть).
-Но если надо выполнить очередь асинхронно - лучше использовать метод Context.BeginInvoke.
-Он запускает асинхронное выполнение очереди и как только очередь была полностью запущена - возвращает объект типа CLTask<>, у которого есть:
-
-- Свойства, возвращающие оригинальную очередь и контекст выполнения.
-- Методы для ожидания окончания выполнения и получения результата очереди.
-
-Метод Context.SyncInvoke реализован как .BeginInvoke(...).WaitRes.
-Поэтому, везде где сказано "... происходит при вызове .BeginInvoke", это же относится и к .SyncInvoke.
-У CLTask, как и у очереди - в <>, указывается тип возвращаемого значения. То есть:
-var t: CLTask<integer>;
+Проверить что очередь ничего не возвращает очень просто:
+var Q: CommandQueueBase;
+...
+if Q is CommandQueueNil(var cqn) then
+ p1(cqn) else
+ p2(Q);
-В такую переменную можно сохранить только результат Context.BeginInvoke для очереди типа CommandQueue<integer>.
-И как и у CommandQueue:
+Но для типа CommandQueue<T> надо указать конкретный тип, чтобы вызвать is.
+Другими словами, с помощью is можно проверять только по 1 типу возвращаемого значения за раз:
+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($'<{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 := typeof(T).ToString;
+
+ 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-ов есть:
-- Существует так же и тип
CLTaskBase, у которого тип возвращаемого значения не указывается.
-- Переменной типа
CLTaskBase можно присвоить CLTask с любым возвращаемым значением.
-Context.BeginInvoke для очереди типа CommandQueueBase возвращает CLTaskBase.
+- Свойства
.OrgContext и .OrgQueue, возвращающие контекст выполнения и выполняемую очередь соответственно.
+- Метод
.Wait для ожидания окончания выполнения очереди.
+У CLTask<T> так же есть метод .WaitRes, вызывающий .Wait и затем возвращающий результат очереди.
Если при выполнении возникла ошибка, о ней выведет не полную информацию. Чтобы получить всю информацию - используется try:
try
@@ -1466,9 +1583,9 @@ end;
Для этого кода есть стандартный снипет. Чтобы активировать его - напишите tryo и нажмите Shift+Пробел.
-
+
-
+
Есть всего 11 базовых способов создать очередь:
-
@@ -1495,11 +1612,11 @@ end;
-
-
+
+
Самый просто способ создать очередь — выбрать объект, имеющий методы, соответствующие командам для GPU, и вызвав его метод .NewQueue.
Полученная очередь будет иметь особый тип, с припиской CCQ (что значит "Command Container Queue", то есть очередь-контейнер для коман GPU).
-К примеру, метод CLArray<byte>.NewQueue вернёт очередь типа CLArrayCCQ<byte>.
+К примеру, метод CLArray<byte>.NewQueue вернёт очередь типа CLArrayCCQ<byte>, наследующего от CommandQueue< CLArray<byte> >.
К такой очереди можно добавлять команды, вызывая её методы, имена которых начинаются с .Add.
К примеру:
## uses OpenCLABC;
@@ -1520,9 +1637,7 @@ q.AddWriteItem(5, 1).AddWriteItem(7, 2);
// Все команды в q будут выполняться в порядке их добавления
// .AddGet методы особенные, потому что они возвращают новую очередь
-// В данном случае эта очередь читает весь CLArray как обычный массив на CPU
-// .SyncInvoke возвращает то, что вернула последняя очередь
-// А .Println сразу выводит полученный массив
+// В данном случае эта очередь читает весь CLArray как обычный массив в RAM
Context.Default.SyncInvoke(q.AddGetArray).Println;
Также, CCQ очереди можно создавать из очередей, возвращающих объект с командами. Для этого используется конструктор:
@@ -1531,28 +1646,31 @@ Context.Default.SyncInvoke(q.AddGetArray).Println;
var q := new MemorySegmentCCQ(q0);
-Команды объектов, представляющих память на GPU можно разделить на группы.
+Команды объектов, представляющих память на GPU, можно разделить на группы.
По направлению передачи:
Write и Fill: Из RAM в память GPU;
-Read: Из памяти GPU в RAM;
+Read и Get: Из памяти GPU в RAM;
Copy: Между двумя областями памяти GPU.
-Get: Как Read, но вместо использования существующей памяти RAM, выделяется новая.
И по типу данных на стороне RAM:
-Data: Используются данные, находящиеся по указанному адресу;
-Value: Используется копия указанного размерного значения;
+Data: Используются данные, находящиеся в RAM по указанному адресу;
+Value: Используется размерное значение;
Array: Используется содержимое указанного массива размерных значений.
Но при этом отсутствуют некоторые комбинации.
В первую очередь, в случае Copy нет понятия типа данных на стороне RAM, потому что RAM в принципе не задействуется.
-Так же нет ReadValue, потому что нет смысла читать в копию указанного значения.
-Вместо него надо использовать GetValue - создающее и возвращающее новое размерное значение.
-Или ReadData, передавая адрес значения, которое надо перезаписать - но с этим надо осторожно, как и в случае KernelArg .
+Так же, WriteValue может принимать размерное значение и NativeValue, но ReadValue принимает только NativeValue.
+Это потому, что принимать размерное значение в ReadValue var-параметром не безопастно,
+как и в случае передачи адреса в качестве KernelArg .
+Если вы понимаете что делаете - используйте ReadData, явно передавая в него адрес вашего значения (то есть указатель).
+Но обычно лучше использовать GetValue, создающее и возвращающее новое размерное значение, либо ReadValue принимающее NativeValue.
+
+Кроме таких объектов, методы-команды для GPU есть только у Kernel. И все они представляют запуск kernel'а.
-
-
+
+
Переменной очереди можно присвоить значение, тип которого совпадает с возвращаемым значением очереди:
var q: CommandQueue<integer> := 5;
@@ -1563,8 +1681,8 @@ var q := new MemorySegmentCCQ(q0);
Writeln($'Очередь не константная');
-
-
+
+
Иногда между командами для GPU надо вставить выполнение обычного кода на CPU.
А разрывать для этого очередь на две части - плохо, потому что
одна целая очередь всегда выполнится быстрее двух её частей.
@@ -1576,7 +1694,7 @@ HPQ — Host Procedure Queue
Пример применения приведён на странице выше.
Так же бывает нужно использовать результат предыдущей очереди в коде на CPU.
-Для этого используются методы .ThenConvert и .ThenUse, соответствующие HFQ и HPQ:
+Для этого используются методы .ThenUse и .ThenConvert:
## uses OpenCLABC;
var Q := HFQ(()->5);
@@ -1584,10 +1702,11 @@ Context.Default.SyncInvoke(Q
.ThenUse(x->Println($'x*2 = {x*2}'))
.ThenConvert(x->$'x^2 = {x**2}')
).Println;
-
+
+Они схожи с HPQ и HFQ соответственно, за исключением того, что .ThenUse возвращает значение, которое в него передали, а не nil.
-
-
+
+
Если сложить две очереди A и B (var C := A+B) — получится очередь C, в которой сначала выполнится A, а затем B.
Очередь C будет считаться выполненной тогда, когда выполнится очередь B.
Если умножить две очереди A и B (var C := A*B) — получится очередь C, в которой одновременно начнут выполняться A и B.
@@ -1627,7 +1746,9 @@ end.
И как и для чисел - A += B работает как A := A+B (и аналогично с *=).
А значит, возвращаемые типы очередей A и B должны быть одинаковыми, чтобы к ним можно было применить +=/*=.
Если надо сложить/умножить много очередей - лучше применять CombineSyncQueue/CombineAsyncQueue соответственно.
-Эти подпрограммы работают немного быстрее чем сложение и умножение, если объединять больше двух очередей.
+Точнее A+B+C это то же самое что CombineSyncQueue(CombineSyncQueue(A, B), C).
+Поэтому CombineSyncQueue(A, B, C) создаст очередь немного быстрее чем A+B+C.
+Но скорость выполнения очереди будет абсолютно одинаковой в этих двух случаях.
Кроме того, они могут принимать ещё один параметр перед очередями:
Этот параметр позволяет указать функцию преобразования, которая использует результаты всех входных очередей:
uses OpenCLABC;
@@ -1655,8 +1776,8 @@ begin
end.
-
-
+
+
Если надо с минимальными затратами производительности изменить представление компилятора об очереди - лучше всего использовать .Cast.
Но он ограничен примерно так же, как метод последовательностей .Cast. То есть:
uses OpenCLABC;
@@ -1674,7 +1795,7 @@ begin
// Можно, потому что к object можно преобразовать всё
Context.Default.SyncInvoke( Q1.Cast&<object> );
- // Нельзя, преобразование между 2 записями, как из integer в byte - это сложный алгоритм
+ // Нельзя, преобразование между 2 записями, как из integer в byte, меняет представление данных в памяти
Context.Default.SyncInvoke( Q1.Cast&<byte> );
// Можно, Q2 и так имеет тип CommandQueue<integer>, а значит тут Cast вернёт (Q2 as CommandQueue<integer>)
@@ -1691,12 +1812,12 @@ begin
end.
-
-
+
+
В данный момент всё ещё не работает... Но уже совсем скоро, правда-правда!
-
-
+
+
Одну и ту же очередь можно использовать несколько раз, в том числе одновременно:
## uses OpenCLABC;
var Q := HPQ(()->lock output do Writeln('Q выполнилась'));
@@ -1720,8 +1841,8 @@ end);
Context.Default.SyncInvoke(CombineAsyncQueue(
res->res,
Q,
- Qs().ThenConvert(i->i**2),
- Qs().ThenConvert(i->i**3)
+ Q.ThenConvert(i->i*i),
+ Q.ThenConvert(i->i*i*i)
)).Println;
Эта программа запросит три разных значения, что не всегда то что надо.
@@ -1739,8 +1860,8 @@ var Qs := Q.Multiusable;
Context.Default.SyncInvoke(CombineAsyncQueue(
res->res,
Qs(),
- Qs().ThenConvert(i->i**2),
- Qs().ThenConvert(i->i**3)
+ Qs().ThenConvert(i->i*i),
+ Qs().ThenConvert(i->i*i*i)
)).Println;
.Multiusable создаёт новую функцию, вызывая которую можно получить любое количество очередей,
@@ -1760,8 +1881,8 @@ var Q2s := Q.Multiusable;
Context.Default.SyncInvoke(CombineAsyncQueue(
res->res,
Q1s(),
- Q1s().ThenConvert(i->i**2),
- Q2s().ThenConvert(i->i**3)
+ Q1s().ThenConvert(i->i*i),
+ Q2s().ThenConvert(i->i*i*i)
)).Println;
@@ -1784,8 +1905,8 @@ Context.Default.SyncInvoke( Qs().ThenConvert(i->i*i) ).Println;
А если контекст разный - надо сохранять результат в переменную и использовать Wait очереди
(подробнее на странице ниже).
-
-
+
+
Между командами в CCQ очередях бывает надо вставить выполнение другой очереди или кода для CPU.
Это можно сделать, используя несколько .NewQueue:
var s: MemorySegment;
@@ -1818,8 +1939,8 @@ q.AddQueue(q);
Все .Add* методы добавляют команды в существующую очередь, а не создают новую.
Поэтому очередь, созданная предыдущим кодом при попытке выполнения начнёт циклически запускать саму себя.
-
-
+
+
Большинство деревьев выполнения очередей можно реализовать используя только сложение и умножение очередей. Но есть несколько проблем:
Большинство но не все. Пример дерева которое нельзя реализовать через сложение и умножение можно найти тут или в файле:
@@ -1852,15 +1973,18 @@ t.Wait;
Внутри вызова .BeginInvoke (до того как он вернёт CLTask)
эта очередь входит в режим ожидания сигналов от маркера M.
В этом режиме она не тратит время процессора, но готова возобновить выполнение в любой момент.
-Далее, вызов M.SendSignal посылает по 1 сигналу всем .BeginInvoke, имеющим Wait очереди в ожидании этого сигнала.
+Далее, вызов M.SendSignal посылает по 1 сигналу всем .BeginInvoke, имеющим Wait-очереди в ожидании этого сигнала.
-WaitMarker, так же, является своеобразной очередью, то есть
-его можно складывать с другими очередями и передавать в .BeginInvoke .
-При выполнении в качестве очереди маркер вызывает свой метод .SendSignal и сразу возвращает nil, так же как HPQ.
-То есть как очередь он равноценен HPQ(M.SendSignal), но немного более эффективен.
-Но стоит так же сказать, прямой вызов M.SendSignal всё равно всегда эффективнее чем Context.Default.SyncInvoke(M).
+
WaitMarker не является очередью, но может быть преобразован к типу CommandQueueBase, в своеобразную выполняемую форму.
+Преобразование в выполняемой форме обычно происходит автоматически, если складывать/умножать его с другими очередями,
+либо передавать в подпрограмму принимающую CommandQueueBase, как .BeginInvoke.
+Так же его можно вызвать явно, написав CommandQueueBase(M), где M - маркер.
+Выпоняемая форма маркера всегда вызывает метод .SendSignal своего макера.
+Таким образом CommandQueueBase(M) обычно равноценна, но немного эффективнее чем HPQ(M.SendSignal).
+Но не всегда - потому что HPQ возвращает nil, а выполняемая форма маркера может возвращать другие результаты (об этом ниже).
+И стоит так же сказать, прямой вызов M.SendSignal всё равно всегда эффективнее чем Context.Default.SyncInvoke(M).
Используйте выполнение маркеров внутри .BeginInvoke только если вам надо активировать его сразу после других очередей:
-var M := new WaitMarker;
+var M := WaitMarker.Create;
Context.Default.SyncInvoke(
HFQ(()->5) + M
@@ -1868,7 +1992,7 @@ Context.Default.SyncInvoke(
Но в этом же коде видно ещё одну проблему - сложение очередей возвращает последний результат,
-то есть результат маркера (который nil).
+то есть результат маркера (который для простого макера из WaitMarker.Create - nil).
Если нужно иметь сразу и маркер и возвращаемое значение предыдущей очереди,
можно создать оторванный сигнал маркера методом .ThenMarkerSignal:
## uses OpenCLABC;
@@ -1882,10 +2006,24 @@ var res := Context.Default.SyncInvoke( Q );
t.Wait;
res.Println;
-Такой сигнал можно ожидать Wait-очередью, но при этом он
-сохраняет результат оригинальной очереди при выполнении в .BeginInvoke.
-Точнее Q в последнем коде сначала выполнит HFQ, затем пошлёт сигнал в
-Wait очередь WaitFor(Q) и в конце вернёт то, что вернула HFQ.
+Тут выполнение Q сначала выполнит HFQ, затем пошлёт сигнал в
+Wait-очередь WaitFor(Q) и в конце вернёт то, что вернула HFQ.
+В общем случае, оторванный сигнал макера является очередью, и имеет то же возвращаемое значение, что очередь из которой он был создан.
+Но в то же время его можно преобразовать типу WaitMarker, так же как WaitMarker преобразовывается в CommandQueueBase.
+Более того, выполняемая форма полученного маркера - это оторванный сигнал, из которого создали этот маркер.
+Другими словами даже после преобразования в WaitMarker - оторванный сигнал можно выполнить и получить его результат:
+## uses OpenCLABC;
+var Q := HFQ(()->5).ThenMarkerSignal;
+var M: WaitMarker := Q;
+
+Writeln(Context.Default.SyncInvoke(
+ CommandQueueBase(M)
+ // CommandQueueBase надо всё равно как то преобразовать,
+ // чтобы SyncInvoke вернуло результат Q
+ .Cast&<integer>
+));
+
+
Того же эффекта можно добится используя .Multiusable:
## uses OpenCLABC;
@@ -1902,7 +2040,7 @@ res.Println;
То есть .ThenMarkerSignal не имеет незаменимых применений, но делает код читабельнее.
-Есть всего 3 подпрограммы, создающие Wait очереди:
+Есть всего три подпрограммы, создающие Wait-очереди:
Глобальная, WaitFor:
Ничего не делает сама, но блокирует выполнение, ожидая сигнала указанного маркера.
@@ -1936,13 +2074,13 @@ WaitFor(WaitAll(M1,M2,M3)); // Ожидание всех маркеров
WaitFor(WaitAny(M1,M2,M3)); // Ожидание любого из маркеров
-Wait очереди работают даже между вызовами Context.BeginInvoke, в отличии от всего остального в OpenCLABC.
+Wait-очереди работают даже между вызовами Context.BeginInvoke, в отличии от всего остального в OpenCLABC.
Это не всегда безопастно:
Context.Default.BeginInvoke(M);
Context.Default.BeginInvoke(WaitFor(M) + Q);
Проблема этого кода в том, что M может послать сигнал ещё до того как WaitFor(M) начнёт ждать.
-Чтоб такое не происходило - надо всегда запускать Wait очередь раньше маркера:
+Чтобы такое не происходило - надо всегда запускать Wait-очередь раньше маркера:
Context.Default.BeginInvoke(WaitFor(M) + Q);
Context.Default.BeginInvoke(M);
@@ -1952,11 +2090,11 @@ Context.Default.BeginInvoke(M);
( WaitFor(M) + Q )
);
-Все Wait очереди начинают ждать в самом начале вызова Context.BeginInvoke, перед началом выполнения очереди.
-Поэтому если Wait очередь и вызов её маркера находятся в общем Context.BeginInvoke - использовать их можно в любом порядке.
+Все Wait-очереди начинают ждать в самом начале вызова Context.BeginInvoke, перед началом выполнения очереди.
+Поэтому если Wait-очередь и вызов её маркера находятся в общем Context.BeginInvoke - использовать их можно в любом порядке.
-Все Wait очереди в одном Context.BeginInvoke, ожидающие один и тот же маркер, образуют общую Wait группу.
-Когда ожидаемый этой группой маркер активируется - он удаляет из Wait группы одну из Wait очередей, посылая ей сигнал.
+Все Wait-очереди в одном Context.BeginInvoke, ожидающие один и тот же маркер, образуют общую Wait-группу.
+Когда ожидаемый этой группой маркер активируется - он удаляет из Wait-группы одну из Wait-очередей, посылая ей сигнал.
## uses OpenCLABC;
var Q1 := HPQ(()->
@@ -1979,7 +2117,7 @@ Context.Default.SyncInvoke(Q1+Q1);
t1.Wait;
Тут Q1 посылает 2 сигнала, сначала в первый WaitFor(Q1), затем во второй.
-В данный момент не рекомендуется расчитывать на порядок Wait очередей в Wait группе.
+В данный момент не рекомендуется расчитывать на порядок Wait-очередей в Wait-группе.
Ну и, конечно, лучше совместить вызовы Context.BeginInvoke, раз контекст общий:
## uses OpenCLABC;
@@ -1998,7 +2136,7 @@ Context.Default.SyncInvoke(
(WaitFor(Q1)+Q3)
);
-Будьте осторожны, лишняя Wait очередь вызовет зависание:
+Будьте осторожны, лишняя Wait-очередь вызовет зависание:
## uses OpenCLABC;
var Q1 := HFQ(()->0).ThenMarkerSignal;
@@ -2014,8 +2152,8 @@ Context.Default.SyncInvoke(Q1);
t1.Wait;
-Wait очереди ожидающие один и тот же маркер в разных Context.BeginInvoke - образуют отдельные Wait группы.
-И при активации маркера - он посылает по 1 сигналу каждой Wait группе:
+Wait-очереди ожидающие один и тот же маркер в разных Context.BeginInvoke - образуют отдельные Wait-группы.
+И при активации маркера - он посылает по 1 сигналу каждой Wait-группе:
## uses OpenCLABC;
var Q1 := HPQ(()->
@@ -2045,8 +2183,8 @@ t1.Wait;
t2.Wait;
-
-
+
+
Для начала немного о распространении ошибок в очередях:
Исключения в очереди Q отменяет выполнение всех очередей, ожидающих Q,
но никак не влияет на параллельные к Q очереди:
@@ -2109,8 +2247,8 @@ end;
Самый простой обработчик ошибок это .HandleWithoutRes:
Он применим ко всем очередям, в том числе CommandQueueBase, потому что игнорирует любое предыдущее
-возвращаемое значение и всегда возвращает nil (только если все исключения были успешно обработаны).
-Есть 2 варианта вызова .HandleWithoutRes:
+возвращаемое значение и создаёт CommandQueueNil.
+Есть два варианта вызова .HandleWithoutRes:
- Ловля всех исключений - аналог
on e: Exception do:
## uses OpenCLABC;
@@ -2218,10 +2356,10 @@ Context.Default.SyncInvoke(
Но HFQ(()->5) >= WaitFor(M) вернёт результат последней очереди, то есть nil.
Поэтому существуют методы .ThenFinallyMarkerSignal и .ThenFinallyWaitFor.
Эти методы так же как их варианты без Finally - возвращают то что вернула исходная очередь.
-Но в отличии от них - посылают/поглащают сигнал не зависимо от предыдущих ошибок.
+Но в отличии от них - посылают/поглащают сигнал не зависимо от ошибок в очереди, из которой их создали.
-
-
+
+
Передавать команды по одной, когда их несколько - ужасно не эффективно!
Но нередко бывает так, что команда всего одна. Или для отладки надо одноразово выполнить несколько команд.
Для таких случаев можно создавать очередь неявно:
@@ -2252,11 +2390,11 @@ a[2] := 7;
a.GetArray.Println;
-
+
-
+
Все методы создающие одну команду (*CCQ.Add* методы и все методы неявных очередей)
-могут принимать очередь вместо значения в качестве любого параметра. Но в таком случае
+могут принимать очередь вместо значения в качестве практически любого параметра. Но в таком случае
возвращаемый тип очереди должен совпадать с типом параметра. К примеру:
## uses OpenCLABC;
@@ -2287,10 +2425,10 @@ a.GetArray.Println;
k.Exec1(N, a)
-
+
-
+
В данной справке в нескольких местах можно встретить утверждения вроде
Вызовы Context.BeginInvoke стоит, по возможности, объединять.
@@ -2298,30 +2436,28 @@ k.Exec1(N, a)
Данный раздел подпробнее объясняет устройство модуля, что делает подобные утверждения более понятными.
Читать его не необходимо для написания работающего кода, но желательно для написания качественного кода.
Но стоит сказать заранее - это не полное объяснение внутренностей модуля. Объясняется только то, что скорее всего окажется нужным.
-Если хотите ещё более полное понимание - используйте Ctrl+клик в IDE по именам, чтоб смотреть исходный код.
+Если хотите ещё более полное понимание - используйте Ctrl+клик в IDE по именам, чтобы смотреть исходный код.
Страницы:
-
-
+
+
Кроме самого выполнения очередей - им так же необходима инициализация и финализация.
Инициализация
Для начала, перед тем как любая из под-очередей в BeginInvoke начнёт выполняться - необходимо инициалировать Wait очереди.
-Иначе у ожидаемой очереди всегда будет шанс выполнится до того как ожидающая начнёт ожидать.
+Иначе у ожидаемого маркера всегда будет шанс выполнится до того как ожидающая его очередь начнёт ожидать.
Инициализация Wait очередей заключается в обходе всего дерева под-очередей.
-Для каждой группы Wait очередей, ожидающих общий маркер в общем .BeginInvoke,
-создаётся счётчик выполнений ожидаемой очереди.
+Для каждой Wait-группы (очередей, ожидающих общий маркер в общем .BeginInvoke),
+создаётся счётчик выполнений ожидаемого маркера.
Запуск очередей
Тоже заключается в обходе дерева под-очередей.
Но в этот раз очереди по которым прошлись - уже начали выполнятся, не ожидая окончания обхода всего дерева.
Как только этот обход закончен - метод BeginInvoke возвращает свой CLTask коду, вызвавшему его.
-Единственное исключение - если очередь, каким то образом, уже завершила выполняться, к моменту как обход дерева закончился.
-Такое возможно, к примеру, если все под-очереди - константные. В таком случае финализация выполняется до выхода из BeginInvoke.
Финализация
Заключается в чистке после выполнения - к примеру удалении всех созданных cl_command_queue.
@@ -2337,19 +2473,19 @@ Context.Default.SyncInvoke(B);
А значит как только A закончит выполнятся - ход выполнения перейдёт на B.
А во втором случае - между окончанием выполнения A и запуском B - будет произведено множество проверок,
а так же выходов/входов в объёмные (что значит JIT их не инлайнит) подпрограммы, как конструктор CLTask.
-Так же, в первом случае многие ресурсы, как объекты cl_command_queue,
+
Так же, в первом случае многие ресурсы, как объекты OpenCL.cl_command_queue,
выделенные при выполнении A, будут ещё раз использованы для выполнения B.
Да, всё это мелочи. Но нулевая задержка всегда лучше ненулевой.
Ну а когда всё же приходится вызывать 2 отдельных BeginInvoke, к примеру на 2 разных
-контекстах - можно использовать Wait очереди, чтоб добится того же эффекта:
+контекстах - можно использовать Wait очереди, чтобы добится того же эффекта:
c2.BeginInvoke(WaitFor(A)+B);
c1.BeginInvoke(A);
Внутренние оптимизации OpenCLABC делают этот код практически не отличимым по скорости, от BeginInvoke(A+B).
Единственное различие - время инициализации. Потому что A не запустится, пока не закончится вызов c2.BeginInvoke.
-
+