Анализируем FADD из Altair Basic

топ 100 блогов nabbla106.02.2025 Мне тут Анализируем FADD из Altair Basic suvorow_ дал ссылочку на дизассемблированный (и слегка прокомментированный, но именно что слегка) код Altair Basic, который, по легенде, Билл Гейтс и Пол Аллен (и приглашённый Монте Давидофф) написали втроём, не имея на руках вообще компьютера Altair или хотя бы процессора 8080 - только эмулятор на PDP10, а когда приехали на демонстрацию своего продукта - он заработал с первого раза.

Вот он: https://github.com/option8/Altair-BASIC

Там, как оказывается, были полноценные 32-битные числа с плавающей запятой (8 бит экспоненты, 1 бит знака, 23 бита мантиссы) и библиотека для работы с ними, вплоть до тригонометрии, при том что весь интерпретатор бейсика занимал менее 4 кБ. Весь математический код писал Монте Давидофф.

Как всегда, начнём с реализации сложения и вычитания с плавающей точкой, FADD / FSUB. Весь этот код занимает 274 байта, что меньше 320 байт под 8051 (см. раз, два), впрочем, и формат числа чуть проще - тут экспонента 0 заведомо означает ноль (никаких "денормализованных чисел" даже на перспективу), нет Nan или Inf, при переполнении можно просто сделать JMP Error, чтобы интерпретатор грязно выругался и завершил исполнение программы. И размещение знака после экспоненты, похоже, упрощает распаковку и упаковку назад.

Код, похоже, вполне рабочий. Местами он написан даже не на ассемблере, а ещё ниже - там применено "перекрытие инструкций", когда одни и те же байты, в зависимости от того, откуда начинается исполнение, значат разные команды.

Проблему пока вижу только одну - ради экономии пары байт здесь отказались от проверки "несопоставимости" чисел. Если попросить эту штуку сложить два числа, отличающиеся в 2254 раза, меньшее число будет сдвигаться вправо 254 раза подряд, а потом получившийся ноль будет честно прибавлен к мантиссе бОльшего числа. На одно только это сдвигание уйдёт 50 миллисекунд, могло оказаться неприятным сюрпризом, в общем, привычка продуктов Билла Гейтса тормозить непредсказуемым образом была заложена ещё тогда!

Разберёмся, что там под капотом... Англоязычные комментарии - с гитхаба, русскоязычные - мои. Процессор 8080.


Начнём с "рабочего пространства":
016F: 00000000  FACCUM  DB 00000000h    ;
0173: 00        FTEMP   DB 00h  ;


В памяти зарезервировано 32 бита под "аккумулятор" - всегда один из операндов для работы с плавающей точкой. И ещё один временный байт, где будет храниться один-единственный бит, определяющий, "сложить или вычесть".

Теперь глянем "входной" код:
0801: 210B0C    FAddOneHalf     LXI H,ONE_HALF  ;Load BCDE with (float) 0.5.
0804: CD200A    FAddMem CALL FLoadBCDEfromMem   
0807: C31208            JMP FAdd+2      
080A: C1        FSub    POP B   ;Get lhs in BCDE.
080B: D1                POP D   ;
080C: CDFA09            CALL FNegate    ;Negate rhs and slimily
080F: 21                DB 0x21 ;LXI H,....     ;LXI into FAdd + 2.
0810: C1        FAdd    POP B   ;Get lhs in BCDE.
0811: D1                POP D   ;
0812: ;к этому моменту в регистрах B,C,D,E лежит второе число, в H,L - "мусор"


При вызове FAdd или FSub предполагается, что второй операнд (первый всегда FAccum) лежит в стеке. Вообще, я пока не вполне понимаю, как он вообще вызывается - такое ощущение, там нужно сначала "ручками" занести адрес возврата, потом аргументы, а в процедуру прыгнуть через JMP или нечто похожее. И действительно, прямого вызова FADD я нигде не нашёл. Видимо, он вызывается как-то через "виртуальную таблицу".

Также есть вариант FAddMem: тогда в паре регистров (H,L) нужно указать адрес второго числа в памяти, и оттуда оно перемещается непосредственно в B,C,D,E.
И здесь же, ради небольшой экономии, примостилась функция FAddOneHalf - прибавить к аккумулятору число 0,5. Как видно, мы просто заносим в (H,L) адрес этой самой "половинки", а дальше по-накатанному.

Вариант FAdd проще всего понять: все 4 байта "выталкиваются" из стека в регистры B,C,D,E. Да, у intel 8080 PUSH/POP всегда двухбайтовые. В них под B имеется в виду пара (B,C), под D - пара (D,E). А я "с нахрапу" как увидел "BCDE" - подумал, что это Binary-Coded-Decimal чего-нибудь там, а оно вон оно что...

lhs в комментариях это left-hand-side, т.е левый операнд, и rhs - это right-hand-side, т.е правый операнд. Как нам объяснили, в аккумуляторе лежит правый операнд, как это ни странно.

FSub поинтереснее. Сначала, как и ранее, загружаем левое число в B,C,D,E, потом вызывается FNegate, вот он:

09FA: 217101    FNegate LXI H,FACCUM+2  
09FD: 7E                MOV A,M 
09FE: EE80              XRI 0x80        
0A00: 77                MOV M,A 
0A01: C9                RET     


Отсюда мы первый раз узнаём, в каком порядке хранятся байты в аккумуляторе: FACCUM самый младший байт мантиссы, FACCUM+1 - средний, FACCUM+2 - старший, его старший бит выражает знак. И в FACCUM+3 лежит экспонента. Старший байт мантиссы загружаем в A, там XORим ему старший бит и помещаем назад. Логично.

Дальше происходит странная вещь (снова глядим начало работы с FSUB, FADD):
080F: 21                DB 0x21 ;LXI H,....     ;LXI into FAdd + 2.
0810: C1        FAdd    POP B   ;Get lhs in BCDE.
0811: D1                POP D   ;


Этот 0x21 в дизассемблере не указан как команда, хотя управление сюда приходит. В комментарии указано, что 0x21 - это команда LXI H, Imm, команда "загрузить непосредственное (Immediate) значение в регистры H,L". Она 3-байтовая, поэтому "захватывает" и следующие два байта, загружая в H,L значения 0xC1 и 0xD1. Эти значения и нафиг нам не сдались в (H,L), вся хитрость в том, чтобы пропустить "ненавистные" POP B, POP D малой кровью, ведь мы уже извлекли число из стека!

Красиво? На первый взгляд, да. Поначалу я думал, что FNegate поменяет знак у только-только вытащенного из стека числа, тогда всё логично. Но когда оказалось, что FNegate меняет знак в FAccum - это на самом деле оказался костыль. Ведь можно было повременить с извлечением из стека, спокойненько вызвать FNegate, а потом воспользоваться уже имеющимися POP B / POP D из процедуры FAdd. Сэкономили бы ещё 3 байта в дополнение к тому, что здесь. Полагаю, раньше FNegate должен был менять знак именно загруженного числа, потом что-то поменяли в парсинге, сменили FNegate, а этот код трогать не стали, "работает-не лезь". Опять же истоки микрософтовской борьбы с пиратством! Всё вокруг open source, если ты знаешь ассемблер. В те времена все знали ассемблер. Но здесь ассемблера мало, нужно в опкодах мыслить...

Поехали дальше:
0812: 78                MOV A,B ;If lhs==0 then we don't need
0813: B7                ORA A   ;to do anything and can just
0814: C8                RZ      ;exit.
0815: 3A7201            LDA FACCUM+3    ;If rhs==0 then exit via a copy
0818: B7                ORA A   ;of lhs to FACCUM.
0819: CA120A            JZ FLoadFromBCDE        ;


А отсюда мы узнаём, что второе число, сидящее в регистрах B,C,D,E, размещено СТРОГО НАОБОРОТ: в B экспонента, в C знак и старшая мантисса, и так далее. Как интересно: хардверной проблемы Big-endian/little-endian тут ещё быть не могло, т.к процессор 8-битный, но программят его уже ровно так!

В общем, проверяем экспоненту левого числа на ноль (т.е минимально возможное значение). Если так, число заведомо ноль, и мы возвращаемся из процедуры. Да, у Intel 8080 были "условные вызовы и условные возвраты". RZ - оно самое, Return if Zero.

Потом проверяем на ноль экспоненту в правом числе, и если так, то применяем "Tail call". Т.е FLoadFromBCDE это функция, но мы её не вызываем, а прыгаем в неё, поэтому она по завершении вернётся уже не к нам, а туда, откуда вызывали нас, что экономит и место, и такты. Вот эта процедура:

0A12: EB        FLoadFromBCDE   XCHG    
0A13: 226F01            SHLD FACCUM     
0A16: 60                MOV H,B 
0A17: 69                MOV L,C 
0A18: 227101            SHLD FACCUM+2   
0A1B: EB                XCHG    
0A1C: C9                RET


Команда XCHG (exchange) меняет местами пару (H,L) с парой (D,E).
Затем SHLD (store H, L Direct) помещает пару (H,L) по адресу памяти, указанному в команде непосредственно. В нашем случае - в первые два байта "аккумулятора".
Потом в H,L заносятся "ручками" регистры B,C и помещаются в следующие 2 позиции. Наконец, старые значения (H,L) восстанавливаются (а регистры D,E "затираются" - на их месте копии B,C). При вызове из FADD/FSUB это нахрен не нужно (они уже затёрты), но где-то в других местах это может быть важно.

Возвращаемся к нашему FADD. Оба числа проверили на ноль, и если хоть одно было нулевым, уже вернулись. Проверка на ноль - это не просто "оптимизация", это необходимость, т.к ноль это особый случай. Если его не рассматривать, то вместо нуля получится число 2-127 (неявная единица в мантиссе!)

Дальше код по сравнению экспонент двух НЕНУЛЕВЫХ чисел:
081C: 90                SUB B   ;A=rhs.exponent-lhs.exponent.
081D: D22C08            JNC L082C       ;If rhs' exponent >= lhs'exponent, jump ahead.
0820: 2F                CMA     ;Two's complement the exponent
0821: 3C                INR A   ;difference, so it's correct.
0822: EB                XCHG    ;
0823: CD020A            CALL FPush      ;Push old rhs
0826: EB                XCHG    ;
0827: CD120A            CALL FLoadFromBCDE      ;rhs = old lhs
082A: C1                POP B   ;lhs = old rhs.
082B: D1                POP D   ;
082C: F5        L082C   PUSH PSW        ;Preserve exponent diff
082D: CD370A            CALL FUnpackMantissas   


С прошлого раза в регистре A уже лежала экспонента правого числа, теперь вычитаем экспоненту левого. Если результат меньше нуля, т.е в "аккумуляторе" число заведомо меньшее по модулю, чем в регистрах, то два числа хитрым образом меняются местами. Но для начала исправляется разность экспонент, по сути, она берётся по модулю. Для этого сначала стоит команда CMA (complement A) - инвертировать каждый бит A, и затем ещё прибавляем единичку. Классика...

Затем XCHG - опять меняем местами (D,E) и (H,L), и вызываем FPush, вот она:
0A02: EB        FPush   XCHG    
0A03: 2A6F01            LHLD FACCUM     
0A06: E3                XTHL    
0A07: E5                PUSH H  
0A08: 2A7101            LHLD FACCUM+2   
0A0B: E3                XTHL    
0A0C: E5                PUSH H  
0A0D: EB                XCHG    
0A0E: C9                RET


а здесь в обратную сторону exchange, потом два младших бита "аккумулятора" загружаются в (H,L). XTHL меняет местами два байта в стеке с двумя байтами из (H,L), т.е адрес возврата из FPush сейчас оказался в (H,L), а 2 байта числа - в стеке. Затем через PUSH H мы помещаем адрес возврата "выше", и потом такой же манёвр проделываем с оставшимися двумя байтами числа. Наконец, XCHG ещё разок меняет (D,E) и (H,L). Похоже, эти "общие процедуры" уверены, что (D,E) можно затереть, проблемы не будет, а (H,L) НЕ ТРОЖЬ! А поскольку у нас была обратная ситуация, её саму ещё обрамили командами XCHG.

Далее вызывается команда FLoadFromBCDE и заменяет аккумулятор на наше левое число, лежавшее в B,C,D,E. И наконец, через POP B, POP D обмен двумя числами заканчивается. Сейчас в FACCUM лежит число предположительно большее, чем в BCDE, но, если экспоненты совпадали, это может быть не так.

Лежащая в A разность экспонент (теперь уже по модулю) помещается в стек (у Intel 8080 нет PUSH A, вместо этого PUSH PSW, тут окромя A заносится слово состояния, Program Status Word).

Наконец, запускается процедура UnpackMantissas:

0A37: 217101    FUnpackMantissas        LXI H,FACCUM+2  
0A3A: 7E                MOV A,M ;
0A3B: 07                RLC     ;Move FACCUM's sign to bit 0.
0A3C: 37                STC     ;Set MSB of FACCUM mantissa,
0A3D: 1F                RAR     ;FACCUM's sign is now in carry.
0A3E: 77                MOV M,A ;
0A3F: 3F                CMC     ;Negate FACCUM's sign.
0A40: 1F                RAR     ;Bit 7 of A is now FACCUM's sign.
0A41: 23                INX H   ;Store negated FACCUM sign at FTEMP_SIGN.
0A42: 23                INX H   ;
0A43: 77                MOV M,A ;
0A44: 79                MOV A,C ;
0A45: 07                RLC     ;Set MSB of BCDE mantissa,
0A46: 37                STC     ;BCDE's sign is now in carry.
0A47: 1F                RAR     ;
0A48: 4F                MOV C,A ;
0A49: 1F                RAR     ;Bit 7 of A is now BCDE's sign
0A4A: AE                XRA M   ;XORed with FTEMP_SIGN.
0A4B: C9                RET     ;


Загружаем старший байт мантиссы из FAccum, где и хранится знак. Делаем "вращение на месте влево", т.е старший бит занимает место самого младшего и попутно попадает в Carry. Затем в Carry принудительно устанавливаем единицу - и теперь вращаем вправо, но на этот раз через перенос. Т.е лежавший в младшем бите знак всё-таки попадает в Carry, а само число возвращается в норму, только вместо знака - всегда единица, и это "неявная старшая единица" мантиссы. Из регистра A возвращаем это дело в память.

Инвертируем Carry, снова впихиваем его старшим битом A - и сохраняем это значение в FTemp в память.

Ровно таким же макаром поступаем со второй старшей мантиссой, лежащей в регистре C, и, по сути, делаем от двух знаков чисел XNOR, т.е результат будет единицей, если знаки совпадают, и нулём - если они разные. Замечу: знак числа из FACCUM лежит себе тихо в FTEMP, а результат XNOR лежит пока в регистре A, и более нигде.

Продолжаем. Число, лежащее в FAccum, трогать не нужно, а вот лежащее в B,C,D,E, возможно, надо сдвинуть вправо. Знаки пока убрали в сторонку, а на их место поставили "неявные старшие единицы", теперь уже явные Следующий фрагмент кода:
0830: 67                MOV H,A ;H=sign relationship
0831: F1                POP PSW ;A=exponent diff.
0832: CDC908            CALL FMantissaRtMult    ;Shift lhs mantissa right by (exponent diff) places.


Нужно ли складывать или вычитать числа (старший бит A) - пока сохраняем в регистре H, а сами возвращаем в A разность экспонент. Затем вызываем FMAntissaRtMult (shift Right Multiple times), вот она:

08C9: 0600      FMantissaRtMult MVI B,00h       ;Initialise extra mantissa byte
08CB: 3C                INR A   
08CC: 6F                MOV L,A 
08CD: AF        RtMultLoop      XRA A   
08CE: 2D                DCR L   
08CF: C8                RZ      
08D0: CDD608            CALL FMantissaRtOnce    
08D3: C3CD08            JMP RtMultLoop  


Некое "топтание на одном месте" - к разнице экспонент прибавили единицу, чтобы потом всегда проверять результат на ноль после вычитания единицы. Странность в том, что можно было бы проверять флаг C, он при декременте (DCR) тоже устанавливается. Тогда INR A можно было бы не делать. И вообще какая-то пляска непонятная. У нас в стеке лежала разность экспонент, в A лежал знак. В итоге мы знак перетащили в H, а разность экспонент в итоге - в L. Чего бы для начала не сделать POP H, потом mov L, A, это ещё до вызова FMantissaRtMult, и тут две строчки уже не нужны.

Ну, дальше всё понятно: вызываем FMantissaRtOnce столько раз, на сколько бит надо сдвинуть. Зачем обнуление A ровно тут, внутри цикла (XRA A) - тоже без понятия. В FMantissaRtOnce он всё равно заместится, разве что по возврату мы ожидаем ноль. так не проще ли его за пределы функции вывести? Я посмотрел: функция вызывается дважды, и только в нашем FAdd ожидается A=0. Чужая душа - потёмки...

Глянем на FMantissaRtOnce:
08D6: 79        FMantissaRtOnce MOV A,C 
08D7: 1F                RAR     
08D8: 4F                MOV C,A 
08D9: 7A                MOV A,D 
08DA: 1F                RAR     
08DB: 57                MOV D,A 
08DC: 7B                MOV A,E 
08DD: 1F                RAR     
08DE: 5F                MOV E,A 
08DF: 78                MOV A,B ;NB: B is the extra
08E0: 1F                RAR     ;mantissa byte.
08E1: 47                MOV B,A ;
08E2: C9                RET     ;


Да, такой код уже видели. 4 байта (3, которые были и ещё один дополнительный) двигаем вправо, используя "вращение вправо через перенос".

В коде для 8051 был куда более совершенный сдвиг вправо: если разность экспонент была более 24, мы сразу прекращали работу, считая второе число нулевым. А потом делали некую разновидность Barrel Shifter: если необходим сдвиг на 16 бит, это делали "побайтово", затем на 8 бит "побайтово", и только оставшиеся 0..7 - сдвиганием по единичке. Здесь ничего этого нет - двигаем по единичке, причём даже нет проверки на разность экспонент превышающую 24. Эта штука может и 254 итерации провернуть, и не поморщится!

Продолжим: мантиссу "меньшего" числа сдвинули сколько надо. Смотрим дальше:
0835: B4                ORA H   ;A=0 after last call, so this tests
0836: 216F01            LXI H,FACCUM    ;the sign relationship.
0839: F24D08            JP FSubMantissas        ;Jump ahead if we need to subtract.
083C: CDA908            CALL FAddMantissas      ;


Признак "складывать или вычитать" поместили в A, с установкой флагов, а регистр H начинает указывать на младший байт мантиссы "аккумулятора".

Если "результат положительный", т.е в старшем бите A сидит нолик, делаем вычитание мантисс, причём это прыжок, а не вызов функции, т.к на этот FSubMantissas мы прыгаем исключительно отсюда, и он сам знает, что делать дальше. А FAddMantissas вызывается, т.к она используется где-то ещё и должна "знать", куда вернуться. Глянем, для начала, на неё:

08A9: 7E        FAddMantissas   MOV A,M 
08AA: 83                ADD E   
08AB: 5F                MOV E,A 
08AC: 23                INX H   
08AD: 7E                MOV A,M 
08AE: 8A                ADC D   
08AF: 57                MOV D,A 
08B0: 23                INX H   
08B1: 7E                MOV A,M 
08B2: 89                ADC C   
08B3: 4F                MOV C,A 
08B4: C9                RET     

Сложили младшие байты, результат вернули в регистр E. Прибавили адрес "указателя" (H,L) - и принимаемся за следующий байт. Тут сложение с переносом (ADC), и таких два. В итоге в C,D,E и флаге переноса лежит итоговый результат.

И посмотрим следующий за сложением код:
083F: D27E08            JNC FRoundUp    ;Jump ahead if that didn't overflow.
0842: 23                INX H   ;Flip the sign in FTEMP_SIGN. (А ВОТ И НЕТ! На байт экспоненты перемещаемся)
0843: 34                INR M   ; 
0844: CAA408            JZ Overflow     ;Error out if exponent overflowed.
0847: CDD608            CALL FMantissaRtOnce;Shift mantissa one place right
084A: C37E08            JMP FRoundUp    ;Jump ahead.


Этот кусочек следит за переполнением. Если его не случилось, сразу прыгаем дальше, в FRoundUp (округление). В противном случае, прибавляем один к экспоненте, проверяем, не переполнилась ли она (стала из 255 нулём) - ежели так, прыгаем в Overflow, а тот, в свою очередь, вызовет Error - выскажет пользователю всё, что о нём думает. Если экспонента не переполнилась, ещё надо сдвинуть мантиссу на единичку вправо (эту процедуру уже разбирали) - и также прыгаем в FRoundUp.

Рассмотрим теперь ветвь вычитания:
084D: AF        FSubMantissas   XRA A   ;B=0-B
084E: 90                SUB B   ;
084F: 47                MOV B,A ;
0850: 7E                MOV A,M ;E=(FACCUM)-E
0851: 9B                SBB E   ;
0852: 5F                MOV E,A ;
0853: 23                INX H   ;
0854: 7E                MOV A,M ;D=(FACCUM+1)-D
0855: 9A                SBB D   
0856: 57                MOV D,A 
0857: 23                INX H   
0858: 7E                MOV A,M ;C=(FACCUM+2)-C
0859: 99                SBB C   ;
085A: 4F                MOV C,A ;


Тут используется и четвёртый, вспомогательный байт мантиссы (лежит в регистре B, и только у "меньшего" числа. Раньше в B лежала экспонента, но экспонента "меньшего" числа уже не нужна!). А в целом, принцип тот же самый, обычное вычитание в столбик. Результат лежит в регистрах С,D,E,B (в порядке старшинства), и ещё в Carry может быть единица, если мы всё-таки не угадали, что из чего вычитать.

Смотрим дальше:
085B: DCB508    FNormalise      CC FNegateInt   ; 


CC - это Call if Carry. То самое - не угадали со знаком, и здесь просто поменять знак не получится. К примеру, в 1-байтном случае, если мы ошиблись и из 0 вычли 1, то сейчас результатом будет 0xFF (т.е 255) и Carry=1. Просто записать это как "-255" - ответ, очевидно, неверен! Нужно сменить знак числу "в дополнительном коде", т.е превратить 255 в 1, ну и знак "-" снаружи приделать. Сейчас посмотрим, как это делается:
08B5: 217301    FNegateInt      LXI H,FTEMP     
08B8: 7E                MOV A,M 
08B9: 2F                CMA     
08BA: 77                MOV M,A 
08BB: AF                XRA A   
08BC: 6F                MOV L,A 
08BD: 90                SUB B   
08BE: 47                MOV B,A 
08BF: 7D                MOV A,L 
08C0: 9B                SBB E   
08C1: 5F                MOV E,A 
08C2: 7D                MOV A,L 
08C3: 9A                SBB D   
08C4: 57                MOV D,A 
08C5: 7D                MOV A,L 
08C6: 99                SBB C   
08C7: 4F                MOV C,A 
08C8: C9                RET


Первые 4 строки меняют знак результата, который хранится в FTemp. Приходится его загрузить в A, инвертировать и поместить назад. Затем A обнуляется через XOR, и этот нолик заносится в L. Дальше мы, по сути "в столбик" вычитаем получившееся число из нуля. Чтобы каждый раз занулить A, но не потерять перенос, нельзя это делать через XOR, вот для того и был нужен нолик в L: ведь mov A,L никаких флагов не меняет. Обращаем мы таким способом 4-байтовую мантиссу.

Продолжаем с вычитанием: если со знаком не угадали, это мы исправили, вычитание завершено, но результат не нормирован - может быть толпа нулей слева.
085E: 2600              MVI H,00h       ;
0860: 79                MOV A,C ;Test most-significant bit of mantissa
0861: B7                ORA A   ;and jump ahead if it's 1.
0862: FA7E08            JM FRoundUp     ;
0865: FEE0      NormLoop        CPI 0xE0        ;If we've shifted 32 times,
0867: CABE09            JZ FZero        ;then the number is 0.
086A: 25                DCR H   ;
086B: 78                MOV A,B ;Left-shift extra mantissa byte
086C: 87                ADD A   ; 
086D: 47                MOV B,A ; 
086E: CD9008            CALL FMantissaLeft      ;Left-shift mantissa.
0871: 7C                MOV A,H ;
0872: F26508            JP NormLoop     ;Loop


Здесь поначалу тоже творится нечто странное. Решено было подсчитывать, сколько уже прошло сдвигов в регистре H, причём считать "в минус", т.е 0, 255, 254, ... 0xE0 = 224. Или: 0, -1, ..., -32.

Вариант "совсем сдвигать не надо" рассмотрен отдельно ещё перед циклом - тогда мы прыгаем в FRoundUp (на нём мы остановились и рассматривая ветвь сложения).

Странность, что далее тот же самый A сравнивается с числом 224 (или -32), хотя это вообще не то сейчас, это старший бит мантиссы! Да, определённая логика есть: нас интересует равенство, а тут равенства заведомо не может быть, ведь если старший бит был единицей, мы уже отсюда свалили!

Потом уже, войдя в цикл, вычитаем единичку из H, сдвигаем влево дополнительный байт мантиссы, путём прибавления к самому себе, и затем вызываем FMantissaLeft, вот она:

0890: 7B        FMantissaLeft   MOV A,E 
0891: 17                RAL     
0892: 5F                MOV E,A 
0893: 7A                MOV A,D 
0894: 17                RAL     
0895: 57                MOV D,A 
0896: 79                MOV A,C 
0897: 8F                ADC A   
0898: 4F                MOV C,A 
0899: C9                RET     


Здесь есть своя хитрость. Для сдвига влево двух байт, E и D, использована команда RAL (вращение влево через перенос), а вот для последнего - ADC A (к регистру A прибавляется он же самый, и ещё перенос). Разница между ними - во времени исполнения (ADC чуть дольше) а ещё во флагах, которые меняются. ADC устанавливает не только Carry (как RAL), но ещё и Zero, Sign и прочие.

Именно Sign нас интересует - это, по сути, старший бит результата. Если он установится в единицу, значит, мы, наконец-то, сдвинулись достаточно. Если нет, мы продолжаем итерации по сдвигу, но если их пройдёт уже 32 штуки, то выпрыгнем в FZero, причём это снова Tail Call - там нам установят нулевой результат и выполнение FADD/FSUB завершится - возврат будет сразу туда, откуда они вызваны.

Если же всё успешно, будет ещё небольшой код:
0875: 217201            LXI H,FACCUM+3  ;
0878: 86                ADD M   ;
0879: 77                MOV M,A ;Since A was a -ve number, that certainly should
087A: D2BE09            JNC FZero       ;have carried, hence the extra check for zero.
087D: C8                RZ      ;?why?


Хотя мы считали "число итераций" в регистре H, каждый раз это значение ещё переносилось в A. Сейчас указываем (H,L) на экспоненту "аккумулятора", и прибавляем её к "отрицательному числу итераций". Тем самым, экспонента приобретает правильное значение. Но за такое количество сдвигов она могла стать отрицательной - тогда через Tail call устанавливаем нулевое значение и выходим. И наконец, могла получиться экспонента строго равная нулю, в таком случае ничего выставлять и не надо - если у числа экспонента нулевая, это уже сразу считаем нулём. Поэтому просто Return if Zero, т.е тоже возврат.

О РАДОСТЬ: МЫ ДОБРАЛИСЬ ДО FRoundUp! Сюда мы приходим по обеим ветвям - и при сложении, и при вычитании. Вот он:
087E: 78        FRoundUp        MOV A,B ;A=extra mantissa byte
087F: 217201            LXI H,FACCUM+3  ;
0882: B7                ORA A   ;If bit 7 of the extra mantissa byte
0883: FC9A08            CM FMantissaInc ;is set, then round up the mantissa.


Проверяем старший бит дополнительного байта мантиссы. Если так, вызываем функцию FMantissaInc:
089A: 1C        FMantissaInc    INR E   
089B: C0                RNZ     
089C: 14                INR D   
089D: C0                RNZ     
089E: 0C                INR C   
089F: C0                RNZ     
08A0: 0E80              MVI C,80h       ;Mantissa overflowed to zero, so set it
08A2: 34                INR M   ;to 1 and increment the exponent.
08A3: C0                RNZ     ;And if the exponent overflows...
08A4: 1E0A      Overflow        MVI E,0Ah       
08A6: C3D501            JMP Error       


RNZ - это Return if Not Zero. В общем, к младшему байту мантиссы прибавляем единичку. Если он стал нулём, продолжаем дальше (ситуация наподобие 999 + 1: в младшем разряде вышел ноль, значит, надо и к следующему единичку прибавлять). Сначала ковыряем 3 байта мантиссы. Если даже старший байт стал нулём, мы "ручками" возвращаем ему старший бит и ещё прибавляем единицу к экспоненте. Если же и она стала нулём - мы "своим ходом" оказываемся на метке Overflow, откуда нас и возьмут за белы рученьки.

Заключительные операции
0886: 46                MOV B,M ;B=exponent
0887: 23                INX H   ;
0888: 7E                MOV A,M ;A=FTEMP_SIGN
0889: E680              ANI 0x80        ;
088B: A9                XRA C   ;Bit 7 of C is always 1. Thi
088C: 4F                MOV C,A ;
088D: C3120A            JMP FLoadFromBCDE       ;Exit via copying BCDE to FACCUM.

На этом моменте у нас мантисса лежала в C,D,E,B (в B самый младший "вспомогательный" байт), итоговая экспонента по адресу FAccum+3, и ещё итоговый знак по адресу FTemp.
Сейчас экспоненту всовываем в B, затем в старший бит A запихиваем знак (а остальные биты нас не волнуют), через XOR объединяем его со старшим байтом мантиссы. Там у нас стояла "неявная старшая единица мантиссы" - вот для чего мы её восстанавливали в FMantissaInc - чтобы был понятен результат этого XOR'а.

Ну и всё - тем самым в регистрах B,C,D,E получился упакованный результат. Осталось только перетащить его в FAccum, чем займётся уже известная нам FLoadFromBCDE. Это снова Tail Call, так что возврат из неё - это и будет завершение FADD/FSUB.

Такая вот хреновина...
Вероятно, она чуть более экономична по занимаемому пространству - даже когда мы весь исполняющийся код посчитали, получилось 274 байта. Но здесь гораздо больше процедур, многие из которых задействованы и в других местах этой "математической библиотеки".

Но прямо "идеальной" её не назовёшь - тут и неприятный "наихудший случай", где сложение/вычитание может выполняться ОЧЕНЬ ДОЛГО, и этот хитрючий костыль с наложением команд, который, как оказывается, нахрен здесь не нужен, и ещё пара мест, где сделано неоптимально. Например, что только после 32 безуспешных сдвигов влево выводится заключение "да это же ноль". Вроде как хватило бы и 24 сдвигов, ну если совсем неуютно - то 25. Ведь самые младшие биты могут получиться только если одно число довольно прилично сдвинули вправо. Но если оно так сдвинуто, то при вычитании не может получиться ноль во всех старших разрядах!

Грубо говоря, у нас было число 1000 0000 (двоичное) и второе, 1111 1111, но с экспонентой на единичку меньше. Его мы сдвинули вправо и получили:
  1000 0000
-  111 1111 1
---------------
            1

Да, здесь старший байт занулился, но всего один "дополнительный" бит. А если сдвиг на две позиции вправо?
  1000 0000
-   11 1111 11
---------------
   100 0000 11


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


View Poll: Код сложения с плав. точкой для Altair Basic

(хотелось добавить вариант windows must die или что-то похожее - считается, что этот успешно заработавший бейсик и заложил основание для Microsoft, но тогда результат немного ожидаем :))

Оставить комментарий

Архив записей в блогах:
Считается, что многие наши болезни мы приобретаем на нервной почве, от стрессов. Да, мы ещё не научились философски относиться к нашим несчастьям и неприятностям, дёргаемся по любому пустяку. И жизнь так устроена, что очень редко гладит нас по головке, всё больше бьёт фейсом об ...
...
Люди странные существа, они хотят всего и сразу и что это пришло к ним само. Одни хотят денег, другие имею деньги, но не у тех и у тех не всегда есть то что важно. А на данный ...
Я - не "Шарли Эбдо"! 10 против 2000.      Я не разделяю европейские ценности.. Считаю, что 2000 погибших мирных горожан Донецка и Луганска, дети и пенсионеры, наши четверо журналистов заслуживали смерти гораздо меньше, чем 10 провокаторов из "Шарли Эбдо".      Наши российские щелкопе ...
Вчера вечером (в ленте Твиттера это появилось около часу ночи ) произошло культурное событие активистов оппозиции. А если быть точнее - новый шаг к торжеству ИСТИННОЙ демократии . Семимильными шагами Удальцов идет по стране и приближает ...