Search code examples
assemblycomparisonsignedintel-8080

How do I efficiently do signed comparisons on the 8080?


I want to compare two 16-bit numbers and branch on the result: the equivalent of if (a<b) goto negative. I'm using an Intel 8080.

The Z80 has a signed arithmetic overflow flag which can be used for this with some degree of effort. The standard code is:

ld de, _left
ld hl, _right
ld a, e
sub a, l
ld a, d
sbc a, h
jp po, $+5  ; branch on overflow flag not set
xor a, 0x80 ; flip sign bit
jm negative ; actually do the test

But the 8080 isn't a strict subset of the Z80, and the code above won't work there --- on the 8080, arithmetic instructions set the P flag based on the parity of the result, with hilarious results.

So what's the idiomatic way to do signed comparisons on the 8080?

Actually calculating the overflow flag is possible but really painful as it requires doing bit operations on both the operands and the result, and I'm running out of registers. Also, it's not actually what I want here; I don't actually care about the overflow. I just want to do the comparison.

(I can't simply subtract and branch if the result is negative because that doesn't work in all cases. Consider INT_MIN < INT_MAX. That's 0x8000 - 0x7fff = 1, which is obviously positive.)


Solution

  • Considering the accepted answer and its comments, I would reconsider Jester's advice (which seems to me only +4B when compared to proposed Z80 code, but with somewhat different code layout, i.e. where less/greater_equal branches reside, which may further complicate or simplify your code ... plus it should perform better than doing xor 0x80 every time to both D and H):

        mov     a,d
        xra     h
        jp      sameSigns   ; as "JNS" in 8086 / "jp p," in Z80
        ; sign bits are different, signed overflow may happen
        ; but if H positive, then DE is less than HL
        xra     d           ; make A=H and set sign flag
        jm      DeIsGreaterEqualThanHl
    :DeIsLessThanHl
        ; DE < HL
        ...
    
    :sameSigns
        ; sign bits are equal, it is safe to do ordinary sub
        mov     a,e
        sub     l
        mov     a,d
        sbb     h
        jc      DeIsLessThanHl
    :DeIsGreaterEqualThanHl
        ; DE >= HL
        ...
    

    You can also modify it as procedure which returns CF=1 when DE<HL by doing:

    :SignedCmpDeHl
        mov     a,d
        xra     h
        jp      sameSigns   ; as "JNS" in 8086 / "jp p," in Z80
        ; sign bits are different, signed overflow may happen
        ; but if H positive, then DE is less than HL
        xra     d           ; make A=H and set sign flag (CF=0)
        rm                  ; return CF=0 when DE >= HL (H is negative)
        stc
        ret                 ; return CF=1 when DE < HL (H is positive/zero)
    :sameSigns
        ; sign bits are equal, it is safe to do ordinary sub
        mov     a,e
        sub     l
        mov     a,d
        sbb     h
        ret                 ; return with CF=1 when DE < HL (CF=0 DE >= HL)
    

    BTW you can turn CF=0/1 into A=0/~0 by sbb a - sometimes that 0/255 is handy for further calculations...

    But as I commented under question, many time this is worth of revisit on architectural level, to see if the whole code logic can't be turned into unsigned 0..FFFF mode of operation, maybe it would lead to adjusting (by -32768) values like "_left" just at one/two specific spots (like final output to user), while many more other internal comparisons/usages would work in unsigned way.

    EDIT:

    Some variants for compares against constants (for one-time constant it's probably better (size-wise) to just load it into other RP and use the generic RP1 vs RP2 compare, especially if you have spare RP and the generic compare is already instantiated for other code ... but for multiple uses of the same constant this will probably win both size-wise and speed-wise ... inlining wins speed-wise? May get on par with subroutine, depends how the result is used).

    reg-pair (actually also any 8b reg) against zero:

    ; signed compare 8b or 16b register vs 0, into SF, destroys A
        xra     a       ; A=0
        ora     R       ; 16b R=[HDB], or any 8b R: SF = (RP < 0 or R < 0)
        ...i.e. "jm hlIsLessThanZero"
    
    ; signed compare 8b or 16b register vs 0, into CF, destroys A
        mov     a,R     ; 16b R=[HDB], or any 8b R
        ral             ; CF = (RP < 0) or (R < 0)
        ...i.e. "jc hlIsLessThanZero" or "sbb a" to get 0/255
    

    reg-pair against any 16b #XY constant:

    ; signed 16b compare RP (HL/DE/BC) vs nonzero constant #XY
    ; subroutine, returns CF=1 if RP < #XY, modifies A
        mov     a,R
        xri     0x80    ; convert 8000..7FFF into 0000..FFFF
        cpi     #X^0x80 ; "X" is xor-ed with 0x80 too to have it in 0000..FFFF range
        rnz             ; if ZF=0, then CF=1 is (RP < XY) and CF=0 is (RP > XY)
        ; R == X, the low 8b P vs Y will decide
        mov     a,P
        cpi     #Y      ; CF=1 if (RP < XY)
        ret             ; 10B for particular #XY constant and RP
    
    ; inlined form
        mov     a,R
        xri     0x80    ; convert 8000..7FFF into 0000..FFFF
        cpi     #X^0x80 ; "X" is xor-ed with 0x80 too to have it in 0000..FFFF range
        jnz     HiByteWasDecisive   ; if ZF=0, then CF is set correctly, done
        mov     a,P     ; R == #X, the low 8b P vs #Y will decide final CF
        cpi     #Y      ; CF=1 if (RP < #XY)
    :HiByteWasDecisive
        ; CF=1 is (RP < #XY) and CF=0 is (RP >= #XY)
        ...