Оберон-клуб «ВЄДАsoft» https://zx.oberon.org/forum/ |
|
Модификация Ofront. Реализуем "правильный" FOR https://zx.oberon.org/forum/viewtopic.php?f=32&t=105 |
Страница 1 из 8 |
Автор: | Saferoll [ 22 апр 2013, 20:30 ] |
Заголовок сообщения: | Модификация Ofront. Реализуем "правильный" FOR |
На основе теоретических изысканий попробуем заставить транслятор Ofront генерировать "незацикливающийся FOR". Сразу оговорюсь, что я не являюсь большим специалистом по этому транслятору, поэтому сделанные мной исправления скорее всего несовершенны. Буду рад, если кто-то более сведущий меня поправит. Как уже заметили, Ofront построен на основе "OP2: A PORTABLE OBERON–2 COMPILER", так что перед выполнением каких-либо модификаций полезно перечитать документ по вышеприведенной ссылке. Общий принцип работы Ofront: парсер ОРР читает при помощи сканера OPS исходный Оберон-текст и строит синтаксическое дерево. OPV проходит по этому дереву и генерирует С-код, вызывая нужные процедуры генерации из OPC и OPM. Что именно мы должны здесь исправить? Вообще модификация «FOR» возможна тремя способами:
2) Исправить OPV, чтобы для FOR генерировался такой код на С, какой нам надо. 3) Добавить модификацию дерева между OPP и OPV. Способ 2 подойдет, если OPP вставляет в дерево уникальный код для FOR и мы сможем потом понять, что это именно конструкция FOR. Поэтому сперва поищем в OPP построение дерева для оператора FOR. После недолгих поисков находим в процедуре OPP.StatSeq нужный кусок кода. Я не вник во все тонкости, но добавлю комментарии о том, что удалось понять (в исходниках Ofront комментариев мало, а русских комментариев и вовсе нет, так что все такие комментарии - мои). Код: "OBERON"
Однако заставить ОРР генерировать такой, или иной подобный фрагмент из этой ветки, слишком сложно, уж очень они непохожи на WHILE n<=B DO. На данном этапе освоения Ofront нужно попробовать что-то менее сложное, нужно реализовать фрагмент кода более похожий на исходный. От генерируемого ОРР он отличается только дополнительной строчкой Код: "OBERON"
Но как обнаружить переполнение? Мог бы помочь контроль флагов процессора, но ни из Оберона, ни из С к ним нет прямого доступа. Использовать ассемблерные вставки? Можно, но 1) это тоже сложно, нужно разбираться , 2)появляется платформозависимость, которой нет на чистом С. Поэтому флаги пока отложим. Будем проверять не после инкремента, а до — «если следующий инкремент даст переполнение, то выйти из цикла», как здесь. На С это будет примерно так: Код: "C" if (n>n+Step) break; (для Step>0) Для этого нужно уметь вставлять в дерево «операции»: IF, сложение, сравнения и break. Можно посмотреть, конечно, как строить дерево для IF (это чуть выше в OPP.StatSeq) , потом поискать прочие операции, но можно пойти более простым путем. Будем считать всю строчку if (n>n+Step) break; одной специальной процедурой breakfor(n,Step). Вернее введем две «спецоперации»: Код: "C" breakforright(n,Step): if (n>n+Step) break; (для Step>0) |
Автор: | Saferoll [ 24 апр 2013, 15:29 ] |
Заголовок сообщения: | Re: Модификация Ofront. Реализуем "правильный" FOR |
Итак, мы решили создать специальные процедуры breakforright(n,Step) и breakforleft(n,Step), чтобы в зависимости от знака Step вставить в дерево вызов одной из них, как это сделано для инкремента OPB.StPar1(y, z, incfn). Вот и посмотрим сейчас эту StPar1: Код: "OBERON"
Код: "OBERON"
Код: "OBERON"
Код: "OBERON"
Код: "OBERON"
Перед обработкой самого тела цикла ( CheckSym(do); StatSeq(s);) имеем Код: "OBERON"
Код: "OBERON"
Код: "OBERON"
Код: "OBERON"
Код: "OBERON"
|
Автор: | Saferoll [ 24 апр 2013, 15:45 ] |
Заголовок сообщения: | Re: Модификация Ofront. Реализуем "правильный" FOR |
Перейдем теперь в модуль ОРВ. Мы уже вставили в начало этого модуля константы breakforright, breakforleft . Теперь займемся их обработкой в PROCEDURE StPar1. Создадим копию ветви CASE для incfn, decfn, заменив эти константы на breakforright, breakforleft. После этого изменим контроль типов, а в остальном ничего не изменится: Код: "OBERON"
Можно даже попробовать откомпилировать и запустить модифицированный Ofront. Вставлю здесь замечание для новичков о работе с модулями BlackBox. Если вы в данном сеансе работы с BB уже пробовали запускать Ofront, то ввести в действие новый откомпилированный вариант OPP \OPB одним нажатием Ctrl-F12 не получится — получится «unloading OfrontOPP failed». Это происходит потому, что модули BlackBox взаимосвязаны: нельзя выгрузить модуль, который используется (импортируется) каким-то другим загруженным в данный момент модулем. Нужно или выйти из BlackBox и запустить его снова или выгрузить и модули Ofront и все их использующие. Для этого открываем список модулей (Info\Loaded Modules), выделяем в этом окне все модули OfrontXXX и находящийся выше XdevCmd, а затем всё выделенное выгружаем командой Dev\Unload Module List. Вот после этого Оfront для трансляции будет использовать уже модифицированные модули (если вы не забыли их откомпилировать Ctrl-K или Ctrl-F12). Попробуем так сделать, чтобы оттранслировать, например, ZXDev.Unsigned. Получим Код: "OBERON"
Код: "OBERON"
Но сейчас мы уже в нужном месте, просто продублируем ветку для инкремента\декремента, но заменим incfn, decf на новые ключевые слова: Код: "OBERON"
Для тестирования внесем еще небольшое исправление — заменим «n^.subcl = decfn» на «n^.subcl = incfn». Больше ничего пока не правим, а попробуем выгрузить модули Ofront, перекомпилировать OPV и снова запустить трансляцию для ZXDev.Unsigned. Получим для нашего цикла конструкцию WHILE с оператором декремента перед инкрементом. Конечно такой цикл сразу зациклится, но не это важно, важно, что мы на верном пути! |
Автор: | Saferoll [ 24 апр 2013, 15:58 ] |
Заголовок сообщения: | Re: Модификация Ofront. Реализуем "правильный" FOR |
Генерация кода для инкремента\декремента состоит из 3-х операций: Код: "OBERON"
Код: "C" breakforright(n,Step): if (n>n+Step) break; (для Step>0) Процедура OPC.Increment устроена довольно просто Код: "OBERON"
Код: "OBERON"
Код: "OBERON"
Код: "C" Unsigned_byte = 32760; Код: "OBERON"
Код: "C" Unsigned_byte = -32760; Cтандартный тип BYTE нельзя использовать в качестве параметра цикла. Для типа SHORTINT код цикла на С конечно же не изменится, но при выполнении FOR byte := 100 TO MAX(SHORTINT) DO происходит зацикливание! Так же как для FOR byte := -120 TO MIN(SHORTINT) BY -1. Для SYSTEM.SHORTCARD тоже зацикливание, хотя предохранительная строчка с break исправно вставилась. Видимо у Си и\или у SDCC свои понятия о приведении операндов сложения и сравнения. Будем выяснять это в следующий раз. |
Автор: | Zorko [ 24 апр 2013, 23:39 ] |
Заголовок сообщения: | Re: Модификация Ofront. Реализуем "правильный" FOR |
Уважаемый Олег! Очень тотально ты приступил к делу, так что желаю тебе успехов! ![]() Поскольку цикл FOR — это настоящая рабочая лошадка, а поэтому действительно заманчиво иметь очень эффективную реализацию, то не помешает рассмотреть ещё и такой вариант. По-моему, он хорошо ложится на внутреннюю структуру Ofront'а. Итак, Ofront заводит внутреннюю переменную цикла _for__x, где x — некоторое число. Переменная нужна для того, чтобы вычислить B только единожды и хранить в ней. Макет типичного цикла: Код: "C" { Вычислить количество повторений цикла. Если A и B — константы, то в compile-time. Если чего-то из них не константа, то вычисляем кол-во повторений в присваивании переменной-счётчику _for__x. Если цикл не повторяется ни разу или же повторяется один раз, учесть (можно отдать этот момент на откуп сишному компилеру). Далее сгенерировать следующий код: Код: "C" n = начальное значение цикла; Код: "ASM" LD HL, _for__x Именно из-за этой конструкции я не предлагаю генерировать, например, это: Код: "C" n = начальное значение цикла; Но наверное второй вариант следует рассмотреть тоже. Плюсы: условные и безусловные переходы дороги. Они имеют значительно более низкую эффективность даже в современных x86-64 — из-за ограниченного объёма кэша команд и возможного их распараллеливания рекомендуется применять линейные последовательности вычислений с минимумом переходов. Сложение не так дорого, но операции сравнения с нулём — очень дешёвы. В Z80 сравнение двух двухбайтовых чисел, со знаком или без, это целая эпопея с вычитанием (или сложением) регистровых пар и ёмкой засылкой значений из памяти в память. Если счётчик будет типа байт, сравнение его с нулём — одна из самых быстрых операций, которую можно проделать даже прямо в памяти, адресуя переменную через пару HL или индексные регистры, экономя использование других регистров. Если на нуль сравнивается регистровая пара — тоже не больно страшно, всего лишь загрузить регистр младшим значением и сделать OR со старшим. Вобщем, получается хорошо. Что смущает. Ёмкость вычисления количества повторений цикла не в compile-time. Это как минимум взятие по модулю, но лучше всё-таки сделать такое вычисление один раз в начале цикла, чем n раз внутри него проверять на переполнение. Трудности. Тип переменной-счётчика для Z80 оптимальнее всего будет задавать на основании вычисленного количества повторений (если известно точно, что их будет <= 255, берём байт. Если точно неизвестно, берём счётчик такого типа, чтобы хватило значений). Здесь неважно, что тип описывается как знаковый, в таком варианте использования он будет фактически без знака. Ну а вот для Win32 лучше всего будет брать счётчик цикла всегда не меньше, чем INTEGER. И только для длинных циклов LONGINT. Эту проблему надо обдумать. Идея. Можно даже 256 повторов хранить в одном байте, для этого нужно будет только лишь предусмотреть это в начале цикла. Надо чуть видоизменить код. Как — пока не придумал. Оптимизация. Не заводить внутренний счётчик, если его значение будет совпадать с n. Проблема. Предлагаемая реализация цикла нечувствительна к изменению счётчика цикла n внутри цикла. Он всегда будет повторяться единожды заданное при вхождении количество раз. Это может привести к неработоспособности тех программ, которые каждую итерацию цикла полагаются на WHILE n <= (Заранее вычисленное)B. Что, как ни крути, всё-таки жёстко задано в стандарте Оберона-2. На самом деле я не считаю это серьёзной проблемой, потому что нечего жалеть построителей корявых циклов. Ведь если кому-то нужно чистое WHILE n <= B, то пусть так и пишет, и без всякого FOR. Вобщем, такие вот мысли. Если я не сделал в рассуждениях явного ляпа, можно попробовать посчитать какой выигрыш будет для разного размера типов на Спектруме (в тактах), и какой — на Win32. |
Автор: | Saferoll [ 10 май 2013, 19:06 ] |
Заголовок сообщения: | Re: Модификация Ofront. Реализуем "правильный" FOR |
К сожалению дела заставили на некоторое время покинуть этот интересный форум ![]() ![]() Zorko писал(а): Поскольку цикл FOR — это настоящая рабочая лошадка, а поэтому действительно заманчиво иметь очень эффективную реализацию… Согласен, проверять и переполнение и выход за пределы В на каждой итерации неэффективно. Более эффективные реализации цикла FOR (в т.ч. вычисление количества повторений) были рассмотрены ранее. Но на данном этапе мне было важно получить работающую модификацию Ofront, пусть и неэффективную. Было бы трудно делать совсем новую конструкцию, вроде описанных в теоретической ветке. А вот одну доп.строчку вставить удалось. … Вставлять дополнительную проверку на переполнение в тело цикла — это серьёзно замедлить цикл. . Теперь понятно: Ofront можно модифицировать, понятно более-менее что у него за что отвечает и как изменить, чтобы остальное не нарушить. Удалось успешно модифицировать - уже дорога проложена. Пусть сама реализованная конструкция цикла не слишком удачна, зато удачно прошла модификация Ofront. А вот дальше можно попробовать другую, более оптимальную конструкцию для цикла реализовать. Цитата: Вычислить количество повторений цикла. Что же, вычисление количества повторений – интересная реализация FOR, но во всех ли случаях она эффективна? Ведь инкремент переменной цикла мы должны делать по-прежнему. Зато теперь надо делать декремент вспомогательной переменной (на 1, но ведь раньше и вовсе не делали) и вычислять количество повторений (однократно, но в общем случае используя деление). Выигрываем только в сравнении - сравнение с нулем эффективнее сравнения с произвольным числом в памяти. Это сравнение происходит на всех итерациях, что даст выигрыш, если итераций много. Что значит «много», нужно еще оценить.… Код: "C" n = начальное значение цикла; В каких пределах может быть количество повторений? Минимальное число повторений =0 (ни разу), максимальное будет при единичном шаге и равно MAX(T)-MIN(T)+1. Даже для беззнаковых чисел (MIN(T)=0) это значение в диапазон не влезает (MAX(T)-0+1> MAX(T)), оно на 1 больше. Как же выходит из положения в таком случаем, например, компилятор Дельфи? А он допускает переполнение и считает, что MAX(T)+1 это 0. И действительно, n:=0; REPEAT …DEC(n) UNTIL n=0; выполняется ровно MAX(T)+1 раз. Но зато случай «ни разу» (n= «настоящему нулю») нужно обрабатывать отдельно, поэтому if (_for__x) в общем случае не пойдет, нужно вместо этого написать if (A<=B) …(или if (n<=B)…). Цитата: Идея. Можно даже 256 повторов хранить в одном байте, для этого нужно будет только лишь предусмотреть это в начале цикла. Надо чуть видоизменить код. Как — пока не придумал. Видоизменить именно так, как написал выше (256=0).Цитата: Как мы понимаем, в первом варианте на каждую итерацию цикла переход всего один [...while (_for__x)]. Во втором же варианте их два [1) начало while (_for__x--); 2) конец while (_for__x--)], причём первый переход хоть и исполняется всего раз в конце цикла — неисполнение условия всё равно приводит к потере машинного времени, хоть и незначительно, но всё же снижая скорость работы цикла. Но наверное второй вариант следует рассмотреть тоже. Да, REPEAT UNTIL в машинных кодах реализуется эффективнее, чем WHILE. Если конечно умный компилятор не заменяет автоматически WHILE P DO S; на GOTO label; REPEAT S; label: UNTIL ~P;Цитата: Что смущает. Ёмкость вычисления количества повторений цикла не в compile-time. Это как минимум взятие по модулю, но лучше всё-таки сделать такое вычисление один раз в начале цикла, чем n раз внутри него проверять на переполнение. Небольшое уточнение: не взятие по модулю, а «деление нацело».NumIters=(B-A) DIV Step + 1, при Step>0, A<=B. В общем случае (если А,В – не константы, шаг не единичный и не степень двойки), операция деления для Z80 трудоемка. Типичная реализация деления для Z80 содержит цикл со сдвигами и сравнениями, но этот цикл выполняется столько раз, какова разрядность данных.Больше ли это, чем инкремент и сравнение с произвольным числом в цикле, который выполняется столько раз, чему равно частное? Видимо зависит от разрядности и частного. При небольшом шаге (что означает большое частное )деление эффективней, при большом - наоборот. Шаг задается константой, поэтому можно это оценить заранее, еще на этапе компиляции. Цитата: Трудности. Тип переменной-счётчика для Z80 оптимальнее всего будет задавать на основании вычисленного количества повторений (если известно точно, что их будет <= 255, берём байт. Если точно неизвестно, берём счётчик такого типа, чтобы хватило значений). Здесь неважно, что тип описывается как знаковый, в таком варианте использования он будет фактически без знака. Верхнюю границу количества повторений можно оценить на этапе компиляции: Цитата: Если А не константа, берем А=MIN(T). Если получилось NumIters<=256, то можно использовать байтовый счетчик. Если В не константа, берем В=MAX(T). Вычисляем NumIters=(B-A) DIV Step + 1 Цитата: Оптимизация. Не заводить внутренний счётчик, если его значение будет совпадать с n. Уточню. «Совпадать» означает единичный шаг ( ABS(Step)=1) и В=…Чему? Чему-то, с чем можно эффективно сравнить. Например, Step=-1,В=1, тогда сравнивать после декремента придется с нулем, что эффективно. Или для беззнаковых чисел Step=1,В=MAX(T), что тоже даст сравнение с нулем. Кроме флага Z, у процессора Z80 есть еще V, что дает возможность эффективно использовать Step=1,В= 127 (Step=-1,В= 128). Для знаковых можно еще применить флаг S, что позволит эффективно реализовать случай Step=-1,В= 0. Жаль, что CY не меняется от инкремента\декремента. Но это флаги процессора, а мы пока транслируем в С. Эффективно ли пройдет трансляция С->ассемблер? Цитата: Вобщем, такие вот мысли. Если я не сделал в рассуждениях явного ляпа, можно попробовать посчитать какой выигрыш будет для разного размера типов на Спектруме (в тактах), и какой — на Win32 Олег, спасибо за мысли. Думаю, на практике чаще будет встречаться единичный шаг (деление совсем не нужно) или шаг равный степени 2 (деление сводится к сдвигу). Ну и чаще наверно будет встречаться небольшой шаг, чем большой, т.е. число итераций будет велико, а значит выигрыш от быстрого сравнения тоже будет велик. Так что, эта реализация в большинстве случаев будет эффективной.Вот только жаль, что уж очень сильно этот способ реализации отличается от стандартной схемы FOR. Значит придется очень сильно модифицировать Ofront, для чего придется получше его изучить. |
Автор: | Saferoll [ 12 май 2013, 16:27 ] |
Заголовок сообщения: | Re: Модификация Ofront. Реализуем "правильный" FOR |
Удалось модифицировать Ofront для реализации FOR через вычисление количества повторений. Пока реализовал черновой вариант, без всяких оптимизаций и учета особых случаев. Но вариант вполне рабочий, даже оптимальней чем я ожидал. Вернее в чем-то оптимальней, а в чем-то наоборот ![]() Код: "OBERON"
Код: "C" /* Ofront 1.2 -xtspkae */ Код: "OBERON"
А вот сравнение (!((int)Unsigned__for__1 == 0)) оставляет желать лучшего ![]() Код: "OBERON"
Надеюсь, скоро выделю время, чтобы описать эту модификацию Ofront более подробно (хотя, наверно, и не так подробно как в первых постах этой ветки). |
Автор: | Zorko [ 12 май 2013, 23:05 ] |
Заголовок сообщения: | Re: Модификация Ofront. Реализуем "правильный" FOR |
Молоток! Прекрасная работа, получены интересные результаты. Saferoll писал(а): А вот сравнение (!((int)Unsigned__for__1 == 0)) оставляет желать лучшего Я уверен, что на Си можно действовать и в рамках других типов. Например, пишем: short int a = 120 + 120 (знаковый байт) и вполне закономерно огребаем переполнение, и без предупреждения со стороны компилятора. КП приводит все короткие вычисления к типу INTEGER, и наверное это разумно (чтобы потом присвоить короткому — надо указать явное SHORT()). А вот Оберон и Оберон-2, по-моему, имеют стратегию такую же, как и в Си. Судя по Ofront'у и OPCL. ![]() ... Причина видимо в том, что по стандарту Си байтовые величины должны сначала приводится к int, с чем уже столкнулся разработчик LLVM Backend для Z80. Регулировать это сложно, потому что Ofront об этом похоже не заботится (а о чем заботится, если это стандарт?), а беззнаковые числа в ZXDev пока "прикручены скотчем сверху". Но с этим что-то нужно делать. Ведь наша цель заключается не в соблюдении стандартов Си любой ценой, программа на Си — лишь промежуточный этап при создании программ на Обероне. ![]() Ну а почему Ofront кастует? Когда сравниваем n типа SHORTINT с константой — у него хватает ума проверить и понять, что константа влазит в тип SHORTINT. А вот когда тип "неизвестен" (прикручен скотчем, как ты верно заметил), то логично перед сравнением преобразовать его в базовый целый тип (int). Ведь даже Си, если я не ошибаюсь, не даст без ворчания (warning) сравнивать знаковые и беззнаковые переменные. Впрочем, надо проверить это. Проверил. Да, не даёт. Код: "C" static void Prog_InitGame (void) Prog.c:29: warning 110: conditional flow changed by optimizer: so said EVELYN the modified DOG Обращаю твоё внимание, что это только варнинги, прога собирается. Однако даже если это будет работать — всё равно как-то не очень радостно получается. Надо покумекать в сторону оптимизации. Уверен, что она хотя бы в некоторых случаях возможна (например, если первое сравниваемое имеет тип SHORTCARD, а второе сравниваемое — константа, то приводить к int не нужно). Кстати, замени тип счётчика цикла в своём примере на SHORTINT (с корректировкой 255 на 127, разумеется) и приятно удивишься более компактному коду. ![]() |
Автор: | Saferoll [ 13 май 2013, 12:10 ] |
Заголовок сообщения: | Re: Модификация Ofront. Реализуем "правильный" FOR |
Как и обещал, привожу более подробные сведения о модификации Ofront для реализации FOR через количество повторений. Т.к. в начале этой ветки был приведен подробный разбор подобной реализации, ограничусь приведением измененного фрагмента модуля OPP Код: "OBERON"
Как видите, изменений гораздо больше, чем в предыдущей реализации. Это и понятно, в предыдущей реализации нужно было вставить всего 1 строчку, здесь же кроме добавления новых операторов необходимо заменить WHILE на REPEAT UNTIL и заключить это в IF (не считая построения сложного выражения для вычисления количества итераций). Но зато изменения необходимо внести только в модуль OPP. Кстати, необходимо изменить еще 1 строку в начале модуля OPP - добавить константу для операции декремента (значение константы подсмотрим в OPB): Код: "OBERON"
В самую первую очередь, я пытался заменить WHILE id<=B DO на IF id<=B THEN, потому что они весьма похожи (вообще, IF - это цикл WHILE который выполняется не более 1 раза). Однако заменить одну строку Код: "OBERON"
Код: "OBERON"
Код: "OBERON"
Много времени я потратил на проблему «Почему иногда меняется константа Step - значение шага, которое я доношу в переменной z почти до самого конца обработки?». Пришлось расставить ASSERT, чтобы найти место «порчи» - OPB.Op(div, y, z). Оказалось, что для деления на степень двойки Ofront заменяет операцию деления на сдвиг, а делитель, соответственно, на величину такого сдвига. Поэтому операции DIV нельзя отдавать саму константу, а нужно создать новую. После замены этой строки на OPB.Op(div, y, OPB.NewIntConst( z^.conval^.intval)); всё стало нормально. Ofront становится понятнее, но остаются темные места. Я до сих пор не понял, что делает OPB.Link(stat, last, x); похоже это как-то связано с присваиванием. Не совсем понятны операторы pos := OPM.errpos и SetPos(x). Понятно, что это создает связь между построенным деревом и исходным текстом Оберон-программы, что позволит потом вставить маркеры ошибок в нужные места. Но в тонкостях этого процесса я пока не разобрался. |
Автор: | Zorko [ 13 май 2013, 18:04 ] |
Заголовок сообщения: | Re: Модификация Ofront. Реализуем "правильный" FOR |
OPM.errpos — это позиция последней найденной ошибки в исходном тексте модуля. Описывается одним числом (когда я модифицировал OPCL для вывода строки/колонки возникшей ошибки — пришлось ввести вместо одного числа, обозначающего позицию, запись со строкой и колонкой. А может были и ещё доп. поля. Деталей уже не помню). |
Страница 1 из 8 | Часовой пояс: UTC + 2 часа |
Powered by phpBB® Forum Software © phpBB Group https://www.phpbb.com/ |