Давным-давно, во времена студенчества в колледже я немного занимался разработкой компьютерных видеоигр. Это была эпоха 8-битных PC, когда игровое оборудование по современным стандартам было почти невозможно медленным.
Поэтому вас не должно удивлять, что программисты игр придумывали всевозможные безумные трюки, чтобы их игры работали с приемлемой скоростью. Безумные, безумные трюки.
Это история об одном из таких трюков.
Я постараюсь припомнить все важные подробности, однако в чём-то могу ошибиться. Если так случится, простите меня, это было очень давно.
Исходные данные
Мой друг, одарённый программист, почти закончил свою новую игру. Каким-то образом ему удалось почти без изменений уместить в компьютер эпохи 1980-х довольно впечатляющую графически на то время игру, популярную на аркадных автоматах.
Единственная проблема заключалась в том, что его версия игры оказалась неиграбельной. Она работала слишком медленно, а дёрганые движения мешали вовлечённости игрока, ведь игра была сайд-скроллером.
Мой друг, работавший над игрой параллельно с учёбой в колледже, начал уже ощущать себя немного вымотанным. Опасаясь, что мог упустить какую-нибудь простую оптимизацию, он попросил посмотреть код меня.
Я посмотрел. Но там нельзя было найти никакой простой оптимизации.
Код был очень оптимальным. Куда бы я не посмотрел, он уже реализовал всё то, что бы я только мог придумать. Циклы были развёрнуты. Необязательная отрисовка была устранена. Все признаки пустой траты ресурсов были уничтожены.
А вместе с ними и наши надежды на простоту исправления.
Но как насчёт сложного исправления? Может быть, даже безумного?
Ну, такая возможность существует всегда.
Однако прежде чем приступать к безумной оптимизации, мне нужно объяснить, как в те времена работало графическое оборудование.
Как работало графическое оборудование
Если вкратце, то графическое оборудование не делало ничегошеньки.
На PC, с которым мы работали, при необходимости отрисовки чего-то на экране это нужно было делать самостоятельно, байт за байтом. Никаких функций наложения текстур, никаких блиттеров. Только байты. Байты, которые нужно перемещать ручками.
Очень захватывающе.
Бо?льшую часть времени игра моего друга тратила на перерисовку фона. (Повторюсь: это сайд-скроллер.) В каждом кадре ей приходилось отрисовывать почти целый экран тайлов фона, сдвинутых на позицию игрока.
Если память меня не подводит, каждый тайл имел размер 28 на 28 пикселей. Каждый пиксель имел один из 16 цветов, то есть на его описание требовалась половина байта. Следовательно, в памяти тайлы были представлены в виде 28 соседних строк по 14 байт каждая. Первые 14 байт представляли первую строку пикселей, вторые 14 байт — вторую, и так далее.
Однако экран имел размер 320 пикселей в ширину и 240 пикселей в высоту. То есть в памяти буфер экрана размещался как 240 соседних строк по 160 байт каждый.
Следовательно, при копировании тайла по адресу X в позицию экранного буфера, начинающуюся с адреса Y, нужно скопировать 28 строк. Чтобы скопировать каждую строку, нужно скопировать 14 байт. К счастью, эта игра работала на процессоре 6809, имевшем несколько 16-битных индексных регистров и замечательные режимы адресации с «автоинкрементом» (наподобие добавления постфиксного оператора
++
к указателям на C). Это означало, что можно скопировать 4 пикселя за раз, одновременно в процессе изменяя регистры X и Y: LDU ,X++ ; read a 2-byte word (= 4 pixels) from source tile
STU ,Y++ ; write it to screen buffer
Чтобы скопировать всю строку, это нужно проделать семь раз, поэтому можно поместить эти строки в цикл со счётчиком 7 итераций:
LDB #7 ; remaining words <- tile width in words
@LOOP
LDU ,X++ ; read a 2-byte word (= 4 pixels) from source tile
STU ,Y++ ; write it to screen buffer
DECB ; reduce remaining-word count
BNE @LOOP ; loop while words remain
Закончив копирование строки, нужно переместить указатель точки назначения Y, чтобы он указывал на начальный адрес следующей строки, которую мы будем отрисовывать в экранный буфер. Так как экранный буфер имеет ширину 160 байт, а тайл имел размер всего 14 байт, необходимо было прибавить их разность к Y:
LEAY 160-14,Y
Вот и всё, мы скопировали строку на экран.
Но это только одна строка. Чтобы скопировать тайл целиком, нужно проделать то же самое 28 раз. Поэтому в свою очередь этот код мы засовываем в цикл со счётчиком 28 итераций.
Соединив всё вместе и дав имена всем важным числам, мы можем получить примерно такую подпроцедуру:
;;; important constants
SCRW = 160 ; screen-buffer width in bytes (= 320 4-bit pixels)
TILW = 14 ; background-tile width in bytes (= 28 4-bit pixels)
TILH = 28 ; background-tile height in rows
WOFF = SCRW - TILW ; s-b offset from end of one tile row to start of next
COPYTILE
;;;
;;; Copy a 28x28 background tile into a screen buffer.
;;; Arguments:
;;; X = starting address of background tile
;;; Y = starting address of destination in screen buffer
;;;
LDA #TILH ; remaining rows <- tile height
@COPYROW
LDB #TILW/2 ; remaining words <- tile width in words
@LOOP
LDU ,X++ ; read a word (= 4 pixels) from source tile
STU ,Y++ ; write it to screen buffer
DECB ; reduce remaining-word count
BNE @LOOP ; loop while words remain
;;
LEAY WOFF,Y ; advance dst ptr to start of next dst row
DECA ; reduce remaining-row count
BNE @COPYROW ; loop while rows remain
;;
RTS ; done! return to caller
И этот код будет хорошо работать.
Конечно, если вас не заботит скорость.
Заботимся о скорости
Зная, что игра скорее всего будет тратить основную часть времени на выполнение этого кода, вы бы поступили как любой хороший программист: начали бы считать такты. Снова покажем внутренний цикл с подготовкой и завершением, с указанием количества тактов:
LDB #TILW/2 ; 2 cycles (set-up)
@LOOP
LDU ,X++ ; 8
STU ,Y++ ; 8
DECB ; 2
BNE @LOOP ; 3
;;
LEAY WOFF,Y ; 8 (finishing)
Изучая эти величины, вы вряд ли упустите аж целых 21 такта на копирование всего 4 пикселей. То есть для копирования полной строки требуется 2 такта + (7 итераций) * (21 такт/итерация) + 8 тактов = 157 тактов. Ой-ёй.
Но и мы не в первый раз за клавиатурой. Мы знаем, что нужно сделать. Развернём этот цикл!
LDU ,X++ ; 8 cycles
STU ,Y++ ; 8
LDU ,X++ ; 8
STU ,Y++ ; 8
LDU ,X++ ; 8
STU ,Y++ ; 8
LDU ,X++ ; 8
STU ,Y++ ; 8
LDU ,X++ ; 8
STU ,Y++ ; 8
LDU ,X++ ; 8
STU ,Y++ ; 8
LDU ,X++ ; 8
STU ,Y++ ; 8
LEAY WOFF,Y ; 8 (finishing)
Теперь количество впустую тратящихся в цикле тактов снижено до нуля — мы избавились от подготовки, на каждую строку требуется всего 7 * (8 + 8) + 8 = 120 тактов. Ускорение на 30 процентов, довольно неплохо.
И на этом большинство программистов закончили бы.
Но не мой друг.
Он знал, что эти операторы
++
затратны, по 3 такта на каждый. А после разворачивания цикла он точно знал, где расположено каждое слово для чтения или записи относительно X или Y. Поэтому он остроумно заменил эти трёхтактовых постинкременты точными смещениями. Каждое из них стоит всего 1 такт, а смещение на 0 по сути бесплатное: LDU ,X ; 5 cycles
STU ,Y ; 5
LDU 2,X ; 6
STU 2,Y ; 6
LDU 4,X ; 6
STU 4,Y ; 6
LDU 6,X ; 6
STU 6,Y ; 6
LDU 8,X ; 6
STU 8,Y ; 6
LDU 10,X ; 6
STU 10,Y ; 6
LDU 12,X ; 6
STU 12,Y ; 6
LEAX TILW,X ; 8 (finishing)
LEAY SCRW,Y ; 8 (finishing)
После таких оптимизаций количество тактов на строку снизилось до (5 + 5) + 6 * (6 + 6) + (8 + 8) = 98 тактов. По сравнению с первоначальным кодом ускорение составило 60 процентов:
original_speed = (1*row) / (157*cycle)
optimized_speed = (1*row) / (98*cycle)
speed_up = optimized_speed / original_speed - 1 = 157 / 98 - 1 = 0.60
Соединим всё вместе (я снова делаю это по памяти, поэтому код мог быть и немного другим), и подпроцедура копирования тайла будет выглядеть примерно так, копируя полный тайл (все 28 строк) всего за 2893 такта:
COPYTILE2
;;;
;;; Copy a 28x28 screen tile into a screen buffer.
;;; Arguments:
;;; X = starting address of background tile
;;; Y = starting address of destination in screen buffer
;;; Execution time:
;;; 4 + 28 * (82 + 8 + 8 + 2 + 3) + 5 = 2893 cycles
;;;
LDA #TILH ; initialize row count (4 cycles)
;;
@COPY1
;; unroll inner loop (copies one row of 28 pixels in 82 cycles)
LDU ,X ; (1) read 4 pixels (5 cycles)
STU ,Y ; write 4 pixels (5 cycles)
LDU 2,X ; (2) (6 cycles)
STU 2,Y ; (6 cycles)
LDU 4,X ; (3) ...
STU 4,Y ; ...
LDU 6,X ; (4)
STU 6,Y ;
LDU 8,X ; (5)
STU 8,Y ;
LDU 10,X ; (6)
STU 10,Y ;
LDU 12,X ; (7)
STU 12,Y ;
;;
LEAX TILW,X ; advance src to start of next row (8 cycles)
LEAY SCRW,Y ; advance dst to start of next row (8 cycles)
DECA ; reduce remaining count by one (2 cycles)
BNE @COPY1 ; loop while rows remain (3 cycles)
;;
RTS ; done! return to caller (5 cycles)
Этот код в сумме оказался на 60% быстрее, чем наивный код
COPYTILE
, с которого мы начали.Но он не был достаточно быстрым, даже близко.
Поэтому когда друг показал мне свой код и спросил, смогу ли я его ускорить, я на самом деле хотел ему помочь. Я действительно хотел ответить «да».
Но мне пришлось ответить «нет». Мне ужасно не хотелось давать такой ответ. Однако, изучив код, я не нашёл никаких способов его ускорить.
Тем не менее, крючок с наживкой был заброшен.
Я не мог выкинуть эту задачку из головы. Вырос я на компьютерах Apple II и их процессорах 6502. Однако код моего друга исполнялся на 6809. Возможно, он позволяет создавать оптимизации, о которых я не знаю.
Возродив свой оптимизм, я набрал номер университетской библиотеки для доступа к каталогу карточек. (В те времена ещё не было World Wide Web.) Через эмулятор терминала VT220 я поискал книги о 6809.
Нашлась только одна: руководство по микропроцессору 6809. Оно находилось в инженерной библиотеке. К счастью, я учился на инженера и имел возможность брать книги.
Поэтому я отправился в библиотеку.
Безумная идея
Добравшись до инженерной библиотеки, я нашёл книгу именно там, где она и должна была находиться, ожидая небольшой встряски:
The MC6809-MC6809E Microprocessor Programming Manual.
Не озаботившись поиском стула, я стоя пролистал страницы в поисках какой-нибудь нетипичной, специфической для 6809 команды способной быстро получать и принимать множество байт. Однако страница за страницей ничего не находилось.
Затем я наткнулся на PSHS. «Push registers on the hardware stack» («Поместить регистры в аппаратный стек»).
Если на моём любимом 6502 вам нужно было сохранить регистры в стек, то это надо было делать по одному за раз, да и ещё передавая их через накопитель. Это было медленно и затратно. Поэтому когда была важна скорость, я учился избегать использования стека.
Однако на 6809 можно сохранять все регистры (или любое их подмножество) одной командой. Удивительно, но для этого требовалось всего 5 тактов, плюс по одному такту за каждый записываемый в стек байт.
Так как процессор имел три 16-битных регистра общего назначения — D, X и Y — я мог загрузить их, а затем использовать команду
PSHS
, чтобы записать 6 байт всего за 11 тактов. Соответствующая pull-команда, PULS
, имела столь же низкие затраты.Более того, 6809 имел два регистра стека, S и U. Я мог использовать один как указатель на источник, а другой — как указатель на точку назначения. Теоретически, при помощи одной пары
PULS
/PSHU
я мог копировать 6 байт за 22 такта.Это безумно, безумно быстро.
Охваченный восторгом, я подошёл к столу библиотекаря и вытащил свой студенческий билет. Я собирался взять эту книгу для дальнейшего изучения.
Безумный план
По дороге назад в общежитие я сформировал свой план.
Я буду сохранять куда-нибудь регистры S и U, а затем указывать при помощи S на тайл фона, а при помощи U на экранный буфер. Затем я буду извлекать данные из S и записывать в U, копируя по 6 байт за раз при помощи D, X и Y в качестве промежуточных носителей. Для копирования 14 байтов, составляющих строку, потребуется три такие итерации, которые в развёрнутом виде составят примерно 60 тактов.
Добравшись до своей комнаты, я нашёл лист бумаги и набросал черновик:
PULS D,X,Y ; first 6 bytes 11 cycles
PSHU D,X,Y ; 11
PULS D,X,Y ; second 6 bytes 11
PSHU D,X,Y ; 11
PULS D ; final 2 bytes 7
PSHU D ; 7
LEAU -WOFF,U ; advance dst ptr 8
Всего 66 тактов, в том числе и корректировка U после строки, подготавливающая его к следующей строке. (Обратите внимание, что корректировка теперь отрицательна.) Для сравнения: наивный цикл копирования строк выполнял ту же задачу за 157 тактов. А оптимизированный код моего друга за 98. Эта безумная идея уже казалась серьёзным выигрышем.
Однако у нас была очень неуклюжая последняя пара
PULS
/PSHU
! Ей нужно было обрабатывать последние два байта строки, потому что строки имели ширину 28 пикселей = 14 байт, а 14 не делится нацело на 6.Этот чёртов остаток в 2 байта!
Вот если бы в игре использовались тайлы 24 на 24 пикселя… Но это было не так, поэтому я тщательно изучил руководство в поисках способа снижения затрат на эту неизбежную последнюю пару.
И, к своему удивлению, я наткнулся на золотую жилу! Это была ещё одна особенность процессора 6809 — регистр DP.
В 6502 и большинстве 8-битных процессоров той эпохи нижние 256 байт памяти назывались нулевой страницей. Эта нулевая страница была особой, потому что её ячейки памяти имели однобайтные адреса и к ним можно было получить доступ с помощью более коротких и обычно более быстрых команд.
Проектировщики 6809 развили эту идею ещё глубже. Они позволили программистам использовать регистр DP, чтобы назначать любую страницу в качестве нулевой, которую они называли «direct page».
Но ни одна из моих передающих байты команд не требовала применения direct page. Это означало, что можно использовать регистр DP как ещё одно дополнительное промежуточное хранилище. Теперь я мог копировать 7 байт каждой парой pull-push!
И 14 точно делится на 7 нацело.
После внесения этого изменения я мог копировать целую 28-пиксельную строку и переходить к следующей всего за 5 команд:
PULS D,X,Y,DP ; first 7 bytes 12 cycles
PSHU D,X,Y,DP ; 12
PULS D,X,Y,DP ; second 7 bytes 12
PSHU D,X,Y,DP ; 12
LEAU -WOFF,U ; advance dst ptr 8
56 тактов!
Благодаря этому фрагменту кода я почувствовал себя потрясающе. Мне удалось задействовать каждый доступный в машине регистр для передачи байтов! D, X, Y, U, S и даже нетипичный DP — все они задействовались в полную силу.
Мне очень нравилось это решение.
За одним исключением…
Мне нужно сделать ещё одну безумную вещь
Если вы знакомы со стеками, то могли заметить небольшую погрешность в моём блестящем плане:
Push и pull работают в противоположных направлениях.
Видите, ли я обманул вас, показывая фрагмент кода. Я не копировал одну строку из тайла фона на экран. На самом деле он разбивал строку на два блока по 7 байт (я назову их септетами), а затем отрисовывал их на экран в обратном порядке.
Помните те строки тайлов, которые удобно укладывались в память как 14 соседних байт?
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| 0 1 2 3 4 5 6 7 8 9 A B C D |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
Когда мы выполняли pull и push строки на экран в 7-байтных септетах, они разделялись горизонтально таким образом:
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| 7 8 9 A B C D | 0 1 2 3 4 5 6 |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
Первый септет становился вторым, а второй — первым. Однако обратите внимание на то, что байты в рамках каждого септета оставались неизменными, сохраняя исходный порядок.
Более того, если выполнить код копирования строк несколько раз, то вертикальный столбец копируемых срок окажется перевёрнутым. Так происходит потому, что код использует push для записи строк на экран. Команды push перемещают указатель стека в сторону нижних адресов, а нижние адреса соответствуют строкам экрана, расположенным выше на дисплее.
Чтобы продемонстрировать хаос, которое может создавать этот переворот строк и обмен местами септетов, давайте представим, что у нас есть такой тайл ключа размером 28 на 28:
Если бы вы использовали 28 раз мой код копирования строк для отрисовки его на экране, то получили бы вот такой не-ключ:
То есть… в этом была моя проблема.
Однако у этой проблемы тоже имелось изящное решение!
Опишем проблему вкратце:
- строки перевёрнуты
- септеты в каждой из строк поменялись местами
- но байты в пределах каждого септета остались неизменными
Моё открытие заключалось в том, чтобы рассматривать «переворот» и «обмен местами» как два примера одного явления. Обращения.
Обращение имеет удобное свойство — оно является инволюцией. Обратите любой объект дважды, и вы получите исходный объект. Учтя это, я начал рассуждать, что если тайлы предварительно обработать так, чтобы обратить их строки, а затем септеты внутри строк, то при выводе на экран тайлы будут выглядеть нормально. Все правки, возникшие во время копирования и предварительной обработки, фактически отменят друг друга.
Проработав всё это на бумаге, я также обнаружил, что этап предварительной обработки может игнорировать строки и просто считать тайлы одной длинной линейной последовательностью септетов1. Чтобы понять, почему это так, давайте рассмотрим небольшой тайл из 2 строк по 2 септета каждая:
+---+---+
| a b |
+---+---+
| c d |
+---+---+
В памяти они будут размещены в построчном порядке, то есть как 4 последовательных септета:
+---+---+---+---+
| a b | c d |
+---+---+---+---+
То есть переворот строк с последующим обменом местами септетов в каждой строке будут аналогичны перевороту порядка септетов в памяти:
+---+---+ +---+---+ +---+---+
| a b | | c d | | d c |
+---+---+ =======> +---+---+ =======> +---+---+
| c d | reverse | a b | swap | b a |
+---+---+ rows +---+---+ septets +---+---+
+---+---+---+---+ +---+---+---+---+ +---+---+---+---+
| a b | c d | | c d | a b | | d c | b a |
+---+---+---+---+ +---+---+---+---+ +---+---+---+---+
То есть у проблемы с искажениями решением оказался простой единовременный этап предварительной обработки: достаточно просто разбить каждый тайл на септеты и обратить их порядок.
Зная, что проблему с искажениями можно решить, мне начала нравиться вся идея в целом. я не видел никаких других проблем, поэтому накидал черновик новой подпроцедуры копирования тайлов, чтобы показать её другу. Так как логика копирования строк занимала теперь всего 5 команд, я использовал её 4 раза, немного развернув последний оставшийся цикл. Теперь вместо копирования 28 одиночных строк я копировал 7 четверных строки.
Код выглядел примерно так:
COPYTILE3
;;;
;;; Copy a 28x28 screen tile into a screen buffer.
;;; Arguments:
;;; X = starting address of *septet-reversed* background tile
;;; Y = starting address of destination in screen buffer
;;; Execution time:
;;; 34 + 7 * (224 + 7 + 3) + 7 + 10 = 1689 cycles
;;;
;; setup: 34 cycles
PSHS U,DP ; save U and DP (8 cycles)
STS >SSAVE ; save S (7 cycles)
;;
LDA #TILH/4 ; initial quad-row count (2 cycles)
STA >ROWCT ; (5 cycles)
;;
LEAS ,X ; initialize src ptr (4 cycles)
LEAU (TILH-1)*SCRW+TILW,Y ; initialize dst ptr (8 cycles)
;;
@COPY1
;; copy four rows of 28 pixels in 4 * (48 + 8) = 224 cycles
PULS X,Y,D,DP
PSHU X,Y,D,DP
PULS X,Y,D,DP
PSHU X,Y,D,DP
LEAU -WOFF,U
PULS X,Y,D,DP
PSHU X,Y,D,DP
PULS X,Y,D,DP
PSHU X,Y,D,DP
LEAU -WOFF,U
PULS X,Y,D,DP
PSHU X,Y,D,DP
PULS X,Y,D,DP
PSHU X,Y,D,DP
LEAU -WOFF,U
PULS X,Y,D,DP
PSHU X,Y,D,DP
PULS X,Y,D,DP
PSHU X,Y,D,DP
LEAU -WOFF,U
;;
DEC >ROWCT ; reduce remaining quad-row count by one (7 cycles)
BNE @COPY1 ; loop while quad-rows remain (3 cycles)
;;
LDS >SSAVE ; restore S (7 cycles)
PULS U,DP,PC ; restore regs and return to caller (10 cycles)
SSAVE ZMD 1 ; stash for saving S while drawing
ROWCT ZMB 1 ; var: remaining rows to copy
После сложения тактов их сумма составила всего 1689. Я сократил код друга почти на 1200 тактов. Ускорение на 70 процентов!
Сияя от радости, я направился к другу.
Кислотный тест
Когда я встретился с другом и сказал, что разобрался, как сделать процедуру копирования тайла на 70% быстрее, его лицо озарилось. Когда я немного объяснил всю концепцию искажений и септетов, он помрачнел и засомневался. Но когда я показал ему код, он всё понял. В голове «щёлкнуло».
«Давай его проверим», — сказал он.
Спустя примерно полчаса работы он интегрировал код в игру. После пересборки и перезапуска игра начала загружаться.
Появился экран заставки.
Он запустил игру.
И…
Чёрт побери!
Она казалась удивительно быстрой. На самом деле, вероятно, только на треть или на половину, но этого было достаточно. Мы превзошли некий порог восприятия. Игровой процесс теперь казался плавным и естественным. Разницу можно было ощутить.
Мы просто продолжали играть в игру и улыбаться. Это был хороший день.
Но наша радость была недолгой.
Семь бед...
Спустя несколько дней проблема со скоростью была далеко позади (или так нам казалось), оставалось добавить окончательные штрихи. Одним из таких штрихов были сэмплируемые звуковые эффекты. Для этого требовалось несколько раз в секунду передавать фрагменты размером в байт в ЦАП вывода звука. Чтобы запланировать передачу, мой друг включил прерывание по аппаратному таймеру.
Всё работало замечательно. Звуки звучали отлично, и всё остальное было идеально.
За одним исключением.
Поиграв в игру однажды вечером, мы заметили, что некоторые тайлы повреждаются. И чем больше играешь в игру, тем больше появляется повреждений.
О, нет.
Происходило что-то очень плохое.
И потом нас озарило.
Что происходит, если прерывание срабатывает во время выполнения процедуры копирования тайлов?
Чтобы подготовиться к вызову обработчика прерывания, процессор помещает своё текущее состояние в системный стек. Однако во время процедуры копирования тайлов системного стека нет. Процедура «конфисковала» регистр системного стека S. И куда же он указывает? Прямо на буфер памяти, содержащий исходные тайлы!
Упс.
Мы пали духом. После всех этих усилий трудно поверить, что наше совершенное необходимое ускорение вызывает повреждение памяти…
Нуждаясь в отдыхе, мы дошли до круглосуточного кафе рядом с кампусом, чтобы поесть и подумать. Поглощая блинчики и бекон, мы обсуждали проблему.
Существовало всего два стековых регистра и без использования их обоих процедура копирования тайлов и близко не будет такой быстрой. Таким образом, мы никак не могли вернуть системе регистр S, не потеряв при этом с трудом заработанный выигрыш в скорости. Но в то же время не было никакого способа реализации надёжного звука без использования прерываний.
Значит, тем или иным образом прерывания должны работать. А если они работают, то при выполнении копирования тайлов всё, на что указывает S, будет повреждено.
«Как можно предотвратить это повреждение?», — спросил я.
Мы сидели, забыв о еде, и вопрос повис в воздухе.
Внезапно мой друг хлопнул по столу. Он понял.
«Не нужно его предотвращать!», — сказал он.
«Что?»
«Не будем предотвращать повреждение. Пусть оно происходит. Просто не с исходными тайлами».
Это на самом деле было просто. Он продолжил:
«Просто поменяем местами S и U в процедуре копирования тайлов. U будет указывать на исходные тайлы, а S — на экранный буфер. Если сработает прерывание, то повреждение произойдёт там, куда указывает S — на экране. Повреждение сохранится только до перерисовки следующего кадра».
«Гениально!», — сказал я.
Стремясь проверить его решение, мы быстро закончили трапезу.
… один ответ
Однако возвращаясь в общежитие мы обеспокоились тем, что игроки смогут увидеть экранные глитчи, даже если это будет всего лишь на кадр. Оба мы чувствовали, что должен быть какой-то способ сделать хак идеальным, и нам просто нужно его найти.
Позже тем же вечером мы его нашли.
Как только мы его придумали, он снова оказался простым. Нам достаточно было всего лишь изменить порядок тайлов. Вместо того, чтобы размещать тайлы на экране сверху вниз, слева направо, мы двинемся справа налево, снизу вверх. Другими словами, от высоких адресов к низким.
Таким образом, при срабатывании прерывания и повреждении ячеек памяти непосредственно перед текущим отрисовываемым тайлом повреждение будет устранено при отрисовке следующего тайла. А поскольку отрисовка тайлов выполняется в буфере, который не отображался до полной отрисовки (и выполнения вертикального обновления), никто никогда не увидит повреждения2.
Это был идеальный хак!
За старые деньки!
Вот так всё тогда и происходило. Трудности заключались не в чрезвычайной сложности систем, как это бывает сегодня. Трудность была в том, чтобы засунуть свои идеи в столь медленные и ограниченные в возможностях машины, что большинству идей в них не хватало места.
Приходилось экспериментировать с идеями, перекручивать их и поворачивать, искать что-то, что-нибудь, что позволит запихнуть их в машину. Иногда ты находил это что-то, и становился на шаг ближе к реализации своих идей, иногда нет.
Однако такой поиск всегда был очень поучительным.
В данном случае поиск привёл к серии небольших побед, которые совместно решили нашу проблему. Но если учесть всё, что требуется, чтобы сделать эту чёртову игру быстрой, это может показаться безумием.
Мы начали с процедуры копирования тайлов, ядро которой состояло из настроенного вручную развёрнутого цикла машинных команд. Затем для получения ускорения на 70%:
- Мы заменили эту подпроцедуру очень специфичным признаком проявления нашего безумия, конфискующим оба стековых указателя и использующим извлечение и запись в стеки, а также все доступные регистры для отрисовки тайлов вниз головой и сломанными по горизонтали.
- Затем мы предварительно обработали тайлы, чтобы отрисовка на самом деле их исправляла.
- Но потом (чёрт возьми!) оказалось, что прерывания, запускаемые во время отрисовки, способны повредить исходные тайлы.
- Поэтому для защиты исходных тайлов мы повреждали вместо них экранный буфер.
- Но такое повреждение было бы видимым.
- Поэтому мы изменили порядок размещения тайлов, чтобы чинить (на лету) любые повреждения, которые могут возникнуть, прежде чем они успеют отобразиться на экране.
- И всё это сработало!
Мы сделали это. Ради 70 процентов.
И это полностью оправдало себя.
Расскажи свою историю
Я хотел поделиться этой историей по двум причинам.
Во-первых, это интересная история, одно из моих первых воспоминаний о борьбе с запутанной компьютерной проблемой. Несмотря на её ожесточённое сопротивление, мы пришли к решению, которое оказалось и эффективным, и изящным. Результат доставил нам громадное удовлетворение.
Я узнал, что борьба оправдывает себя.
Во-вторых, я хотел вдохновить вас рассказать свои истории. Я знаю, что «в былые времена» создание большинства видеоигр приводило к появлению множества таких историй. Я бы хотел их услышать. Но слишком многие из них утеряны, стёрты из памяти до того, как кто-нибудь догадался их сохранить.
Если у вас есть история, не ждите. Расскажите её, с каждым днём ожидания это становится сложнее.
- Задание: доказать для всех конечных последовательностей из конечных последовательностей, что применение (reverse ? concat) эквивалентно применению (concat ? reverse ? map reverse).
- Нам также нужно было проверять, что ничего ценного не хранится в ячейках памяти непосредственно перед экранным буфером. Теоретически они тоже могли бы быть повреждены при срабатывании прерывания в момент размещения верхнего левого септета верхнего левого углового тайла. Адрес этого септета соответствует началу буфера.
Fox_exe
Эх, оптимизация… Как же её не хватает в современном мире интерпритируемых («Управляемых») языков…
MinimumLaw
Если отказаться от «управляемых» языков, то очень быстро окажешься в мире встраиваемых систем и низкоуровневого программирования, где до сих пор главенствуют C (просто С) и ассемблер. И хватает простора для безумных оптимизаций.
Приятно читать такие статьи. Еще приятнее осознавать, что подобные «хаки» уже использовались в твоих решениях. К счастью сегодня в большинстве случаев можно не считать такты (и не экономит байты XOR'я указатели). Впрочем, это умение всегда оказывается востребованным в самый неожиданный момент. И владеть им очень даже полезно.
Fox_exe
Благодаря Ардуине и прочим микроконтроллерам — такие специфические знания по-прежнему популярны (более-менее).
MinimumLaw
Боюсь тут мы с Вами по разные стороны баррикад. С моей колокольни именно Arduino первой обесценила умение «мыслить оптимальным кодом на ассемблере». А когда выяснилось что для более-менее серьезных задач это все же необходимо подоспел STM32, который позволил «не думать оптимально» дальше.
В итоге если раньше меня звали на проект с фразой «Мы хотим того и этого. Что тебе надо для того чтобы сделать?», то теперь всегда одно и то же «У нас STM32, а молодой и перспективный предшественник слился». И уж поверьте на слово, «молодой и перспективный» в подавляющем большинстве случаев совсем не горел желанием «оптимально мыслить на ассемблере». Скорее ему хотелось «нагородить фичь и выполнить все хотелки руководства». И пока получалось — работал. А как перестало… зовут меня.
Конечно, глупо винить в таком раскладе Arduino или STM32. Но и признавать их полную непричастность я тоже отказываюсь.
Fox_exe
Ну да… Ардуину я зря упомянул. Тут надо STM32F103 + HAL, хотябы, упомянуть. Или STM8…
FForth
У меня был один из любимых «хаков» в программе на ассемблере
проверить один регистр, например на нуль — т.е. сформировать соответствующий флаг в регисте состояния (например флаг Z-нуля, на допустимый тайм-аут выполнения текущего цикла), а другой командой проверить ещё какой то регистр, но командой не изменяющей первый флаг, а устанавливающий/сбрасывающий другой флаг (наприме флаг переноса Carry), И финальной командой или остаться в цикле или выйти из него по результату командой перехода анализирующей оба флага (знаковые, безнаковые… переходы)
P.S. Вот, такие и ещё «моменты», хотелось бы видеть в учебниках применения ассемблера… :)
MinimumLaw
Ммм… А нужен ли здесь ассемблер? Насколько я знаю любой более-менее уважающий себя компилятор C умеет (и с радостью проделывает подобное) на уровнях оптимизации больше «none».
Вообще, в современном мире вопрос оптимизаций он очень скользкий. Мне чаще всего приходится бороться с избыточной оптимизацией. Когда компилятор в искренней уверенности что он пытается сделать самый быстрый (или самый маленький) код «ломает» критические точки «быстрого» алгоритма. А тот в свою очередь взят был ровно потому, что «наивная» (она же «понятная») реализация оказывается слишком медленной.
Ну и спуск на уровень ассемблера… Это тема отдельного разговора. И, конечно, это всегда компромис. Другое дело, что работа со встраиваемыми системами это всегда бесконечный день сурка. Каждая новая железка начинается с написания того, что уже было написано неоднократно ранее. Та сама ситуация, против которой всегда выступает так любимый прикладным сообществом лозунг «не повторяйся» (DRY code). Но… Каждая новая итерация приносит чуть больше понимания и делает код чуть быстрее и чуть более качественным. Очень медленный процесс. Фундаментальные изменения крайне редки. С другой стороны, этот самый лозунг «не повторяйся» больно бьёт прикладников порождаю «технический долг» и прочий «Legacy Code». Так что своя порция «счастья» есть везде.
Потому мой обычный принцип состоит в том, что если не нравится итоговый ассеблерный код — правь алгоритмы на C. И только если это невозможно спускайся ниже. Другое дело, что понять что именно не нравится и определиться с тем надо ли спускаться можно только понимая ассемблер и зная как именно один код преобразуется в другой (и как на этот процесс можно влиять). Но это наш кактус — кактус embedder'щиков. И мы его потихоньку грызём.
slovak
Ну ладно Ардуино с кучей С++ абстракций над GPIO, а с STM32 что не так?
MinimumLaw
Так и с Ардуино проблем нет. AVR классный чип с отличным набором инструкций и превосходной документацией. Он очень на многое способен.
Что до STM32, то у меня к нему вопросы исключительно по качеству документации. Кто реально пытался с ним работать на уровне «шуршать по регистрам» тот знает о чем речь. Частенько приходилось заглядывать в библиотеку или HAL и натыкаться на странные вещи типа важной не задокументированной последовательности инициализации периферии.
Впрочем, возможно уже поправили. И документацию и чипы. Просто лет 5 назад это настолько взбесило, что зарекся с этим чипом писать на уровне регистров. А современный HAL, наконец позволяющий делать нормальные callback'и, конечно, красив. И, самое главное, под капотом имеет довольно не плохой код. Совсем не чета старой Standard Peripheral Library.
Впрочем, низкий порок вхождения генерирует проблемы. Что на Ардуино, что на STM32. Пока эти проблемы не выходят за рамки DIY — проблем нет. Но обнаружить хм… дурно пахнущий код… в промышленном изделии (крайне плохо) работающим тысячами и раскиданным по всей территории России… А самое главное потом его полностью переписать и понять что STM32 не недостаточен (как считалось) а избыточен (как получилось)… И если б это единичная история… Потому с моей колокольни это одного поля ягоды.
Впрочем, упаси меня бог запрещать кухонные ножи, топоры или автомобили только по той причине, что они могут убить. Потому отвечаю на Ваш вопрос так — проблема не в конкретных чипах. Проблема в людях, которые эти конкретные чипы используют. И если Atmel (теперь Microchip) несколько дистанцируется от Arduino, то ST Microelectronics всячески способствует увеличению таких «специалистов». Для них это хорошо. Для рынка, пожалуй, тоже неплохо. Но кто сказал что моя оценка обязана совпадать с рыночной?
Mike_soft
а много ли ардуинщиков знает, что «у неё внутре
неонкаatmel»?Ну а ST просто захватывает рынок. И переводит его из «элитарного» в утилитарное. 30 лет назад слово микроконтроллер знали, условно, единицы. 20 — уметь что-то сделать на МК было крутостью. 10 лет назад это стало обычным в узких кругах, и элитарным в среде школьников благодаря ардуине. сейчас не освоить ардуину — стыд и позор, а STM хочет занять ее место для всех слоев — от школоты, использующих вместо ардуины — нуклео или блупилл (да, ftitzing и вот это всё), включая середнячков, которые знают, что такое принципиальная схема, и которым кубик просто сильно экономит время, и заканчивая такими, как Вы, профессионалами. И у STM это получается.
MinimumLaw
Наверное мне стоит благодарить ST Microelectronics за то, что без работы я не сижу. Но, предельно честно, это не та работа которой хочется заниматься. Хочется разрабатывать и рождать не костыльные решения (и это моя основная работа уже много лет), а приходится подтирать сопли за теми, кто переоценил свои силы в плане освоения STM32 (и это тоже многолетняя подработка).
И, слушайте, откуда взялось «элитарное», «утилитарное»? Это чья оценка? По мне или ты профессионал и делаешь продукт или… не профессионал и не делаешь. Во всяком случае за деньги. Я же не бросаюсь учиться оперировать на мышах и соседских собаках и не кричу что это «элитарное» умение должно быть продвинуто в массы и стать «утилитарным».
Резюмируя скажу так. Я бы предпочел жить без подработки. Но это тема политэкономии, а не техники. Потому простите, но… Я завязываю.
Mike_soft
Выдавать глобальные идеи — это удовольствие; искать сволочные маленькие ошибки — вот настоящая работа. (Фредерик Брукс-мл)
Между «профессионал» и «не профессионал» — широкий спектр. У некоторых это целая жизнь.
«элитарное» — это доступное немногим. «утилитарное» — доступное всем. Жизнь постоянно переносить элитарность в утилитарность, в т.ч. и в профессиях. В начале прошлого века «шофер» был крутой дядька в кожаной куртке и очках, ездящий на настоящей автомашинеЭ с замасленными руками и жутко умный (ибо еще и чинил свою машину). Сейчас шофером может быть даже слабоумная «блондинка». Редкое и особенное стало распространенным и обычным.
MinimumLaw
Даешь атомный реактор в широкие массы! Раньше электричеством занимался только головастый Фарадей, а теперь любой дошкольник может не просто замкнуть электрическую сеть включая свет в туалете, так ещё и способен обуздать полупроводники банальным нажатием кнопки на планшете. Было б смешно, если б не было так грустно.
Ваш пример в шофером — сущая провокация. Даже в извозчики брали не всех. Нужно было пройти обучение и подтвердить квалификацию. В частности и по знанию ПДД. Извозчик почти всегда должен был иметь навыки конюха (и, отчасти, ветеринара). Ранние шоферы — не просто водители, они еще и автомеханики. А нередко еще и автоконструкторы. Куда там любой современной блондинке до старого шофера. Как и до шеф-повара. Механически повторять рецепты на кухне (даже немного разнообразя их количеством специй) совсем не то же самое, что придумать реально новое блюдо. Да и дать мужу или ребенку парацетамольчик, чтоб сбить температуру блондинка может. И то в зависимости от степени «блондинистости» — а то ведь только после подсказки провизора из аптеки (по сути варианта блондинки с медицинским уклоном).
Не думаю, что подавляющее большинство населения заинтересовано в том, чтоб блондинки без специального образования (а часто и огромной практики под руководством более опытных соратников) водили такси и автобусы, преподавали в школе, лечили в больницах (и, подавно, вне больниц).
При этом все (хорошо, почти все) работаю на эту самую блондинку. Медики придумывают безопасные и действующие лекарства, повара новые блюда, автоконцерны придумываю новые коробки-автоматы и беспилотные автомобили. И даже программисты стараются изо всех сил (хоть и плюются с «тупых» юзеров) ровно для нее. При этом и прикладники (условно — кнопочки красивые и на своих местах) и системщики (система отзывчивая и «не глючит»).
А теперь простой вопрос. После всего сказанного Вы будете продолжать настаивать на праве блондинки не просто иметь свою мнение по любому вопросу (включая управление атомным реактором), а именно на праве активно учавствовать в работах и принятии решения? По мне такой мир будет просто безумно страшным. Пусть блондинка решает давать парацетамол или грипферон (или вообще ничего не давать). Пускай она отвечает за выбор блюд на ужин и постинг фоточек в социальные сети. Это те решения, которые ей ясно и недвусмысленно делегированы. Но ни на шаг больше.
Так что я не готов согласиться с тем, что «Жизнь постоянно переносить элитарность в утилитарность». Нет, она просто постоянно расширяет границы утилитарности. Вселенная расширяется, энтропия возрастает…
А цитата хороша. Впрочем… Вреден я. Ведь и здесь не соглашусь. Выдавать глобальные идеи — это политика, популизм, научная фантастика или что-то им близкое. А вот реализовывать глобальные идеи, получая удовольствие от устранения мелких сволочных ошибок — это уже работа мечты. К счастью у меня такая вот уже 20 лет как есть. И, надеюсь, еще хотя бы 25 продержаться.
slovak
Странно, что производители чипов стали виновны в низком уровне разработчиков с Вашей точки зрения. А не образовательная система, например.
Каким образом, я так и не понял из Вашего сообщения. Вы жаловались на плохую документацию в прошлом, но, возможно именно эти трудности и сделали Вас специалистом.
MinimumLaw
Давайте так. Научить программиста в ВУЗе невозможно. Сколько б минусов я не словил за столь категоричное высказывание. Любого программиста делает в первую очередь опыт практической работы. Я не говорю что базовые знания бесполезны. Нет. Но я утверждаю — выпускник ВУЗа это полуфабрикат. Его еще готовить и готовить. При чем в любой отрасли. Невозможно подготовить специалиста по базам данных. По веб-технологиям. По ИИ. Ну и токовый специалист по встраиваемым системам это плод длительной работы. Сначала с наставником, потом самостоятельной, потом обучения. Да, умение передавать знание не менее важно, чем умение получать. Рассказывая, объясняя и обосновывая почему так сам глубже понимаешь вопрос и пересматриваешь свои подходы. Возможно, я не прав. Но мне кажется что это касается вообще любой сферы. Учителя, врачи, токари, слесари, плиточники, монтажники…
Теперь чем же мне не нравится ST Microelectronics. Да всем нравится. Кроме одного. Она выдает микроскопы и не объясняет как ими пользоваться. В итоге в неподготовленных руках они безоговорочно превращаются в молотки. И ладно б так. Почему б и не забить саморез микроскопом, раз микроскоп доступен и саморезов навалом? Но беда в том, что люди заколотившие саморез становятся абсолютно уверенны в том, что только так и можно. Более того — как все неофиты они искренне и с достойную лучшего применения рвением пытаются доказать всему оставшемуся миру свою правоту. Вот именно за это я и не люблю ST Microelectronics. Atmel (Microchip), Sharp, Maxim, NXP — да фактически все уделяют внимание в первую очередь документации и базовой поддержки средствами разработки. Справедливо полагая что код напишут знающие люди (и напишут так хорошо, как только смогут). STM же выдает странный полуфабрикат. Который превратится в едва съедобное в руках неопытного разработчика, и во что-то более или менее похожее на правильный результат в руках опытного специалиста.
Я поправлюсь. Я видел очень классный код работающий на STM. Но беда в том, что таких проектов подавляющее меньшинство. Нужна определенная смелость для того чтобы все же пробраться через огрехи документации и доделать нижний слой. К счастью на хабре такие присутствуют. Чтоб ненароком кого не обидеть не стану называть никого. Но видел я и такие статьи. И скажу честно — слегка завидовал. Меня на такое не хватило. Впрочем, там в основном плюсы были. А это несколько не мой профиль.
Mike_soft
Ардуина — вряд ли. там тупокод цветет и пахнет.
а контроллеры вообще- да, хотя сейчас «историю одного байта» повторять никто не будет — проще взять контроллер на несколько центов дороже, с вдвое большей памятью.
kAIST
В «история одного байта» все же условия были несколько экстримальные: есть МК который утвержден и под него уже все готово и нужно было впихнуть фичу, про которую заказчик забыл.
quwy
Подождите немного. Даже на хабре полно статей о том, как сделать очередную мигалку светодиодом на контроллере, с тегами «python» или «javascript». Эту чуму уже не остановить, персоналки пали под натиском скриптового безумия, контроллеры — на очереди.
Fox_exe
И это печально… Теперь найти интересную работу, где надо напрягать мозги, стало крайне проблематично.
MinimumLaw
… и опять я со своими уточнениями. Работы такой навалом. Проблема не в работе. Проблема в том, что оплачивать ее адекватно не хотят. Как правило бюджет съеден на этапе обрастания фичами. Но увы, это практически невозможно исправить.
Hardcoin
В большинстве случаев конечный пользователь не хочет платить на скорость, он хочет платить за функционал. Так что это не проблема, просто IT даёт то, на что есть спрос.
Представьте, что вы хотели бы вкусную еду, а вам в ресторане подавали бы только полезную, не разрешая заказать то, что вам хочется. Можно сколько угодно агитировать ресторан так делать, но он не сделает так никогда.
Так же и с софтом. Только тот софт быстрый, пользователи которого требуют и платят за эту скорость (не считая проектов, живущих на энтузиазме).
MinimumLaw
Я за то, чтоб был McDonald's. Но я категорически против того, чтоб был ТОЛЬКО McDonald's. Я хочу хоть иногда отдохнуть в Метрополе. И сильно расстроюсь если закроют сильно любимые Питерские кафехи Север. Больше того — суши бары, азиатская кухня, и все остальное тоже вкусно и интересно.
Но времена складывается ощущение что я единственный кому интересно хоть что-то кроме Макдака. А вот это уже страшно.
ramzes2
Можно, например, пойти работать в NASA и попробовать добавить новую фичу в прошивку Вояджеров.
kAIST
Да оставьте вы эти статьи в покое — они для людей для которых это просто хобби. В то же время есть и статьи для «хардкорщиков». Но первых гораздо больше, вот и статей таких появляется больше.
Ну вот я «мигал светодиодом» недавно на esp32. Это был единичный и практически одноразовый девайс. Я взял платку, которая была под рукой, спаял пяток проводов своей криворукой пайкой, набросал пару десятков кода в ardiuno ide. На все про все ушло пол часа времени. Зачем мне лезть в дебри, если это для меня лишь хобби. Я отдыхаю а не работаю (хотя этот девайс мне нужен был для работы, но повторюсь, он был не серийный и одноразовый).
Iwanowsky
Помню времена, когда для ускорения работы кода в программу вставлял многочисленные ассемблерные вставки (в т.ч. для отрисовки на экране, вывода графических файлов, обмена с внешними устройствами и пр., чтобы экономить процессорные ресурсы), да еще старался сильно оптимизировать программный код (чтобы уложиться в объем небольшого ОЗУ). А сейчас — все увеличивающиеся мощности компьютеров, ОЗУ и дисковое пространство, ООП, визуальное программирование, фреймворки и пр. попросту убивают искусство программирования; и современные программы реально пожирают ресурсы компьютеров.
Alexey2005
Искусство программирования убито кроссплатформенностью. Теперь нужно либо для каждой из платформ придумывать собственный набор трюков и хаков, фактически написав одну и ту же программу столько раз, сколько она поддерживает платформ. Либо же использовать фреймворки и создавать как можно более предсказуемый код, чтоб он работал везде.
И ситуация изменится не раньше, чем рынок полностью монополизируется единственной платформой. В этом плане есть некоторая надежда на интенсивное развитие WebAssembly и сдыхание всех альтернативных браузеров в пользу одного-единственного.
То есть, конечно, если такое случится, то негативных эффектов будет масса. Но вот возможности оптимизации возрастут колоссально, вплоть до написания байт-кода вручную.
MinimumLaw
Это не совсем так. Есть множество хаков мало зависящих от конкретного процессора или системы команд. Хотя, конечно, часть правды в этом есть. Но в коце-концов тот же Unix придумывался в первую очередь ради переносимости. Но это не мешает оптимизировать и ядро (на конкретной железке) и прикладное ПО (как под железку, так и кроссплатформенно).
quwy
Вот только некому будет писать.
MinimumLaw
Ooopss… Похоже ошибся уровнем. Извиняюсь.
А Вы современный программист? Вот только честно и для себя.
Я когда-то решил, что мне не интересно прикладное программирование. Вот совсем. И решил остаться на самом низком уровне. Пусть и менее востребовано, пусть и денег здесь меньше — за то я четко знаю как каждый оператор моей программы влияет на бег электронов внутри изделия. И на этом уровне все осталось как раньше. Те самые бешенные хотелки в процессе реализации выжимают все даже из таких монстров как STM32, а неуемные аппетиты прикладного ПО не дают расслабиться и на уровне ядер операционной системы. И загрузку быструю все хотят — загрузчик неизбежное зло, а потому должен быть максимально эффективным.
Другое дело, что мало кто желает идти этим путем. Было бы интересно добавить в эту (и ей подобные статьи) опрос — кто насколько понял написанное. Думаю при честных ответах едва ли половину осилили. Букв много. Буквы такие, которые требуют знания «внутреннего мира» процессора. Хотя по сути просто описана давно известная оптимизация.
А искусство убить невозможно. Оно так или иначе неистребимо. Другое дело, что в живописи тоже были свои эпохи. Импрессионисты не примут Авангард, но и те и другие будут посланы далеко в лес художниками эпохи Возрождения. Так что главное выбрать путь и идти по нему. По возможности не навязывая окружающим своего мнения. Временами даже полезно узнать, что там нового в соседском лагере (и где мы это уже видели).
CodeName33
Помню, как-то переписывал все свои ассемблерные вставки обратно на С++, т.к:
1. Код не компилировался на другую архитектуру.
2. Когда я писал этот код, он и правда был быстрее того, что генерил компилятор того времени для тех процессоров. Но с тех пор сменилось уже не одно поколение процессоров и версий компиляторов, и современный компилятор уже давно генерил для новых процессоров более быстрый код, в чем я убедился переписав его на С.
MinimumLaw
Ассемблер — это не ядерная бомба. Это скальпель. А у хирургического вмешательства всегда есть единственное показание — без него жизнь по угрозой. Потому если без него МОЖНО обойтись, то без него НУЖНО обойтись.
А вопрос написания кроссплатформенного кода сильно шире ассемблерных вставок. В общем случае наиболее справедливый ответ выглядит как «вы просто не умеете их готовить». Впрочем — еще раз повторюсь — это всегда хирургия. И великое искусство всегда ровно в том, чтоб с одной стороны не впадать в гомеопатию, с другой не скатываться до хирургии. И уж подавно не доводить до необходимости хирургического лечения избыточным увлечением гомеопатией.
klirichek
Даже на встраиваемых 1МГц нынче встретишь нечасто. И уж тем более нечасто там бывают задачи о плавной отрисовке тайлов в реалтайме. Поэтому запилить туда python и запульнуть скрипт на сотню строчек, который опрашивает данные очередной "погодной станции" — норм. А читать посты о том, как кто-то запустил эмулятор ZX-Spectrum на двухдолларовом программаторе-"свистке" — уже вызывает "аах!"
klirichek
Именно что полезно уметь и держать в арсенале. А заодно — знать, как оно там под капотом шевелится. От встраиваемых систем недалеко (качественно) до какого-нибудь core-i9; покуда там больше "количества".
Ардуина хороша высокоуровневостью и и предлагаемым другим подходом: чтобы просто помигать светодиодом пару раз в секунду не нужно лезть в ассемблер!
(если задача состоит именно в этом — скрипт-ардуидди парой строчек, скопипащенных с какого-нибудь форума/stackoverflow решит такую задачу гораздо быстрее гуру ассемблера). Но досадно, что дальше, порой, дело не идёт. И причина этому — сама ардуина. Она, во-первых, слишком быстрая, чтобы заметить недостатки высокоуровневых языков. Во-вторых, в её родной экосистеме программируется всё же на Си, который компилируется, и тем самым оставляет место по сути лишь для безумных оптимизаций (покуда все разумные уже сделал компилятор).
В этом плане тот же ZX Spectrum гораздо больше провоцирует к исследованиям: сам как ардуино (запустил — и вот тебе бейсик!), но при этом более-менее серьёзные программы на бейсике с какой-то числодробительной математикой уже ЗАМЕТНО тормозят, и при этом под рукой куча программ, написанных на некоих "машинных кодах", которые те же задачи выполняют плавно и быстро. Оп-па, челлендж! Надо разобраться!
Да и встроенное руководство их упоминает, и даже приводит пример простейшей программы, причём в НУЖНОМ месте — в самом конце, когда бейсик уже освоен
хм… непонятные буковки… Интересно!
titovmaxim
На том же Z80 (ZX-Spectrum, к примеру) двух стековых регистров не было. Но никто не мешал, скажем, читать через POP из памяти, а писать в фиксированный адрес ячейки, что бы без всяких ++. Да, процедуру нужно было разворачивать «руками» для каждой ячейки экрана. Благо, что их было не так много. Зато максимально быстро, на Z80 быстрее сделать было нельзя.
klirichek
На спектруме в целом всё более "теплично".
Возьмём условно частоту 7Мгц и экран размером 7Кб (так легче прикидывать). Это сразу даёт возможность возиться над каждым байтом по 1000 тактов, чтобы обновлять картинку раз в секунду. Делим на "жёсткий реалтайм" того времени (25 кадров/сек) — 40 тактов. Берём реальную частоту (половина от 7МГц) — 20 тактов. И если учесть, что экран всё же 6,75К, и цветовые байты на каждый чих обновлять не надо — даже чуть больше. Так что даже в этих условиях вроде всё получается без всяких безумных оптимизаций (тот же LDIR насколько помню кушал 17 тактов на байт). Плюс, никто не думал о простое процессора (а сколько он кушает в HALT до следующего прерывания, по сравнению с тем, если мы вместо этого будем что-то считать). Ну и 25fps — тоже жёстко, да и цвета всё равно будут прыгать по 8 строчек и портить визуальное впечатление. А раз идеала всё равно не получится — можно и тут понизить и освободить ресурсы для логики.
Stanislavvv
Насчёт оптимизаций — LDIR был медленнее кучи LDI. А обновить все 6.75кб экранной памяти между прерываниями от кадровой развёртки (50к/с на телевизоре и, соответственно, спектруме) не хватало времени даже при помощи LDI, так что приходилось идти на ухищрения типа обновления только части экрана, занятой активными спрайтами и, соответственно, полностью статичным фоном в игре (Dizzy) или использования не всего экрана, а к примеру, верхних двух третей (Elite) или даже средней трети (демоверсия чего-то похожего на Doom). Ну и особенности адресации видеопамяти тоже доставляли, но к этому уже можно было привыкнуть и использовать для относительно быстрого вывода спрайтов с точностью до знакоместа.
klirichek
50fps там никто особо не ждал (и реально заметных глазу поводов стремиться к этому не было). Да и прерывания на два-три тика можно просто запретить. Или заменить обработчик своим собственным, легковесным (чтобы пошустрее всё проверил и вернулся). Но в целом да, сделать половину/две трети экрана чёрными и там не рисовать вообще — тоже вариант. Но мне кажется, это уже шаги в сторону Agile. Сделать хоть как-то быстрее, но на пол-экрана, и уже завоёвывать рынок. Против "сделать плавно и гладко на весь экран, но + пол-года разработки безумных оптимизаций"
thealfest
LDIR — это 21 такт на байт, а через стек можно почти вдвое быстрее.
shiru8bit
На ZX не 7 МГц, а вдвое меньше, и LDIR 21 такт на байт. И в игровом рендере нельзя обойтись просто линейным копированием байт, одна только обвязка кадра может занять сравнимое с копированием врем, не говоря про наложение спрайтов по маске.
klirichek
Про вдвое меньше я прямо в комментарии и написал, просто вы не дочитали.
И цель была, в общем-то, просто прикинуть рамки выше которых точно не прыгнуть.
shiru8bit
Трудно построить реалистичные прикидки на изначально очень неточных данных. Это довольно странная идея, сначала завысить-занизить параметры, потом как-то поделить. Поэтому у вас получилось, что легко получить хороший фреймрейт без креативных оптимизаций. Но в реальности это было далеко не так. В реальности была медленная память, снег, порча стека, пропуск прерываний, ради 25 FPS был код типа pop de:ld (hl),e:inc l:ld(hl),d:dec l, т.е. чтение стеком и вывод змейкой с оптимизацией перехода 256-байтных границ (~16 тактов на байт), и многое другое.
Про подобное определение рамок вспоминаются статьи из 90-х о невозможности полноэкранной прокрутки текста в 50 FPS, там тоже считали примерно так же. Но фактически она таки возможна, что и было многократно реализовано чуть позже.
NedoPC
Особенно весело было в Ghosts'n'Goblins под ZX Spectrum 48K:
loop:
ld bc,7
add hl,bc
ld sp,hl
add hl,bc
pop de
pop bc; вот этими словами будем рисовать одну строку
ld sp,hl
exx
pop de
pop bc; а вот этими соседнюю
ld sp,hl; вот сюда будем рисовать строку
push bc/de *14; некая комбинация слов (например, небо и потом текстура земли)
inc h
ld sp,hl; а вот сюда соседнюю
inc h
exx
push bc/de *14; аналогично
dec a
jr nz,loop
С вариациями (там несколько таких циклов).
И дизайн игры подчиняется этому техническому решению, чтобы было быстро.
thealfest
На спектруме часто заполняли все регистровые пары из стека, потом меняли указатель стека на буфер экрана и pushили их туда. Для этого даже использовали второй набор регистров, чтобы реже менять указатель стека.
mvaspb
На моей памяти одна демка на спектруме устанавливала адрес стека на начало экранной памяти 16384 и пушила туда картинку. Это позволяло получить чуть-ли не полноценную анимацию… Вспомнил название — «Lyra 128»
www.youtube.com/watch?v=EpQ94LmXaSQ&lc=Ugi2Y8idS0jnhngCoAEC
Меня тогда удивила как быстро она работает и я полез поковырять ассемблерный код.
hddmasters
Ошибочка. При добавлении чего-либо в стек изменение регистра SP (указателя на вершину стека) происходит в сторону уменьшения.
В случае ZX Spectrum, если требовался вывод только пикселов без изменения атрибутов, то указывалось на #5800 (начало области атрибутов) и далее PUSH'илось на экран до #4000.
mvaspb
Точно, вы правы. Уже забыл детали архитектуры. Но простительно, уже 25 лет не трогал спектрум :)
napa3um
Вы, очевидно, просто не представляете, сколько оптимизаций в движке JS V8, например. Все эти ваши разворачивания циклов или вынос переменной в регистр прост покурят рядышком, не отсвечивая.
ramzes2
А что толку с этих оптимизаций, если куча веб-программистов на форумах спрашивают, нужны ли им знания алгоритмов?
VM390
Это не оптимизация, это бит-жонглирование…