Discussion:
shorter code to print a 16bit number in 8086
(too old to reply)
luser...@nospicedham.gmail.com
2021-07-30 05:11:00 UTC
Permalink
I wrote this machine code to trace the LIT word in my forth interpreter.
Is it possible to tighten up this code? It's trying to print a 16bit signed
integer (ignoring INT_MIN) left to right with no leading zeros.

This is assembly code, but I hope you'll forgive my ideosyncratic syntax.
The operands map left to right to the reg and reg/mem fields into the
mod-reg-reg/mem byte and the opcodes target the "to" form (direction
field = 1), eg. in "MOV(,R,BX,AX)", BX is the dest, AX is the source, the
R is needed to select register-to-register mode (the mod field). The empty
first argument is for tweaks to the opcode which could be F (clear the
direction field by subtracting 2) or BYTE (clear the word field by subtracting 1).

It tests each digit for zero where it jumps over the int 10h call except
the last digit.

It occurs to me that the DIV instruction has a mod field. So that means
the divisor could be register indirect, right? like through SI or BX, maybe?
Then it could probably be rolled up in a loop, right? Sorry if I'm jumping
the gun asking for help before exerting the requisite effort.

CODE(lit, lit, LODS, PUSH(AX)

#ifdef TRACE

, MOV(,R,BX,AX), OR(,R,BX,BX), JGE,7, MOVI(AX,0x0E00+'-'), INT(10), NEG(R,BX),

MOV(,R,AX,BX), XOR(,R,DX,DX), MOVI(CX,10000), DIV(R,CX),

OR(,R,AX,AX), JZ,6, MOVBI(AH,0x0E), ADDAL(48), INT(10),

MOV(,R,AX,DX), XOR(,R,DX,DX), MOVI(CX,1000), DIV(R,CX),

OR(,R,AX,AX), JZ,6, MOVBI(AH,0x0E), ADDAL(48), INT(10),

MOV(,R,AX,DX), MOVBI(CL,100), XOR(BYTE,R,CH,CH), BYTE+DIV(R,CL),

MOV(BYTE,R,DL,AH),

OR(BYTE,R,AL,AL), JZ,6, MOVBI(AH,0x0E), ADDAL(48), INT(10),

MOV(BYTE,R,AL,DL), XOR(BYTE,R,AH,AH), MOVBI(CL,10), BYTE+DIV(R,CL),

MOV(BYTE,R,DL,AH),

OR(BYTE,R,AL,AL), JZ,6, MOVBI(AH,0x0E), ADDAL(48), INT(10),

MOVBI(AH,0x0E), MOV(BYTE,R,AL,DL), ADDAL(48), INT(10),

MOVI(AX,0x0E00+' '), INT(10)

#endif

)/**/
Kerr-Mudd, John
2021-07-30 12:52:44 UTC
Permalink
On Thu, 29 Jul 2021 22:11:00 -0700 (PDT)
Post by ***@nospicedham.gmail.com
I wrote this machine code to trace the LIT word in my forth interpreter.
Is it possible to tighten up this code? It's trying to print a 16bit signed
integer (ignoring INT_MIN) left to right with no leading zeros.
This is assembly code, but I hope you'll forgive my ideosyncratic syntax.
The operands map left to right to the reg and reg/mem fields into the
mod-reg-reg/mem byte and the opcodes target the "to" form (direction
field = 1), eg. in "MOV(,R,BX,AX)", BX is the dest, AX is the source, the
R is needed to select register-to-register mode (the mod field). The empty
first argument is for tweaks to the opcode which could be F (clear the
direction field by subtracting 2) or BYTE (clear the word field by subtracting 1).
It tests each digit for zero where it jumps over the int 10h call except
the last digit.
It occurs to me that the DIV instruction has a mod field. So that means
the divisor could be register indirect, right? like through SI or BX, maybe?
Then it could probably be rolled up in a loop, right? Sorry if I'm jumping
the gun asking for help before exerting the requisite effort.
CODE(lit, lit, LODS, PUSH(AX)
#ifdef TRACE
, MOV(,R,BX,AX), OR(,R,BX,BX), JGE,7, MOVI(AX,0x0E00+'-'), INT(10), NEG(R,BX),
MOV(,R,AX,BX), XOR(,R,DX,DX), MOVI(CX,10000), DIV(R,CX),
OR(,R,AX,AX), JZ,6, MOVBI(AH,0x0E), ADDAL(48), INT(10),
MOV(,R,AX,DX), XOR(,R,DX,DX), MOVI(CX,1000), DIV(R,CX),
OR(,R,AX,AX), JZ,6, MOVBI(AH,0x0E), ADDAL(48), INT(10),
MOV(,R,AX,DX), MOVBI(CL,100), XOR(BYTE,R,CH,CH), BYTE+DIV(R,CL),
MOV(BYTE,R,DL,AH),
OR(BYTE,R,AL,AL), JZ,6, MOVBI(AH,0x0E), ADDAL(48), INT(10),
MOV(BYTE,R,AL,DL), XOR(BYTE,R,AH,AH), MOVBI(CL,10), BYTE+DIV(R,CL),
MOV(BYTE,R,DL,AH),
OR(BYTE,R,AL,AL), JZ,6, MOVBI(AH,0x0E), ADDAL(48), INT(10),
MOVBI(AH,0x0E), MOV(BYTE,R,AL,DL), ADDAL(48), INT(10),
MOVI(AX,0x0E00+' '), INT(10)
#endif
)/**/
I think I got the jist of it;

here's some std asm code from earlier, rejigged for int 0x10 - it requires 5 free stack positions.
uses ax,bx,cx,dx - 36 bytes

PrtNum: ; ax to con using int10 fn 0E: no leading spaces or zeros but show '-'
test ax,ax ; Is it positive?
jge NotNeg
push ax
mov ax,0x0E00+'-'
int 0x10 ; bios prtc
pop ax
neg ax ; show as positive: hibit set only for 0x8000!#
NotNeg:
mov bx,10 ; decimal
xor cx,cx ; Digit count (5 max)
NextDigit:
inc cx ; prt at least 1 digit (i.e. '0')
xor dx,dx ; clear prev, cant cwd for #8000
div bx
push dx ; Remainder is the digit
test ax,ax ; Is it zero yet?
jnz NextDigit
PutDigit:
pop ax ; get digit (000x)
add ax,0x0E+'0' ; digit to display value, prtfn in ah
int 0x10 ; bios prtc
loop PutDigit
--
Bah, and indeed Humbug.
Kerr-Mudd, John
2021-07-30 15:45:29 UTC
Permalink
On Fri, 30 Jul 2021 13:52:44 +0100
Post by Kerr-Mudd, John
On Thu, 29 Jul 2021 22:11:00 -0700 (PDT)
Post by ***@nospicedham.gmail.com
I wrote this machine code to trace the LIT word in my forth interpreter.
Is it possible to tighten up this code? It's trying to print a 16bit signed
integer (ignoring INT_MIN) left to right with no leading zeros.
[]
Post by Kerr-Mudd, John
I think I got the jist of it;
here's some std asm code from earlier, rejigged for int 0x10 - it requires 5 free stack positions.
uses ax,bx,cx,dx - 36 bytes
; corrected version - 37
Post by Kerr-Mudd, John
PrtNum: ; ax to con using int10 fn 0E: no leading spaces or zeros but show '-'
test ax,ax ; Is it positive?
jge NotNeg
push ax
mov ax,0x0E00+'-'
int 0x10 ; bios prtc
pop ax
neg ax ; show as positive: hibit set only for 0x8000!#
mov bx,10 ; decimal
xor cx,cx ; Digit count (5 max)
inc cx ; prt at least 1 digit (i.e. '0')
xor dx,dx ; clear prev, cant cwd for #8000
div bx
push dx ; Remainder is the digit
test ax,ax ; Is it zero yet?
jnz NextDigit
pop ax ; get digit (000x)
add ax,0x0E+'0' ; digit to display value, prtfn in ah
I didn't test! Make that line:
add ax,0x0E00+'0' ; digit to display value, prtfn in ah

int 0x10 ; bios prtc
Post by Kerr-Mudd, John
loop PutDigit
--
Bah, and indeed Humbug.
--
Bah, and indeed Humbug.
wolfgang kern
2021-07-30 12:53:56 UTC
Permalink
Post by ***@nospicedham.gmail.com
I wrote this machine code to trace the LIT word in my forth interpreter.
Is it possible to tighten up this code? It's trying to print a 16bit signed
integer (ignoring INT_MIN) left to right with no leading zeros.
I'm sure that thjis could be made much faster and shorter too :)
Post by ***@nospicedham.gmail.com
This is assembly code, but I hope you'll forgive my ideosyncratic syntax.
The operands map left to right to the reg and reg/mem fields into the
mod-reg-reg/mem byte and the opcodes target the "to" form (direction
field = 1), eg. in "MOV(,R,BX,AX)", BX is the dest, AX is the source, the
R is needed to select register-to-register mode (the mod field). The empty
first argument is for tweaks to the opcode which could be F (clear the
direction field by subtracting 2) or BYTE (clear the word field by subtracting 1).
It tests each digit for zero where it jumps over the int 10h call except
the last digit.
to speed it up you can use reciprocal MUL instead if DIV
tried to fall through instead of jump over ?
better would be a buffer and only a single write to screen.
Post by ***@nospicedham.gmail.com
It occurs to me that the DIV instruction has a mod field. So that means
the divisor could be register indirect, right? like through SI or BX, maybe?
Then it could probably be rolled up in a loop, right? Sorry if I'm jumping
the gun asking for help before exerting the requisite effort.
don't you use them already? DIV(R,CX)
Post by ***@nospicedham.gmail.com
CODE(lit, lit, LODS, PUSH(AX)
#ifdef TRACE
, MOV(,R,BX,AX), OR(,R,BX,BX), JGE,7, MOVI(AX,0x0E00+'-'), INT(10), NEG(R,BX),
MOV(,R,AX,BX), XOR(,R,DX,DX), MOVI(CX,10000), DIV(R,CX),
OR(,R,AX,AX), JZ,6, MOVBI(AH,0x0E), ADDAL(48), INT(10),
MOV(,R,AX,DX), XOR(,R,DX,DX), MOVI(CX,1000), DIV(R,CX),
OR(,R,AX,AX), JZ,6, MOVBI(AH,0x0E), ADDAL(48), INT(10),
MOV(,R,AX,DX), MOVBI(CL,100), XOR(BYTE,R,CH,CH), BYTE+DIV(R,CL),
MOV(BYTE,R,DL,AH),
OR(BYTE,R,AL,AL), JZ,6, MOVBI(AH,0x0E), ADDAL(48), INT(10),
MOV(BYTE,R,AL,DL), XOR(BYTE,R,AH,AH), MOVBI(CL,10), BYTE+DIV(R,CL),
MOV(BYTE,R,DL,AH),
OR(BYTE,R,AL,AL), JZ,6, MOVBI(AH,0x0E), ADDAL(48), INT(10),
MOVBI(AH,0x0E), MOV(BYTE,R,AL,DL), ADDAL(48), INT(10),
MOVI(AX,0x0E00+' '), INT(10)
#endif
I'm not familiar with this syntax, but I see many equal lines before the
too many INT 10.

F7 F0 ...F7 F7 and F6 F7..F6 F7 are indeed DIV by reg
same for the IDIV groups F7 F8...
__
wolfgang
R.Wieser
2021-07-30 15:26:59 UTC
Permalink
luser,
It's trying to print a 16bit signed integer (ignoring INT_MIN)
left to right with no leading zeros.
...
It tests each digit for zero where it jumps over the int 10h call
except the last digit.
Those two do not match. And as your code seem to be written according to
the second ...
Try printing the value 3002 (or something else with embedded zeroes) to see
what I mean.
Is it possible to tighten up this code?
I'm not sure what "tighten up" is ment to be read as. Smaller ? Faster ?
In the first case, how many bytes is it now ?

My own, erstwhile "trick" was, while doing the same as you (dividing by the
largest divisor first), to load a register with the divisor and than call a
small routine doing the division and printing.

But in the light of your "skip leading zeroes" you could do worse than to
look at what John posted, as that method automatically discards leading
zeroes - it simply stops as soon as the remainder becomes zero. I'm not so
sure about if that "loop" at the end will work though, as the INT 0x10 could
easily trash cx..
It occurs to me that the DIV instruction has a mod field. So that
means the divisor could be register indirect, right? like through SI
or BX, maybe?
Yes, would work. And that same list can than also be used to skip leading
zeroes.
Sorry if I'm jumping the gun asking for help before exerting the
requisite effort.
I can imagine that you ask, but I do not quite understand why you did not
test your own code first ...

Regards,
Rudy Wieser
Kerr-Mudd, John
2021-07-30 18:55:09 UTC
Permalink
On Fri, 30 Jul 2021 17:26:59 +0200
Post by R.Wieser
luser,
It's trying to print a 16bit signed integer (ignoring INT_MIN)
left to right with no leading zeros.
...
It tests each digit for zero where it jumps over the int 10h call
except the last digit.
Those two do not match. And as your code seem to be written according to
the second ...
Try printing the value 3002 (or something else with embedded zeroes) to see
what I mean.
Is it possible to tighten up this code?
I'm not sure what "tighten up" is ment to be read as. Smaller ? Faster ?
In the first case, how many bytes is it now ?
My own, erstwhile "trick" was, while doing the same as you (dividing by the
largest divisor first), to load a register with the divisor and than call a
small routine doing the division and printing.
But in the light of your "skip leading zeroes" you could do worse than to
look at what John posted, as that method automatically discards leading
zeroes - it simply stops as soon as the remainder becomes zero. I'm not so
sure about if that "loop" at the end will work though, as the INT 0x10 could
easily trash cx..
Seems to be OK on this "DOS" VDM under XP - but yes, the int 10 was put in for the OP - originally the code put to a buffer at di, with a single print (ah=9, Int 21) at the end.
Post by R.Wieser
It occurs to me that the DIV instruction has a mod field. So that
means the divisor could be register indirect, right? like through SI
or BX, maybe?
Yes, would work. And that same list can than also be used to skip leading
zeroes.
Sorry if I'm jumping the gun asking for help before exerting the
requisite effort.
I can imagine that you ask, but I do not quite understand why you did not
test your own code first ...
Regards,
Rudy Wieser
--
Bah, and indeed Humbug.
R.Wieser
2021-07-30 19:23:19 UTC
Permalink
John,
Post by Kerr-Mudd, John
Post by R.Wieser
sure about if that "loop" at the end will work though, as the INT 0x10
could easily trash cx..
Seems to be OK on this "DOS" VDM under XP - but yes, the int 10 was
put in for the OP - originally the code put to a buffer at di, with a
single
print (ah=9, Int 21) at the end.
Yes, it also works here (XP Pro sp3). But its not something I will bet my
life on - because the specs say that a(ny) function call (including INTs)
may trash AX thru DX at will.

I know its hard to take, but the extra two bytes for a push/pop cx around
that last INT 0x10 are really needed. :-)

Regards,
Rudy Wieser
Rod Pemberton
2021-07-30 10:01:13 UTC
Permalink
On Thu, 29 Jul 2021 22:11:00 -0700 (PDT)
"***@nospicedham.gmail.com" <***@nospicedham.gmail.com>
wrote:


Sorry Frank, the first part of this may be a off-topic,
but I haven't been reading or posting to comp.lang.forth
in a while. So, I can understand if the OP posts here
with some Forth-ish assembly ...
Post by ***@nospicedham.gmail.com
I wrote this machine code to trace the LIT word in
my forth interpreter.
LIT is usually very simple and just stores an integer from
the interpreter's input stream into the current Forth word.


In C, my LIT primitive - the low-level function which goes
with the LIT dictionary entry - is:

void lit__(void)
{
push((cell_t)*ip);
ip++;
}

This dereferences/fetches what's at the Forth interpreter's
instruction pointer IP, stores that value on the parameter/data
stack, and then increments the IP.


I have LIT in the initial dictionary in C code (which I
use instead of assembly), because LIT is needed prior to
loading a high-level Forth dictionary. LIT is not a
standardized Forth word (subroutine).


You should be able to construct a LIT from your COMPILE
definition. E.g., just drop the , (comma) or equivalent
Forth word like COMPILE, from the end of the definition:

: COMPILE R> DUP CELL+ >R @ , ;
: LIT R> DUP CELL+ >R @ ;

For my personal Forth interpreter, the LIT is high-level
version of LIT equivalent to the low-level C code above.

I.e., the R> (r-from) captures the interpreter's saved IP,
DUPlicates the IP, increments one copy of the IP to skip
over the value, places the adjusted IP onto the return stack
via >R (to-r), fetches the value from the original IP onto
the parameter/data stack. That is the LIT value.
Post by ***@nospicedham.gmail.com
Is it possible to tighten up this code? It's trying to
print a 16bit signed integer (ignoring INT_MIN) left to
right with no leading zeros.
Well, I really didn't look at the Forth-ish assembly,
but my suggestion to make your life easier would be
to print the decimal(?) "16bit signed integer" instead
in hexadecimal as a "16bit UN-signed integer".
--
...
Frank Kotler
2021-07-30 22:39:39 UTC
Permalink
Post by Kerr-Mudd, John
On Thu, 29 Jul 2021 22:11:00 -0700 (PDT)
Sorry Frank,
Sorry Rod for the delay in posting = you keep changing your address so
you're not in the "whitelist" (name changed soon to "karenlist" so it's
not racist)
Try to stay on topic everyone... but it's bot as if we're swamped with
traffic...

Best,
Frank
[moderator]
luser...@nospicedham.gmail.com
2021-08-07 05:04:30 UTC
Permalink
Post by ***@nospicedham.gmail.com
I wrote this machine code to trace the LIT word in my forth interpreter.
Is it possible to tighten up this code? It's trying to print a 16bit signed
integer (ignoring INT_MIN) left to right with no leading zeros.
This is assembly code, but I hope you'll forgive my ideosyncratic syntax.
The operands map left to right to the reg and reg/mem fields into the
mod-reg-reg/mem byte and the opcodes target the "to" form (direction
field = 1), eg. in "MOV(,R,BX,AX)", BX is the dest, AX is the source, the
R is needed to select register-to-register mode (the mod field). The empty
first argument is for tweaks to the opcode which could be F (clear the
direction field by subtracting 2) or BYTE (clear the word field by subtracting 1).
It tests each digit for zero where it jumps over the int 10h call except
the last digit.
It occurs to me that the DIV instruction has a mod field. So that means
the divisor could be register indirect, right? like through SI or BX, maybe?
Then it could probably be rolled up in a loop, right? Sorry if I'm jumping
the gun asking for help before exerting the requisite effort.
Thanks for the help, everyone! Thanks to Rudy for pointing out that my
algorithm was malformed. I had tested it (sort of, I ran it and saw that
there was output), but I didn't check it against known values and so
didn't see the problem of suppressing inner zeros. (the numbers it
printed did appear weird, but I didn't properly consider and valuate
that clue).

John's code is so sweet and simple, I am envious of the ability to write
in such a way. I translated it to my syntax and then tried a few of the
variations alluded to earlier, like using BX to point to a table of divisors.
But I haven't actually implemented the LOOP instruction in the emulator
and the syntax I have for using labels in machine code hasn't been
incorporated into the emulator at all yet. So I haven't tested any of the
following code except to run it through CPP to check that macro expansion
works.

First I just translated John's code to my syntax (now with labels) and
lo and behold, it's super short and sweet. Exactly what I asked for.

CODE(lit, (LIT, LODS, PUSH(AX))
#ifdef TRACE
, (PrtNum, TEST(R,AX,AX), JGE, NotNeg-_, \
PUSH(AX), MOVAX(0xE00+'-'), INT(10), POP(AX), NEG(R,AX)),
(NotNeg, MOVBX(10), XOR(,R,CX,CX)),
(NextDigit, INC(CX), XOR(DX,DX), DIV(BX), PUSH(DX), TEST(AX,AX), JNZ, NextDigit-_),
(PutDigit, POP(AX), ADDAX(0xE00+'0'), INT(10), LOOP, PutDigit-_)
#endif
)

First I tried doing it with a table of divisors and unrolled code to divide by each
word in the table.

CODE(lit, (LIT, LODS, PUSH(AX))
#ifdef TRACE
, (stub, sJMP, CODE-_),
(divisors, DW(10000), DW(1000), DW(100), DW(10)),
(CODE, MOVBX(divisors), XOR(,R,DX,DX),
DIV(Z,BX_), ADDAX(0xE00+'0'), INT(10)),
(L1, MOV(,R,AX,DX), DIV(B,BX_),2, ADDAX(0xE000+'0'), INT(10),
MOV(,R,AX,DX), DIV(B,BX_),4, ADDAX(0xE000+'0'), INT(10)),
(L2, MOV(,R,AX,DX), DIV(B,BX_),6, ADDAX(0xE000+'0'), INT(10),
MOD(,R,AX,DX), ADDAX(0xE00+'0'), INT(10))
#endif
)

Which is longer, uglier, replete with repetition.

Then I tried doing it with a table of divisors and a loop to run through left-to-right.

CODE(lit, (LIT, LODS, PUSH(AX))
#ifdef TRACE
, (tracelit, MOVBX(divisors), XOR(,R,DX,DX), MOVCX(5)),
(digit, DIV(Z,BX_), ADDAX(0xE00+'0'), INT(10),
MOV(,R,AX,DX), ADDI(BX,2), LOOP, digit-_,
NEXT)
(divisors, DW(10000), DW(1000), DW(100), DW(10), DW(1)),
#endif
)

where NEXT is a macro for jumping to the next Forth word, pointed to by SI.
That's pretty short, but it treats all numbers as unsigned and makes
no attempt to suppress leading zeros.

Then I tried without using a table and calling a procedure for each digit.

CODE(lit, (LIT, LODS, PUSH(AX))
#ifdef TRACE
, (tracelit, XOR(,R,DX,DX), MOVCX(5),
TEST(R,AX,AX), JGE,notneg,
MOVI(AX,0xE00+'-'), INT(10), NEG(R,AX), JNO, notneg,
INC(R,DX)),
(notneg, MOVI(BX,10000), DIV(R,BX), CALL, print,
MOVI(BX,1000), MOV(,R,AX,DX), DIV(R,BX), CALL, print,
MOVI(BX,100), MOV(,R,AX,DX), DIV(R,BX), CALL, print,
MOVI(BX,10), MOV(,R,AX,DX), DIV(R,BX), CALL, print,
MOV(,R,AX,DX), CALL, print,
NEXT),
(print, ADDAX(0xE00'0'), INT(10), RET)
#endif
)

It's pretty cool that adding labels also lets me have local procedures,
but I feel like the call instruction vs, the ADD and INT instructions
doesn't really save much and is slower and longer on the screen.
But maybe all that just means that I should be using a DOS string
printing function instead of a character printing function.

And also, Rod was right and this whole effort was a wholy
unnecessary X/Y problem. And if I just add a high-level LIT word,
then I can call U. to print the number be done with this snipe hunt.

On a side note, I stumbled across the source on github for DOS 1.25
and 2.0 and I'm browsing ASM.ASM from 1.25 to get ideas about how to
make an assembler with a more common syntax.

Loading...