nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

QuatCore+GPU: ещё чуть ускорим

Вот это место меня больше всего раздражает:

		JGE	@@NotSoBright	;похоже, на спад пошло. ТУТ ОБЯЗАН СТОЯТЬ ТОЛЬКО JGE, если нет - меняем местами операнды!	
					;нужно обновить координаты и яркость нашей яркой точки, а заодно и "размер" прибавить					
					;хорошо, "заказываем" два отрезка для этой точки: первый "на поиск", второй "на коррекцию".
@@QueueAnyway:	[SP++]	@@ActPointsStart	;после вызова AcqQueue возвращаемся
		NOP	CALL(AcqQueue)	;сразу на метку @@ActPointsStart						
					;ветвь исполнения, если старая точка оказывалась ярче.
					;Нужно сообразить: не пора ли ей на покой, то бишь в список AllPoints?
					;в кои-то веки работаем с Y-координатой!
@@NotSoBright:	Acc	[X+2j+1]	;координата точки (из списка)
		DIV2A	[X+1]		;прибавить половинку её диаметра
		SUB	[SP+2j]	;вычесть текущий номер строки
		JGE	@@QueueAnyway	;здесь ОБЯЗАН быть именно JGE, т.к флаг знака является одним из "аргументов" для AcqQueue
					;пора эту точку перетаскивать в другой список... 
		X	AllPoints
		CALL	ListOp
		X	Y		;теперь [X] ссылается уже не на обработанную и удалённую точку, а на следующую, так что всё верно
		JMP	@@ActPointsStart	;ура...					

...

AcqQueue proc	
		i		2	;даже если не нужно обновить точку, не навредит... (это мы Hazard убираем мучительно!)
		JGE		@@queue
		;здесь обновляем точку
@@updCycle:	[X+2j+i]	[SP+i]
		iLOOP		@@updCycle
@@queue:	Acc		[X+2j+k]
...



Такие фортеля здесь крутятся!


Смысл в том, что отправка заданий на GPU может происходить в трёх случаях:

1. Только что обнаружили новую точку и внесли её в список. Тогда нужно занести в список и все её параметры (координаты, яркость и диаметр пятна), и затем сформировать два отрезка на обработку. Этот вызов сейчас не показан.
2. Анализируем отрезок ПОД уже обнаруженной точкой, и нашли там точку ЕЩЁ ЯРЧЕ. Тогда нужно обновить параметры точки, указанные в списке, и сформировать два отрезка на обработку. Так происходит, если прыжок в @@NotSoBright не был выполнен. Тогда мы вызываем процедуру, и внутри неё также не срабатывает прыжок на @@queue, то есть мы не пропускаем обновление параметров точки.
3. Анализируем отрезок ПОД уже обнаруженной точкой, но яркость уже пошла на спад. Тогда нужно проверить, не пора ли вообще удалить эту точку из списка, поскольку мы уже далеко ушли от её центра, а если всё же не пора - вызвать процедуру и сформировать два отрезка для обработки, но НЕ ОБНОВЛЯТЬ параметры точки. Так происходит, если мы выполнили прыжок в @@NotSoBright, а оттуда после арифметических действий выполнили прыжок JGE @@QueueAnyway, а оттуда уже в процедуру, а раз здесь сработал JGE, он же сработает в процедуре, и мы пропустим обновление.

В случаях 2 и 3 мы должны из процедуры вернуться аккурат на метку @@ActPointsStart, с этим мы уже разобрались. А вот если мы всё-таки решили точку удалить, то процедура не вызывается вовсе, мы удаляем точку - и переходим на ту же самую @@ActPointsStart.

Так получилось, что самый частый сценарий (яркость пошла на спад, а удалять ещё рано) получил нехилые такие тормоза: сначала перепрыгиваем в @@NotSoBright, тратя 1 такт на прыжок и ещё 2 такта дополнительно для наполнения конвейера. Только мы оказались в процедуре - мы совершенно бесполезно для себя инициализируем регистр i, затем делаем ещё один прыжок, теряя на этом 3 такта - и только тогда выполняем что хотели. Итого 7 тактов. Ещё один такт мы теряем в других сценариях, проходя через бесполезный JGE, который заведомо не выполнится. Потери на нашей многострадальной 10-й строке составляют 15 тактов, поскольку там мы вызывали эту процедуру 3 раза, из них первый раз при добавлении новой точки, и два раза - для уже существующих, яркость которых пошла на спад.

Похоже, что можно довольно неплохо ускорить это дело, сохранив объём кода.

Первым делом, упрощаем процедуру:
	AcqAndUpdate proc	
			i		2	;даже если не нужно обновить точку, не навредит... (это мы Hazard убираем мучительно!)
			;здесь обновляем точку
	@@updCycle:	[X+2j+i]	[SP+i]
			iLOOP		@@updCycle
	AcqNoUpd:	Acc		[X+2j+k]
			DIV2S		[X+1]	;теперь в аккмуляторе у нас X - Ceil(D/2)
			ACQ		Acc		;первый отрезок
			ADD		[X+1]	;а вот теперь X + Floor(D/2)
			ACQ		Acc		;второй отрезок
			JMP		[--SP]
	AcqAndUpdate endp


Убрали ненавистный JGE, а ещё поменяли имя процедуры, теперь это AcqAndUpdate. А вот метка, куда раньше шёл прыжок, теперь называется AcqNoUpd, и она ГЛОБАЛЬНАЯ! Когда перед ней нету "собак", @@, это означает, что она видна отовсюду. (точнее, компилятор, когда видит @@, добавляет туда имя процедуры, получается что-то вроде AcqAndUpdate::AcqNoUpd, и пока никто не воспрещает обратиться по такому хитрому адресу. Но концептуально, @@ нужны чтобы давать "локальным" меткам короткие имена и не бояться повториться)

Смысл в том, что если надо обновлять список, вызываем AcqAndUpdate, а если только выдать задания на обработку - то AcqNoUpd. Как мы когда-то рассказывали, ключевые слова proc / endp служат лишь ограничителями видимости локальных меток, название процедуры - это просто метка на первую команду внутри этой процедуры, ну и в листинг оно попадает, чтобы хоть какую-то структуру ему придать. Поэтому вызвать AcqNoUpd - абсолютно не проблема.

К сожалению, при компиляции будет обнаруживаться Hazard между строками
i   2


и
@@updCycle:	[X+2j+i]	[SP+i]


так что кода осталось ровно столько же, и потери в один такт (на NOP) при вызове AcqAndUpdate мы не избежали. Но и хуже хотя бы не стало...

И поменяем код, вызывающий процедуру:
		[SP++]	@@ActPointsStart	;нам по-любому придётся вернуться именно сюда!
		JGE	@@NotSoBright	;похоже, на спад пошло. ТУТ ОБЯЗАН СТОЯТЬ ТОЛЬКО JL, если нет - меняем местами операнды!
					;нужно обновить координаты и яркость нашей яркой точки, а заодно и "размер" прибавить					
					;хорошо, "заказываем" два отрезка для этой точки: первый "на поиск", второй "на коррекцию".
		JMP	AcqAndUpdate	;сразу на метку @@ActPointsStart						
					;ветвь исполнения, если старая точка оказывалась ярче.
					;Нужно сообразить: не пора ли ей на покой, то бишь в список AllPoints?
					;в кои-то веки работаем с Y-координатой!
@@NotSoBright:	Acc	[X+2j+1]	;координата точки (из списка)
		DIV2A	[X+1]		;прибавить половинку её диаметра
		SUB	[SP+1]	;вычесть текущий номер строки
		JGE	AcqNoUpd	;здесь ОБЯЗАН быть именно JGE, т.к флаг знака является одним из "аргументов" для AcqQueue
		;пора эту точку перетаскивать в другой список... 
		X	AllPoints
		CALL	ListOp
		X	Y		;теперь [X] ссылается уже не на обработанную и удалённую точку, а на следующую, так что всё верно
		JMP	[--SP]	;ура...					


Мы сразу же заносим в стек "адрес возврата", метку @@ActPointsStart.

Если обнаруженная на текущей строке точка оказалась ярче, чем записано в списке, JGE @@NotSoBright не срабатывает, и мы ПРЫГАЕМ на AcqAndUpdate. Адрес возврата уже сформирован заранее, так что процедура выполнится от начала до конца, а вернёмся мы в @@ActPointsStart.

Если яркость пошла на спад, мы прыгаем сначала в @@NotSoBright. Там проверяем, не пора ли удалить точку из списка. (Мы предусмотрительно заменили [SP+2] на [SP+1], зная что указатель стека сдвинут на единицу вперёд. Ничего ценного нам адрес возврата не затёр.) Если ещё нет, мы прыгаем СРАЗУ ЖЕ в AcqNoUpdate. Никто нам этого запретить не может, прыгнуть из середины одной процедуры в середину другой процедуры, притом по КОМАНДЕ УСЛОВНОГО ПЕРЕХОДА! Опять же, адрес возврата уже лежит где надо, поэтому по завершении процедуры мы вернёмся в @@ActPointsStart.

Наконец, если пришла пора удалять точку из списка, начнётся полный треш и угар! А стеке уже лежит адрес возврата в @@ActPointsStart, но при вызове процедуры ListOp мы занесём в стек ещё один адрес возврата, при этом затрём X-координату самой яркой точки на текущей строке, ну и хрен с ней, всё равно там ничего не было. Спокойненько завершим свои дела и вернёмся по более "новому" адресу возврата куда надо, а в конце вместо JMP @@ActPointsStart мы написали JMP [--SP] - так мы прыгнем туда же, зато вернём указатель стека в исходное состояние. Не сделай мы этого - при каждом удалении точки у нас бы указатель стека сдвигался всё дальше и дальше, пока наконец не затёр бы чего-нибудь ценного.

Компиляция всего этого безобразия проходит успешно, в те же 83 слова кода. Кстати, теперь у нас всего две записи в таблице вызовов: ListOp и OutOfMem, тогда как AcqQueue исчезла - мы решили вызывать её через JMP с обычными метками.

Симуляция показала: мы сэкономили 11 тактов, поэтому на координате 0x18, где в прошлый раз случилось переполнение буфера, в этот раз всё в порядке. Но на координате 0x1F оно всё-таки произошло, и я не вполне понимаю, почему? Вроде ж у нас 6 отрезков, один мы вычленили, осталось 5, всё под завязку.

Да и если на то пошло: почему в прошлый раз уже на 0x18 переполнение случилось?

Разберёмся завтра...


Upd. С выходным буфером всё в порядке, я заработался и не смог посчитать до 7. Для 11-й строки мы заготовили СЕМЬ отрезков. Первый сразу прочитали, ещё 5 попали в буфер, а последний не влез.

А программу можно укоротить на 1 команду и ещё ускорить на 7 тактов (на злополучной 10й строке тестового изображения). Мелочь, а приятно.
Tags: ПЛИС, Программки, работа, странные девайсы
Subscribe

Recent Posts from This Journal

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 6 comments