https://pushbx.org/ecm/doc/insref.htm is a corrected + improved version of the appendix of the NASM manual that documents the CPU required for each form of each instruction, along with English descriptions of them. e.g. that
shl r/m8, imm8 was new in , with  only having shift by 1 or by CL. Current versions of the NASM manual stripped out the English descriptions of instructions as it got too long with new SIMD instructions.
The simple way is to just put the count in
cl and use
shl al, cl. Or for counts of
2, just repeat shift-by-1 twice. Maybe even for a count of
3, although 3x
shl al,1 (2 bytes each) costs more code-size than mov/shl (4 bytes total) so could be slower depending on surrounding instructions. (Variable-count shifts are very slow on 8086, though, so slow that the prefetch buffer will fill up.)
shl al, 6 has room for being clever, like maybe rotate right by 2 and mask, instead of shifting left by 6. (SAL and SHL are the same instruction. Many people prefer to always write SHL regardless of the signedness of the operand.)
imul for constants. They're very slow on 8086/8088, and a constant can be broken down into its set bits like
x*40 = x*32 + x*8 = (x*4 + x)*8, which you do with shifts and adds. Or
x*16 - x. Never use
imul for small power-of-2 constants like
4, that's what shifts are for. (Even if you need to widen the result to 16 or 32-bit, prefer shifts for powers of 2.) If you do use a hardware multiply, use
mul r8 unless you need the high half of the result to be signed.
imul is slower on 8086.
Shift-and-add is what compilers (like GCC) will do with
-m32 -mtune=i386 even when it takes much more than 2 instructions, although you'd need to stop GCC from using 32-bit LEA addressing modes for its shifts and adds if you want it to help you figure out a good sequence of shifts and adds. Perhaps AVR GCC, since AVR also only has 1-bit shifts. For example on Godbolt multiplying by 40 using
((x<<2)+x) << 3. Or by 14 using
((x<<3)-x) << 1. (For 8086, you can decide when it's worthwhile to put a count into
cl instead of repeating shift instructions, since that's an option AVR doesn't have.)
Fun fact: 8086 does have an immediate multiply,
AAD imm8 which does
AX = (AL + AH*imm8) & 0xFF. But it's not fast (60 cycles), and some non-Intel CPUs ignore the immediate and assume it's 10.
If you're optimizing for performance, that's often the same thing as optimizing for code-size on 8088, since code-fetch is the major bottleneck. (1 byte per 4 cycles, half the bus-width of 8086.) 8088's prefetch queue is 4 bytes, vs. 8086's being 6 bytes.
The prefetch queue is discarded on a taken branch (there's no branch prediction), so that's probably part of why they made it shorter on 8088. (But it still takes 4 fetches to fill it, vs. 3 on 8086.)
Supercat's point that counting memory accesses (including code fetch) is usually the best way to predict performance on 8088, except around very slow instructions like
mul that would let the prefetch buffer fill and leave memory untouched for many cycles.
This is what makes x86's 1-byte
xchg ax, reg useful sometimes instead of
mov ax, reg or
mov reg, ax, if you just need a value somewhere else and don't need to keep a copy. And optimizing to have values in AX or AL for the short-form encodings like 2-byte
add al, imm8. And
inc cx is 1 byte, vs. 2 bytes for
inc cl, so use 16-bit
inc when possible even if you only care about the low 8 bits.
https://www2.math.uni-wuppertal.de/~fpf/Uebungen/GdR-SS02/opcode_i.html instruction timings for 8088 through Pentium, but not including code-fetch costs. i.e. cycles when the instruction is already prefetched.
mov reg, imm8 -
4 cycles (But it's a 2 byte instruction so it costs 8 cycles of code-fetch.)
shl reg, 1 -
2 cycles (code size = 2 bytes)
shl reg, cl -
8 + 4*count cycles. (code size 2 bytes)
mul r8 - 70 to 77 vs.
imul r8 80-98
mul r16 - 118-133 vs.
imul r16 128-154
- (So if you don't need the high-half result, use
imul. The product bits out to the width of the inputs, not overlapping with any sign or zero-extension bits, is the same for mul and imul.)
Increasing Efficiency of binary -> gray code for 8086 on Stack Overflow, putting some of this into practice for the int->hex part of the question.
Shifting by more than half a register
mov cl, 6 and a slow
shl al, cl will give the prefetch buffer time to fill (up to 4 bytes) while the shift is running. But the shift costs
8+4*6 = 32 cycles, time for 8 bytes of memory fetches. After refilling the prefetch buffer (4 fetches taking 16 clock cycles), that leaves another 4 fetch cycles wasted. So the prefetch buffer was only able to hide half the cost of the shift.
If you're counting in memory bytes, count that
shl al,cl as 2 bytes (to fetch itself) plus 4 bytes of lost/wasted fetch time, assuming later instructions are fast and not a branch, so they consume those 4 bytes of prefetch before more cycles are wasted. So the total cost of
mov cl,6 /
shl al,cl is equivalent to at least 8 bytes of instruction fetch, if it started with the buffer empty and usefully ended with it full.
; emulate shl al, 6
and al, 3 ; 2 bytes, 4 cycles
ror al, 1 ; 2 bytes, 2 cycles
ror al, 1 ; 2 bytes, 2 cycles
(Or rotate first and end with
and al, 0xc0, i.e.
(0xff<<6) & 0xff. I don't think this will matter, since even the slower instruction is still as fast as a memory access. Even if this is preceded by slow instructions so it starts with the prefetch buffer full, it will leave the prefetch buffer empty when it finishes. Unless AND-immediate takes more cycles before it frees space in the prefetch buffer, in case it was full when this started decoding.)
This is more actual code size than
mov cl,6 /
shl al, cl but faster even considering the prefetch buffer: only its actual 6 code-fetch bytes, no lost code-fetch cycles of prefetch-buffer overflow.
I also considered
mov ah, al +
mov al, 0 / 2x
shr ax, 1. Or maybe
xchg ah, al if AH had already been zero. But rotate is better if you don't have a use for the
x >> 2 value in AH.
The same analysis method applies for
mov cl, 40 /
mul cl vs. multiple
add instructions, but
mul cl is so slow you have a lot more room for more instructions while still coming out ahead. (70-16)/4 is 13.5, and that's not counting the 4 bytes for mov+mul themselves. Even moreso for 16-bit multiplies which take a minimum of 118 cycles. (118-16)/4 is 25.5, and you'd need 3-byte
mov dx, 40 to set up for it. DX is a good temporary since
mul r16 produces a result in DX:AX.)
For 16-bit shifts by 8 or more, start with
mov ah, al /
mov al, 0 for left shifts.
Xor-zeroing isn't particularly useful for 8-bit registers: same code size unlike for 16-bit registers.
mov al, 0 is 4 cycles vs. 3 for
xor al,al on 8086, though, so it could matter if the prefetch buffer might overflow due to slow instructions before and after.
xor ah,ah is slower than
mov ah,0 on some modern CPUs (like Intel Haswell and later). Of course,
xor ax, ax is better than
mov ax, 0, saving a byte of code-size, so always use it for 16-bit registers. (Modern CPUs don't special-case it as a zeroing idiom though, for registers narrower than 32-bit.)
Intel P6 family (Pentium Pro through Nehalem) did handle
xor ax,ax as a 16-bit zeroing idiom. uops.info throughput test results for Core 2 show
xor r8w,r8w averaging 0.33 cycles per instruction, vs. 1 cycle for
xor r8w,r9w (bottlenecked on latency; later tests with multiple independent XORs in the loop show it can run them at 3/clock as well). (Despite Core 2's front-end being 4 uops wide, it still needed a back-end uop to actually write a zero to a register, unlike on Sandbybridge-family where zeroing idioms are eliminated in the issue/rename stage.)
P6 renamed the low 16-bit register separately from the full register. Unlike first-gen Sandybridge which renamed 8-bit (but not 16-bit) partial registers, and later Sandybridge which only renames AH/BH/CH/DH separately from the full registers.
If you want a shifted copy of a register, you might start with
xor dx,dx /
mov dh, al /
shl dx,1 to do
dx = ax<<9. In that case, xor-zeroing does indeed work well, at least as good as
mov dl, 0 /
mov dh, al on most CPUs, and better on P6-family. (Writing DH and then shifting DX still causes a partial-register stall on P6-family CPUs, though.)
If all else is equal in terms of 8086, you might want to consider modern CPUs that might also run your code when choosing instructions.