27

In comments to a parallel question (Why was IBM BASIC so Huge?) one point discussed is code density of 8 vs. 16 bit CPUs. Some assumptions were that 16-bit must be more bloaty due to its need for an address mode byte in each instruction, whereas 8-bit includes this in the opcode, while others claimed 8086 code to be quite compact.

While this reminds of the old CPU-wars, there must be some tradeoff, as the ROM sizes in some examples are quite close between x80 and x86.

15
  • 4
    And yes, it's a Holiday (6th of January) and I'm bored :))
    – Raffzahn
    Jan 6, 2020 at 19:45
  • 19
    In the USA it's our third day back at work since New Year's. I'll leave open the question as to which of us is more bored.
    – Tommy
    Jan 6, 2020 at 20:42
  • 2
    The benefit of living in a catholic tradition :)
    – Raffzahn
    Jan 6, 2020 at 21:11
  • 4
    Pointers! 16-bit values as addresses are common in any sophisticated program on an 8-bit architecture. Having written a bit of 6502 code, I can attest that handling pointers byte-by-byte is both painful and bloated.
    – RETRAC
    Jan 7, 2020 at 5:29
  • 3
    @Raffzahn Heilige Drei Könige? I miss my years in Bavaria Jan 7, 2020 at 7:36

4 Answers 4

39

[Preface: This is neither about discussing programming tricks nor how some changes could squeeze out a byte or two. Code can often be optimized by narrowing down the environment. The examples are meant rather for a generic estimation. ]

The question has already been asked in ways of 6502 vs. Z80 and PNDC provided a good answer pointing out that real code can always be seen as a form of variable length encoding preferring shorter codes for more often used instructions. He as well cites a quite nice paper about Code Density Concerns. Here various architectures, including Z80 and 8086 are treated in assembly to solve certain problems.

The paper puts x86 (*1), in all explicit examples, ahead of the Z80 (*2). So 16 bit seems like a clear winner in code size.

Except, it somehow feels wrong, doesn't it? I mean, it's easy to see that a MOV A,B in 8080 is a single byte (78h), while a similar MOV AL,CH in 8086 needs two bytes (88h C5h). So maybe some explicit example?

Lets take a routine like it's seen in many BASICs/OSes during startup - checking the available memory. One simple way to do is to see if bytes can be changed at will. There may be many machine specific ways to do so, this is a simple generic, checking every first byte of a 256 byte page of it can be written. It scans from some RAMSTRT to MAXRAM, whatever these values are on a specific system, and returns the highest page plus 1 when done.

(I'm using straight 8080 code for this examples. Z80 doesn't differ much (*3))

0000                RAMSTRT   EQU   0*1024   
0000                MAXRAM    EQU   48*1024   
0000                             ; 
0000   21 00 00               LXI   H,RAMSTRT ;FIRST PAGE
0003   06 C0                  MVI   B,(MAXRAM-RAMSTRT)/256   ;MAX PAGES TO CHECK
0005   7E           NPAGE:    MOV   A,M     ;GET BYTE
0006   2F                     CMA           ;COMPLEMENT
0007   77                     MOV   M,A     ;STORE IT
0008   BE                     CMP   M       ;DID IT GET STORED
0009   C2 13 00               JNZ   MSIZE   ;NO -> NOT RAM
000C   2F                     CMA           ;COMPLEMENT BACK
000D   77                     MOV   M,A     ;RESTORE BYTE
000E   24                     INR   H       ;NEXT PAGE
000F   05                     DCR   B       ;MAX TESTED?
0010   C2 05 00               JNZ   NPAGE   
0013                MSIZE:    ;H CONTAINS THE MAXIMUM PAGE NUMBER+1

Simple, straightforward and 19 Bytes in length.

The same, translated for 8086. After all the 8086 was made to make 8080 source translation simple by using tools like Trans86 or XLT86 (*4)

= 0000              RAMSTRT EQU      0*1024  
= C000              MAXRAM  EQU     48*1024

0100  BB 0000               MOV     BX,0    ;FIRST PAGE
0103  B1 C0                 MOV     CL,(MAXRAM-RAMSTRT)/256 ;MAX PAGES TO CHECK
0105  8A 07         NPAGE:  MOV     AL,[BX] ;GET BYTE
0107  F6 D0                 NOT     AL      ;COMPLEMENT
0109  88 07                 MOV     [BX],AL ;STORE IT
010B  3A 07                 CMP     AL,[BX] ;DID IT GET STORED
010D  75 0A                 JNZ     MSIZE   ;NO -> NOT RAM
010F  F6 D0                 NOT     AL      ;COMPLEMENT BACK
0111  88 07                 MOV     [BX],AL ;RESTORE BYTE
0113  FE C7                 INC     BH      ;NEXT PAGE
0115  FE C9                 DEC     CL      ;MAX TESTED?
0117  75 EC                 JNZ     NPAGE
0119                MSIZE:  ;BH CONTAINS THE MAXIMUM PAGE NUMBER+1

Same program, but 25 Bytes in length. That's an increase of ~31%.

And yes, the example is picked on purpose, as 8 of the 12 instructions are single byte on the 8080 side, which usually result in the worst case result of doubling code size. Except here it's offset already due no additional bytes used for R/M on the immediate loads and the short jumps the 8086 features. So already a straight translation without using any new feature or optimization produces acceptable results.

Now lets take something else, maybe were the x86 can use some of its advantage. For example swapping two memory blocks - a task which could happen for example in a simple multiprogram environment (*5).

0000                BLOCK1:   EQU   1000h   
0000                BLOCK2:   EQU   2000h   
0000   0E 00                  MVI   C,0     ;BLOCKLN = 256
0002   21 00 10               LXI   H,BLOCK1   
0005   11 00 20               LXI   D,BLOCK2   
0008   46           XLP:      MOV   B,M     ;LOAD BYTE FROM BLK1
0009   1A                     LDAX  D       ;LOAD BYTE FROM BLK2
000A   77                     MOV   M,A     ;STORE BYTE TO BLK1
000B   78                     MOV   A,B     ;"SWAP" TO A
000C   12                     STAX  D       ;STORE BYTE TO BLK2
000D   03                     INX   B       ;POINTER IN BLK1
000E   23                     INX   H       ;POINTER IN BLK2
000F   0D                     DCR   C   
0010   C2 08 00               JNZ   XLP     ;NOT DONE ->

19 bytes so far for the ability to swap 1..256 bytes between two memory regions without (*6,7). On the 8086 the new instructions simplify the task:

= 1000              BLOCK1 EQU   1000h
= 2000              BLOCK2 EQU   2000h

0100  B9 0080              MOV   CX,128  ;BLOCKLN
0103  BF 1000              MOV   DI,BLOCK1
0106  BE 2000              MOV   SI,BLOCK2
0109  FC                   CLD           ;DIRECTION UPWARD
010A  8A 05         XLP:   MOV   AL,[DI] ;LOAD BYTE FROM BLK1
010C  86 04                XCHG  AL,[SI] ;EXCHANGE TO BLK2
010E  AA                   STOSB         ;STORE BYTE AND INC PTR
010F  46                   INC   SI      ;POINTER IN BLK2
0110  E2 F8                LOOP  XLP     ;DEC COUNTER AND LOOP

18 Bytes, that's one byte or 6% less - plus the ability to swap up to 64 KiB at once.

So while the first example, made on purpose to let the 8086 look bad only resulted in 31% code bloat, the second did tune this. Both have been selected to be simple enough to show what it's about without hours of study. More complex examples will show that the 8086 will in next to all non trivial cases offer higher code density than an 8080 (or Z80).

The remarkable point here is 'greater ability' while singular instructions need slightly more encoding, the CPU offers a larger number of them.

Conclusion: For similar tasks, 8086 code density will play in the same range as 8 bit code - or better.

One point to keep in mind is that cross platform programs, like the mentioned BASIC, have not been rewritten throughout for each new CPU (*8), but just adapted. So 30% increase (30 KiB instead of 24) looks much like in the range of what an automatic translation could provide - even when ignoring the enhanced features provided by the 8086 version.


*1 - In form of i386.

*2 - For total length (Fig.2) the Z80 gains a slight advantage, but it's explained that this may be rather due platform specific issues.

*3 - Due the use of a prefix byte most enhanced Z80 instructions do not perform as well as one might assume - but that's a different story.

*4 - I didn't pull out either, just hand translated. It's simple enough.

*5 - This is inspired by a problem Alan Cox mentioned during Fuzix development.

*6 - This is on purpose not an example where the 8086 will excel due specific abilities, but something more common.

*7 - To be less hard, I did skip one of the most obvious advantage of the 8086, the ability to hold two 16 bit pointers and a 16 bit counter at the same time. Here it would allow to swap more than 256 bytes at once with the very same code - for the 8080 a few more instructions would have been necessary, as it can not hold 3 16 bit pointers plus two 8 bit values.

*8 - Microsoft BASIC 2 as used for the Apple and Commodore is a great example. Its development process seemed to be based on an emulation of 8080 instructions via macros and a step by step optimization thereafter.

1
  • Comments are not for extended discussion; this conversation has been moved to chat.
    – Chenmunka
    Jan 8, 2020 at 14:55
13

It's possible to write bloated code for any CPU. Reportedly one of the candidate operating systems for the Acorn Archimedes was dropped because it couldn't run sixteen simple tasks in 4MB RAM without swapping, while "Arthur" (which became RiscOS) was demonstrated to be able to do it in 256KB. With RAM prices at the time, that was a huge advantage. One reason was that Arthur was tightly coded in ARM assembly by people who knew what they were doing, while its rival was written in a Modula-2 variant with a particularly dumb compiler.

In general, 16-bit CPUs tend to have larger instruction words than their 8-bit ancestors, but it's possible to do more with each instruction. For example, it's much more likely that a 16-bit CPU will have built-in multiply and divide instructions, and will handle more conveniently sized integers with single operations, reducing the need to write and call software routines for those purposes.

The 8086 is actually exceptional in that it doesn't handle its full physical address space even as conveniently as the typical 8-bit CPU; rivals such as the 65816, 6809 and 68000 are at least no worse than their ancestors, and often better (the latter two being able to hold and manipulate full-size pointers in registers). Technically the 68000 should be classed as a 32-bit CPU, but machines based on it are generally counted in the "16-bit generation". But this means that code wanting to take full advantage of the 8086's memory space will be more bloated than average.

To illustrate this, let's take the memory-size-scanning benchmark from an earlier answer, and rewrite it for the 65816, probably the simplest 16-bit CPU of any note, with full 24-bit addressing support:

PTR=$80
ScanRAM:
 ; select 8-bit accumulator, 16-bit index registers
 SEP #$20
 REP #$10
 ; initialise pointer in zero page
 STZ PTR
 STZ PTR+1
 LDA #RAMSTART ; high 8 bits of start address
 STA PTR+2
 LDX ##MAXRAM-RAMSTART
 LDY ##0
scanloop:
 LDA [PTR],Y
 EOR #$FF
 STA [PTR],Y ; write complement and see if it stuck
 CMP [PTR],Y
 BNE notram
 EOR #$FF
 STA [PTR],Y ; restore original RAM contents
 INY
 BNE scanloop
 INC PTR+2 ; increment high byte of address
 DEX
 BNE scanloop
notram:
 STY PTR
 RTS ; 24-bit PTR now contains address of first non-RAM byte

If we compare this with 8-bit 65C02 code capable of operating in its full 16-bit address space, we find that we only need to drop the initial SEP and REP instructions (2 bytes each), the LDX and LDY instructions get one byte smaller each because they are loading 8-bit constants instead of 16-bit ones, and one of the early STZ instructions is deleted (2 bytes). The only other changes needed do not change the size or existence of any instruction.

So the 65816 code for a 24-bit address space takes 43 bytes, and the 65C02 code for a 16-bit address space takes 35 bytes; the 65816 code is 23% larger.

But if we say that 16-bit address support is sufficient for both CPUs, we can instead write the following for the 65816:

ScanRAM:
 ; select 8-bit accumulator, 16-bit index registers
 SEP #$20
 REP #$10
 LDY ##RAMSTART
 LDX ##MAXRAM-RAMSTART  ; full 16-bit addresses
scanloop:
 LDA $0000,Y ; absolute indexed, no zero-page reference
 EOR #$FF
 STA $0000,Y ; write complement and see if it stuck
 CMP $0000,Y
 BNE notram
 EOR #$FF
 STA $0000,Y ; restore original RAM contents
 INY
 DEX
 BNE scanloop
notram:
 RTS ; 16-bit Y register now contains address of first non-RAM byte

Here some of the instructions actually get bigger, but some other instructions are deleted. The total code size is now 33 bytes, smaller than the 8-bit 65C02 equivalent, and considerably faster due to the use of more efficient addressing modes.

1
  • 4
    re the Archimedes, its very tight hand-coded OS written by the ARM architect has, unfortunately, meant that it's virtually impossible to port to any other processor. It's even hard to run it on later ARM processors. Perhaps a case of Knuth's premature optimization: RISC OS became a curiosity, with few of its good ideas in mainstream use.
    – scruss
    Jan 7, 2020 at 17:08
9

one point discussed is code density of 8 vs. 16 bit CPUs.

At least Sun Microsystems recommended to write 32-bit applications for 64-bit versions of Solaris unless 64-bit integer operations are used and/or there is the need of more than 4 GiB of memory.

However, the reason was the data size (each pointer variable requires 8 instead of 4 bytes), not the code size (which is nearly the same in 32- and 64-bit code for Sparc CPUs).

Some assumption was that 16 bit must be more bloaty due its need for an address mode byte in each instruction ...

MC68HC11 is a 16-bit CPU; it is binary compatible to the 8-bit MC6800 - so instructions that already existed for the 6800 have the same length.

And even some 16-bit instructions (like ADDD) have the same length as the corresponding 8-bit instruction (unless immediate data is used, of course).

The same is true for the Hitachi 6301 which adds - according to Wikipedia - even (undocumented) 32-bit instructions to the MC6800 instruction set...

On the other hand, you can design an 8-bit CPU with an additional addressing byte (for example an MC6809-compatible microcontroller without the D register instructions).

So the reason for the longer code is not the word size (8- vs. 16 bits), but the additional addressing features.

... while others claimed 8086 code to be quite compact.

"Compact" compared to ...

  • ... the 68000,
  • ... the Z80 with 64 KiB memory or
  • ... the Z80 addressing more memory using bank switching?

In this context you have to see the overhead needed to address more than 64 KiB of memory.

The following code:

push ax
mov ax, XXXX
mov es, ax
pop ax
add ax, es:[YYYY]

Would look like this if you use the 8086 in a computer that has only 64 KiB of memory:

add ax, [YYYY]

And in the case of BASIC there are even additional commands (DEF SEG) which are required due to the segment registers.

6
  • 1
    Compact as defined in the title: 8080 vs. 8086. But then again seams to be similar for all 3 examples given. Check PNDC's link. Having done a few 8086 assembler lines by now, I can assure you the segment overhead is rather small and in fact another reason why x86 code is more compact than the same for 68k. Last but not least, why using such a complicated way to load a segment register via AX when a simple MOV ES,XXXX can do it as well? Doing any banking on a Z80 will require more instructions.
    – Raffzahn
    Jan 7, 2020 at 0:05
  • 1
    @Raffzahn Unfortunately, you cannot load segment registers directly (mov es, XXXX) on x86 CPUs. And my counterquestion was: What exactly did the people write who "claimed 8086 code to be quite compact"? Did they just say: "The code of the 8086 is quite compact" or did they say: "The code of the 8086 is quite compact compared to 8080/Z80/6502/... code"? Jan 7, 2020 at 6:14
  • @MartinRosenau They probably wrote "it's quite compact compared to its main competitor, the 68000"
    – JeremyP
    Jan 7, 2020 at 9:47
  • @MartinRosenau Of course can a segment register be loaded directly from memory. For example 8E46 will load ES from a stack frame. And for you 'counterquestion' it would be best if you read all cited material.
    – Raffzahn
    Jan 7, 2020 at 15:00
  • 2
    I would not count the 68HC11 as a 16-bit CPU, only if you squint very hard from a long distance. Everything with it is 8 bit. The 68HC12 was the "true" 16-bit version with an internal and external 16 bit bus. It doesn't really change your answer, but as a fan of both the HC11 and HC12 I couldn't stand idly by. :)
    – pipe
    Jan 7, 2020 at 17:29
1

Depends on what your code is doing. The amount of code (in bytes) needed to do heavy duty numerics (linear equation solvers, FFTs, etc.) would need to be much larger on a 6502 or 8080 than on an 8086+8087, or even a modern 64-bit ARM, due to more types of instructions and a larger register set.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .