nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

Function inlining, QuatCore-style

Вчера к вечеру меня немного "перемкнуло", я не смог сосчитать до 7. Именно на столько отрезков оказалась поделена строка 11. Собственно, посчитать их легко: 2N+1, где N- количество обнаруженных "точек", в смысле пятен. Поэтому немудрено, что случилось переполнение буфера: из этих 7 точек мы одну сразу запросили, ещё 5 уложились в буфер, а последней ткнуться было уже некуда...

Так что в принципе всё вчера отработало правильно, но всё равно не так быстро, как мне хотелось бы. Всё-таки, именно этот код есть основания вылизывать до блеска, ведь он будет выполняться свыше тысячи раз за кадр, и как минимум 10 кадров в секунду, он обязан быть быстрым!

В голову пришли ещё способы, как повысить быстродействие, и наиболее дурацкий - запихнуть одну процедуру внутрь другой, чтобы при вызове хотя бы из одного места не нужно было в действительности куда-то прыгать!


В первую очередь, это актуально для процедуры AcqAndUpdate (и её "составной части" AcqNoUpd). Сейчас она вызывается из трёх мест. Вот первое из них:

			[SP++]	@@MidCycle		;после вызова AcqQueue возвращаемся
			JMP	AcqAndUpdate	;сразу на метку @@MidCycle
			;а вот и слияние. В простейшем случае увеличиваем размер точки, и на этом считаем работу выполненной...
@@DoMerge:		[Z+1]	[SP]	;пока так			


Мы "ручками" задаём адрес возврата, поэтому, перейдя в процедуру, мы уже не возвращаемся на следующую строчку, @@DoMerge, а "выпрыгиваем" сразу на начало цикла. Так почему бы нам не поместить процедуру прямо сюда, и выкинуть строку JMP AcqAndUpdate?

Элементарно:
					[SP++]	@@MidCycle		;после вызова AcqQueue возвращаемся
											;сразу на метку @@MidCycle

;"заказать" видеопроцессору два отрезка на обработку,
;первый - на поиск новой точки (т.е здесь мы пока не ожидаем ничего найти),
;второй - на уточнение координат существующей
;в [SP+1], теперь уже [SP], лежит X-координата
;в [SP+2j]=[SP+2], теперь уже [SP+1] - Y-координата точки,
;в [SP+2j+1]=[SP+3], теперь уже [SP+2j]=[SP+2] - яркость точки
;при вызове AcqAndUpdate обновим значения, при вызове AcqNoUpd - не будем

;в [X+1] лежит радиус точки,
;в [X+2j+k]=[X+2] - X-координата
;в [X+2j+1]=[X+3] - Y-координата
;в [X+4j+k]=[X+4] - яркость
	AcqAndUpdate proc	
			i		2
			NOP		0	;избежать Hazard, ведь i участвует в следующей строке
	@@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	
										
			;а вот и слияние. В простейшем случае увеличиваем размер точки, и на этом считаем работу выполненной...
			@@DoMerge:	[Z+1]	[SP]	;пока так	


Укоротили программу аж на одну команду (прыжок), снизили время выполнения на 3 такта, мелочь - а приятно.

Самое главное: вызов этой процедуры из других мест происходит в точности так же, как и раньше! Собственно, какое нам дело, где именно она лежит?

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


До тех пор, пока мы вызывали процедуру с помощью CALL, который "не терпит условностей", т.е вызывается всегда, это было понятно. Когда мы стали заносить адрес возврата в стек заблаговременно (чтобы сразу выпрыгнуть куда надо), то теперь вместо JMP можно вполне написать JL, а предыдущий JGE попросту выкинуть - мы попадём куда хотели "своим ходом":

		[SP++]	@@ActPointsStart	;нам по-любому придётся вернуться именно сюда!
		JL	AcqAndUpdate	;отсюда "выпрыгнем" сразу на метку @@ActPointsStart						
		;ветвь исполнения, если старая точка оказывалась ярче.
		;Нужно сообразить: не пора ли ей на покой, то бишь в список AllPoints?
		;в кои-то веки работаем с Y-координатой!
		Acc	[X+2j+1]	;координата точки (из списка)


JGE и JMP заменён на один-единственный JL. Дешево и сердито!

Как результат, экономится 2 лишних такта на прыжке в @@NotSoBright (конвейер не сбрасывается). Если же JL срабатывает, то экономится 1 такт за счёт отсутствия невыполнившегося JGE.

В злополучной 10-й строке тестового изображения мы пока ожидаем экономии в 7 тактов: 3 при добавлении новой точки, и ещё по 2 при выдаче "старых" отрезков.

ListOp
Эта процедура у нас вызывается в двух местах. В одном: чтобы добавить точку в список ActivePoints, в другом - чтобы перетащить её из ActivePoints в AllPoints. Она вызывается наиболее "классически", в одну строку:
CALL ListOp


что на самом деле транслируется в
[SP++]  CALL(ListOp)


то есть "правая часть команды" (SrcAddr) выдаёт на шину данных значение PC (Program Counter), и в качестве "побочного эффекта" прыгает куда надо (есть попросту 16 команд CALL, адрес для каждой "зашит" прямо внутри QuatCore). Левая часть команды заносит PC в стек, чтобы потом знать, куда возвращаться. А вернуться мы хотим на следующую команду, чтобы продолжить выполнение "как ни в чём не бывало".

Если мы хотим вставить процедуру прямо "в тело главной программы", часть которой выглядела так:

	Y	Heap									
	CALL	ListOp	;добавит в X новую, пока что пустую точку.
			;если добрались досюда, значит памяти хватило. X содержит адрес указателя на новую точку, а вот Z содержит адрес самой точки!
			;наполним её содержимым!						
	X	Z	;для дальнейшей работы (после MidCycle)... Чтобы мы не взялись за только что добавленную точку!



То получим в итоге такое:

					Y	Heap									
					;добавит в X новую, пока что пустую точку.
					[SP++]	@@AfterListOp
						
;взять первый элемент из списка Y и перецепить его в начало списка X
;значения X,Y остаются неизменными,
;при вызове должно быть k=0, затираем регистр Z, Acc и флаг знака
;адрес возврата хранится в стеке, как положено
	ListOp proc			
			ZAcc		RoundZero
			Z		[Y+k]
			SUB		[Y+k]
			[Y+k]		[Z+k]
			JGE		@@OutOfMem									
			[Z+k]		[X+k]
			[X+k]		Z
			NOP		0
			JMP		[--SP]
	@@OutOfMem:	[--SP]	Call(OutOfMemory)	;SP возвращается на место, но выпрыгиваем в другое место, на обработку ошибки
	ListOp endp						
											
			;если добрались досюда, значит памяти хватило. X содержит адрес указателя на новую точку, а вот Z содержит адрес самой точки!
			;наполним её содержимым!						
			@@AfterListOp:	X	Z		;для дальнейшей работы (после MidCycle)... Чтобы мы не взялись за только что добавленную точку!


Строк осталось ровно столько же, вся разница - в отсутствии заминки на "сброс конвейера", когда мы вызываем ListOp именно отсюда, поскольку нам не приходится прыгать. А вызов из другого места, опять же, "не замечает ничего странного" - ему глубоко по барабану, где в действительности лежит ListOp!

Толку от этого не шибко много, я оцениваю выигрыш ОДНИМ ТАКТОМ. Будет больше, если мы обучим QuatCore проверять адрес прыжка JMP, и если это аккурат следующая команда, ничего не делать, зная что именно в неё мы и так придём. Вот тогда и "выпрыгивание" из процедуры в этом частном случае не будет отнимать лишних 2 тактов на сброс конвейера. В принципе, техническая возможность это осуществить есть, притом малой кровью, лишними 3-4 ЛЭ. У нас же и так есть сумматор для относительного адреса. Сделать его "вычитатором" (поменять в относительных адресах плюс на минус - не проблема для компилятора), и во время прыжка по абсолютному адресу проверять разность, если там "-1" (все единички) - ничего не делаем!

В результате всех странных манипуляций, размер программы снизился на 2 слова, с 83 до 81, а время выполнения на злополучной 10-й строке тестового изображения должен снизиться на 8 тактов :)

Сейчас проверим. Так оно выглядело вчера, во время переполнения буфера:


От переполнения (когда дошли до пикселя номер 0x1F) до команды 0x8A (GPUH), которая вытаскивает значение из буфера, прошло 11 тактов. Поэтому, даже если действительно мы свои 8 "отыграем", всё равно от переполнения не спасёмся, так что размер выходного буфера увеличиваем с 5 до 6.

Как результат, вся драндулетина "в сборе" (включая генератор тестового изображения) сейчас занимает 1175 ЛЭ.

Наконец, запускаем симуляцию:


Поскольку мы увеличили размер выходного буфера, переполнения не случилось, вместо него лишь "зловещий комбинаторный выброс", а код явно "сдвинулся влево", то бишь ускорился. Теперь до команды 0x8A проходит только 7 тактов. Выходит, мы "отыграли" всего 4.

А вот и ответ, куда остальные такты пропали:

Мы опять застряли на отправке задания на GPU: размер входного буфера 8, и когда в нём ещё оставалось последнее задание 10-й строки (дождаться синхроимпульса и Back porch), и все 7 отрезков 11-й строки, то на отправке команды на синхроимпульс буфер заполнился - и мы стали ждать. Потеряли на этом 5 тактов.

Это всё равно не спасло бы нас от переполнения выходного буфера, оставь мы его "старый" размер, но теперь есть шанс пройти 11-ю строку, а если на её прохождение не хватит 5 тактов, мы знаем, откуда их взять :)


Сейчас возьмёмся за 11-ю строку тестового изображения, и если с ней всё в порядке, то нужно будет уже по-быстренькому "промотать" до самого конца, и затем уже по содержанию памяти разобраться, что же у нас получилось в итоге.
Tags: ПЛИС, программки, работа, странные девайсы
Subscribe

Recent Posts from This Journal

  • Нахождение двух самых отдалённых точек

    Пока компьютер долго и упорно мучал симуляцию, я пытался написать на ассемблере алгоритм захвата на ближней дистанции. А сейчас на этом коде можно…

  • Слишком общительный счётчик

    Вчера я чуть поторопился отсинтезировать проект,параметры не поменял: RomWidth = 8 вместо 7, RamWidth = 9 вместо 8, и ещё EnableByteAccess=1, чтобы…

  • Балансируем конвейер QuatCore

    В пятницу у нас всё замечательно сработало на симуляции, первые 16 миллисекунд полёт нормальный. А вот прошить весь проект на ПЛИС и попробовать "в…

  • Огари разговаривают

    Сегодня по пути на работу встретил огарей прямо в Лосином острове, на берегу Яузы. Эти были на удивление бесстрашны, занимались своими делами, не…

  • Ковыряемся с сантехникой

    Наконец-то закрыл сколько-нибудь пристойно трубы, подводящие к смесителю, в квартире в Москве: А в воскресенье побывал на даче, там очередная…

  • Мартовское велосипедное

    Продолжаю кататься на работу и с работы на велосипеде, а также в РКК Энергию и на дачу. Хотя на две недели случился перерыв, очередная поломка,…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 0 comments