[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.