This is a follow-up to last week’s post, which discussed the fact that fully 30% of the 8086 processor’s 1-byte opcode space is given over to redundant opcodes (i.e. short forms of longer, more general opcodes). Today, I want to look, in a quick-and-dirty way, at how useful those opcodes might have been in practice. From at least one narrow perspective on a particular dataset, the vast majority of them seem to have been pretty worthless.
Why Short-Form Opcodes?
It’s worth considering what purpose short-form opcodes might serve. I can think of two advantages they might offer over their longer cousins:
- They are shorter, so their use would shrink executable sizes
- They might be executed more quickly
Today, executable size isn’t much of an issue, but it was a bigger deal in 1978, when main memory was measured in kilobytes and addressed in 64K segments, and software came on floppy disks. If a chip had an architecture that enabled the generation of executable images smaller than those built for its rivals, it might have a real advantage in the marketplace.
Special-purpose instructions might be faster to execute. The time required to fetch and decode different arrangements or encodings of instructions has always had an impact on performance (e.g. the u- and v-pipes on the first Pentium, the 4-1-1 rule on more modern Intel chips). Instruction fetch time, in particular, seems to have been a big bottleneck on the original 8086.
I’m a little too lazy to dig into the question of whether or not the short-form opcodes were used, in practice, to make software run a lot faster. Not only would that mean digging up 8086 instruction timings, it would require me to find, and possibly re-code, historical inner loops. It’s a nightmare, so I’m just going to look at the question of whether or not these opcodes make software much smaller.
I’m going to look, as an example, at the primary code segment of the Neuromancer executable, which I dumped at runtime. This is a hopelessly anecdotal way to look at things, but it produces some interesting results.
The 65536 bytes of Neuromancer’s primary code segment can be decoded into 26734 instructions, covering the first 65535 bytes (the last byte is ignored in this exercise). Of these instructions, 8389 contain redundant, short-form opcodes. If every opcode was replaced by its longer-form counterpart, these 26734 instructions would occupy 73924 bytes, an increase in size of approximately 13%, which seems non-negligible.
However, not every short-form opcode is used equally frequently. This chart shows the percentage of instructions (relative to all instructions in the code segment) containing each of the 18 most prevalent short-form opcodes.
As you can see,
PUSH AX) and
MOV AX, Imm16) are heavily used, and everything else seems relatively unimportant. In fact, if we eliminate all short-form opcodes other than
B8, the instructions in the code segment would only occupy 68880 bytes, an increase in size of approximately 5%. While still not inconsequential, a 5% reduction in executable size seems a poor return on an investment of 30% of your opcode space.