Speed Optimization (Michael Kunstelj / Lorenzo Micheletto)
PIPELINING IN INTEL PROCESSORS
The 386 is a ''partially pipelined'' processor with some optimizations. It checks for jumps ( A check) in a quite slow way and most of times can execute every stage in one cycle. It cannot perform the address generation check (B check) so it must execute stage 3 and 4 in ''not pipelined mode''. This explains why THE FASTEST 386 INSTRUCTIONS TAKE TWO CYCLES ( stage 3 unit must wait stage 4 to complete before managing
the next instructions).
The 486 has an improved EXECUTION unit (stage 4), improved memory access AND its control unit can perform the address generation check so that it can pipe stage 3 and 4 if there are no register/memory conflicts with two successive instructions.
This explains why lots of 486 instructions take just one cycle (if the pipeline is filled and no pipeline stalls are produced). What''s more, pipeline reloading is improved, so even jumps have less impact on execution time.
It also has a 4Kbyte internal cache that boosts memory access and allows big gains from strip-mining optimizations. Another nice thing is the BURST READ access that accelerates memory reads when it needs to update its internal cache.
PIPELINED,SUPERSCALAR & BRANCH PREDITING CPU: THE PENTIUM
Well, what''s beyound a fully pipelined processor like the 486 ? There is a 586/Pentium processor, that''s SUPER SCALAR (more than one pipeline) and BRANCH PREDICTING (it has a specialized unit that annotates the position and the result of some og the most recent jumps, and so it can try to ''guess'' from where the next instruction after the jump execution will be loaded, if it guess correcly a pipeline stall is avoided, else it stalls)(this helps speeding up the execution of nested loops).
The 586 can execute UP TO TWO integer operations in a single step (because it has TWO pipelines, but with some limitations) it can BURST READ/WRITE and has TWO indipendent 8k caches (Harvard style CPU to cache architecture) this means you can strip-mine to the max. if strips fit into the 8Kbyte data cache.
Well, now you know how they work, let''s see how to make ''em work to the max.
SPEEDING UP 486/586 CODE
ADDRESS GENERATION STALLS (AGI)
An Address Generation Interlock occurs when a register which is currently being used as the base or an index was the destination component of a previous instruction (the stage 3 to 4 pipeline stall i described above)
For example,:
add edx, 4
// AGI, ONE CLOCK STALL
mov esi D§edx
For the 486, AGI''s can only occur on adjacent instructions.
On the 586 or Pentium instructions up to 3 locations away can cause an AGI. ( i.e. an instruction waiting to execute step 4 on pipeline 1 may need to wait the result produced by an instruction still executing on pipeline 2)
pipeline step pipe1 pipe2
addres_calculate/fetch_operand C D |
execute B A |
V
If C needs the results of A, it must stall pipeline 1 for one cycle to wait for A to ''exit from'' pipeline 2.
If C needs the results of D, it must stall pipeline 1 for two cycles to wait for D to ''exit from'' pipeline 2.
An example of 3 instructions away AGI:
add esi, 4
pop ebx
dec ebx
mov edx D§esi
Takes 4 clocks on a 486. On a 586, the move command must wait for the add command, thus AGI stalling for one clock. The code above then would run in three clock cycles on a 586.
Remember that even instructions that read or write implicitly to registers cause AGI stalls:
mov esp,ebp
pop ebp.
is executed as...
mov esp,ebp
[agi] ; pop uses the esp as a pointer
pop ebp
INSTRUCTION DECODING STALLS
When decoding an instruction with an IMMEDIATE VALUE AND either an INDEX OFFSET or an IMMEDIATE DISPLACEMENT 486''s have a one clock penalty. (586''s do not)
Example:
mov spam 248
mov D§esp+4 1
This happens because the decode stage of the pipeline can read and decode ONE ''immediate'' value at a time while an immediate value and an immediate offset are TWO immediate values. The 586 instead has not such problems (when decoding) (execution is different)
REGISTER ACCESS CPU STALLS
486''s have a 1 clock penalty when modifying the lower word of a DWORD register that''s used immediately after it, 586''s do not.
Example:
mov al,0
mov D§ebp eax
(a one clock penalty on 486, no penalty on a 586)
CODE ALIGNMENT
To speed up the instructions, alignment of code should be on the beginning of cache boundaries. (32 bytes on 586, 16 bytes on 486)
Thus, start your routine on the start of the cache page. This way, when someone calls the routine, the processor will just have to load a cache line to get the first sequence to feed into the pipeline, and while they are executing it will have enough time to load the other cache lines needed.
This will speed up the processing of all the instructions within that page. The effect of this speed up on the 586 is not a dramatic as is it is on the 486 because the 586 has bigger caches and faster access to memory so a cache line load/store has less impact on cpu.
DATA ALIGNMENT
Misaligned access in the data cache causes an extra 3 cycles on both the 486 and 586.
Ways to speed up data:
For DWORD data, alignment should be on a 4-byte boundary.
For WORD data, alignment should be on a 2 byte boundary for the 486, and simply within the 4-byte page for the 586.
For 8 byte data (64 bits), data should be aligned on a 8-byte boundary so it is possible to use BURST READS (and burst writes too, on a Pentium).
And yes, on many applications with tight inner loops, these things do accumulate to make a noticeable speed-up.
SPEEDING UP REGISTER AND OPERAND USAGE
Use the EAX as much as possible. Many instructions are 1 byte shorter when using EAX. That''s less code you have to move around and slightly less internal processing. Use the DS register as much as possible for roughly the same reason, In the case of several references being made to a variable addressed with a displacement, load the displacement into a register.
Try to use the ESP register to reference the stack in lower levels of your subroutines (faster than push/pop things and leaves EBP free for other uses)
HANDY INFO ON SPEEDING UP INTEGER INSTRUCTIONS
1. Avoid using complex instructions like LEAVE, ENTER, LOOP, string instructions etc Simpler instructions will get the job done faster.Simpler instructions have been getting faster with every new processor that has come along.
2. With 8-bit operands, do your best to use the byte opcodes, rather than using the 32 bit instructions with sign and zero extended modes.Internal sign extension is expensive, and speed increases can be found by simplifying the process as much as possible.
3. LEA''s generally increase the chance of AGI''s. However, LEA''s can be advantageous because:
In many cases an LEA instruction may be used to replace constant multiply instructions. (a sequence of LEA, add and shift for example)
LEA may be used as a three/four operand addition instruction. LEA ECX D§EAX+EBX*4+ARRAY_NAME Can be advantageous to avoid copying a register when both operands to an ADD are being used after the ADD as LEA need not overwrite its operands.
The general rule is that the ''generic''
LEA A D§B+C*INDEX+DISPLACEMENT
where A can be a register or a memory location and B,C are registers
and INDEX=1,2,4,8
and DISPLACEMENT = 0 ... 4*1024*1024*1024
or (if performing signed int operations)
-2*1024*1024*1024 ... + (2*1024*1024*1024 -1 )
replaces the ''generic'' worst-case sequence
MOV X,C ; X is a ''dummy'' register
MOV A,B
MUL X,INDEX ;actually SHL X, (log2(INDEX))
ADD A,DISPLACEMENT
ADD A,X
So using LEA you can actually ''pack'' up to FIVE instructions into one Even counting a ''worst case'' of TWO OR THREE AGIs caused by the LEA this is very fast compared to ''normal'' code. What''s more, cpu registers are precious, and using LEA you don''t need a dummy ''X'' register to preserve the value of B and C.
4. Zero-Extension with short ints terror tales
The MOVZX takes four cycles to execute due to due zero-extension wobblies. A better way to load a byte into a register is by:
xor eax,eax
mov al,memory
As the xor just clears the top parts of EAX, the xor may be placed on the OUTSIDE of a loop that uses just byte values. The 586 shows greater response to such actions.
It is recommended that 16 bit data be accessed with the MOVSX and MOVZX if you cannot place the XOR on the outside of the loop.
N.B. Do the ''replacement'' only for movsx/zx inside loops.
5. When comparing a value in a register with 0, use the TEST command.
TEST operands by ANDing the operands together without spending any internal time worrying about a destination register. Use test when comparing the result of a boolean AND command with an immediate constant for equality or inequality if the register is EAX. You can also use it for zero testing. (i.e. test ebx,ebx sets the zero flag if ebx is zero)
6. Address Calculations
Pull address calculations into load and store instructions. Memory reference instructions have 4 operands: a relocatable time segment base, a base register, a scaled index register and a displacement. Often several integer instructions can be eliminated by fully using the operands of memory addresses. (more on this later)
7. INTEGER DIVIDE
In most cases, an Integer Divide is preceded by a CDQ instruction. This is as divide instructions use EDX:EAX as the dividend and CDQ sets up EDX. It is better to copy EAX into EDX, then arithmetic-right-shift EDX 31 places to sign extend. The copy/shift instructions take the same number of clocks as CDQ, however, on 586''s allows two other instructions to execute at the same time. If you know the value is a positive, use XOR EDX,EDX.
8. INTEGER MULTIPLY
The integer multiply by an immediate can usually be replaced with a faster and simpler series of shifts, subs, adds and lea''s.
As a rule of thumb when 6 or fewer bits are set in the binary representation of the constant, it is better to look at other ways of multiplying and not use INTEGER MULTIPLY. (the thumb value is 8 on a 586)
A simple way to do it is to shift and add for each bit set, or use LEA.
Here the LEA instruction comes in as major cpu booster, for example:
LEA ECX D§EDX*2 ; multiply EDX by 2 and store result into ECX
LEA ECX D§EDX+EDX*2 ; multiply EDX by 3 and store result into ECX
LEA ECX D§EDX*4 ; multiply EDX by 4 and store result into ECX
LEA ECX D§EDX+EDX*4 ; multiply EDX by 5 and store result into ECX
LEA ECX D§EDX*8 ; multiply EDX by 8 and store result into ECX
LEA ECX D§EDX+EDX*9 ; multiply EDX by 9 and store result into ECX
And you can combine leas too!!!!
lea ecx D§edx+edx*2 ;
lea ecx D§ecx+ecx*8 ; ecx <-- edx*27
(of course, if you can, put three instructions between the two LEA so even on Pentiums, no AGIs will be produced).
9. Clearing Registers
Using XOR reg,reg is fast but sets up conditions codes. A slightly slower way to do it of course is to mov reg,0 which preserves condition codes.
10. Avoid ENTER, instead, try something like:
PUSH EBP
mov ebp, esp
sub esp, BYTE_COUNT
11. JUMP INSTRUCTIONS
Jump instruction come in two forms, one real near that jumps between -127 and 128 of the current location, and a 32 bit version that does a full jump. The short form is quite a bit faster, however unfortunately many compilers put long jumps where short jumps would suffice. To ensure that short jumps can be used (when you know it is possible), explicitly specify the destination as being byte length (i.e use jmp short instead of plain jmp, if you can)
12. Task Switching
Task Switching is evil. It is slow, real slow. Avoid task switching too often, as more and more of your time will be spent in processing the task switch.
For faster task switching, perform you task switching in software. This allows a smaller processor state to be saved and restored. Anything that shaves time off 75+ clock cycles isn''t all that bad in my book.
13. Minimize segment register loads and the use of far pointers as dearly much as you can. If you are doing a lot of processing on a small piece of data far away, it might be faster just to copy the data to nearby and work on it there (by the way, this is a good reason to use flat protected mode).
...and....
14. THINK ABOUT WHAT YOU WANT TO DO
All the other optimizations of Unrolling Loops, moving the invariant data etc still apply. That, however, is more an algorithmic problem for you, but still keep it firmly in mind.
586 Specific Optimizations
The 586Pent has a five stage pipeline structure. However, the pipeline is split into two different pipelines, known as Pipeline U and Pipeline V. This allows two instructions to be executed in parallel and thus presents the opportunity of executing two/instuctions per clock. The U pipeline is very much like the 486 pipeline, in that it can handle the entire set of instructions. Pipeline V on the other hand can handle only ''simple'' instructions. The fast parts of the U and V pipelines are possible as much of the functions are hardwired not microcoded.
Anyway, I''ve blurted on about that for a bit, but what you want to know is ''How to I get two instructions running in one clock/cycle?''
Lament no further, here are the criteria:
1. Both instructions must be simple (see below)
2. There must not be a read-after-write or write-after-read register/flag dependencies between them.
3. Neither can have a displacement and an immediate
4. Instructions with prefixes can only occurr in the U-pipe (except for branches on condition jz,jc, etc.etc.)
The following are considered ''simple'' instructions, the ALU means any ALU command (ADD etc):
1. Mov reg, reg/mem/immed
2. mov mem, reg/imm
3. ALU reg, reg/mem/immed
4. ALU mem, reg/immed
5. inc reg/mem
6. dec reg/mem
7. push reg/mem
8. pop reg
9. nop
10. lea reg/mem
11. Jmp/ Call / Jcc near
N.B Remember only the U pipe can perform SHIFTS/ROTATIONS so ''couple'' every shift/rol with a simple instruction before and after to be sure every pipeline is filled.
Note, however, than while both pipelines can perform ALU instructions concurrently, this is not the case when one ALU instruction requires the output of the other pipeline ALU instruction. Example: ALU instruction in pipe U and performing ADC in pipe V.
There are two exceptions to this rule:
1) In the case of a compare/branch combination.
2) With push/pop combinations (because of optimized stack access)
Branch Prediction
Another nice optimization with the 586 hardware is that whenever there is a possibility of a jump, for example, Jg, the 586 can automatically start ''reading ahead'' code from the jump location just in case the the Jump is accepted. Remember the CODE alignment thing above? Well, it couldn''t hurt your grandmother to align the labels on the code pages.
Amazing new 586 Optimizations and speedups
The 586 has a lot of really nice optimizations and kick-ass features in addition to what I''ve mentioned above, unfortunately, this information is proprietary and will only be given to people who sign non-disclosure agreements and shell out a $$ or two... Bummer... (Meethinks, 586 clone-makers won''t be too happy about that... :-) )
Compiler/OS makers will probably be the purchasers. Quite a few of these optimizations won''t work on anything less than a 586, so don''t sweat too much about it. Hey, just write for 386/486/586 and you''ll be ok.
Hovewer, i found a nice article on July 1994 Byte about some ''secret weapons of Pentium coders''....
PENTIUM''S PROFILING REGISTERS
Pentiums have a set of 64bit ''machine specific registers'' (MSR) that are accessed by way of the RDMSR (read MSR) and WRMSR (write MSR) instructions.
THESE ARE PROTECTED MODE, RING 0 INSTRUCTIONS (CPU LEVEL 0) ( can work in a 386Powered programs only if executing under VCPI XMS or ''no V86 manager'' or if you find a way to ''toggle'' DPMI from ring 3 to ring 0) (I think they can work even if you boot ms-dos in real mode
and use the proper instruction prefixes needed to execute 32bit instructions in real mode).
RDMSR Read MSR, Copies the MSR register indexed by ECX into the 64bit pair EDX:EAX
[RDMSR | db 0F 032]
WRMSR Write MSR, Copies into the MSR register indexed by ECX, the value contained into the 64bit pair EDX:EAX
Macro to insert it into code if your assembler does not support Pentiums (RosAsm does not need this):
[WRMSR | db 0F 030]
Intel Pentium user manuals, documents only MSR 0, 1 and 0Eh and states that MSR 3, 0Fh and above 13h are reserved and illegal.
So, what''s left? And what''s useful to you ?
MSR 10h is the counter of CPU CYCLES since last power-up.That value can also be read with the RDTSC (Read time stamp counter instruction) RDTSC is 0Fh,031h in bytes and must be executed while in ring 0 too.
MSR 10h is the most precise timer available on the Pentium because it ''ticks'' at the CPU frequency.
Then comes MSR 11h,12h and 13h there you will find the hot stuff
The lower 32bits of MSR 11h are actually two INDEXES INTO THE CPU PROFILING REGISTERS!!!!
The profiling registers are CPU EVENT TIMERS AND COUNTERS they can keep track of what''s going on inside and outside your super-duper processor, they can detect nearly everything the CPU does.
The first 16bits of MSR11h selects the profiling register accessible thru MSR 12h.The second 16bits of MSR11h selects the profiling register accessible thru MSR 13h.
Here comes the format of the ''profiling register indexes'':
bit 0..5 : type of the profiling register to access
bit 6 : Set if you want to monitor the events in cpu ring 0,1,2 (system)
bit 7 : Set if you want to monitor the events in cpu ring 3 (user level)
bit 8 : 0 = access count-of-hardware-events
1 = access count-of-total-cpu-cycles used to process the cumulated
events.
(i''m not sure of this, maybe 0 means count time and 1 count events)
bit 9..15: UNKNOWN, DO NOT MODIFY
Profiling registers types:
INDEX NAME
0 data write
1 data read
2 data TLB miss (translation look-aside buffer)
3 data read miss
4 data write miss
5 write (hit) to M or E state lines
6 data cache lines written back
7 data cache snoops
8 data cache snoop hits
9 memory accesses in both pipes
0Ah bank conflict
0Bh misaligned data memory reference
0Ch code read
0Dh code TLB miss
0Eh code cache miss
0Fh segment load
10h ????
11h ????
12h branches
13h BTB hits (Branch Target Buffer)
14h taken branch OR BTB hit
15h pipeline flushes
16h instructions executed
17h instructions executed in V-pipe
18h bus utilization (clocks)
19h pipeline stalled by write backup
1Ah pipeline stalled by data memory write
1Bh pipeline stalled by write to E or M line
1Ch locked bus cycles
1Dh i/o read or write cycles
1Eh non cacheable memory references
1Fh AGI (Address Generation Interlock)
20h ????
21h ????
22h FPU operations
23h breakpoint 0 match
24h breakpoint 1 match
25h breakpoint 2 match
26h breakpoint 3 match
27h hardware interrupts
28h data read or data write
29h data read miss or data write miss
So if you want to profile things:
0) Set properly the environment of the test (cache empty, cache filled with all the routine code, etc. etc.)
1) Set MSR 11h to the two counters you want to read
2) Read MSR 12h,13h to get the initial values,
3) Execute the code you want to profile
4) Read the final values of MSR 12h,13h (without setting MSR 11h again this time).
5) Calculate the difference between the initial and final values.
This way if you are not sure how to optimize things you can actually test how they work and what''s going on.
USING THE PROFILING REGISTER YOU CAN ''TEST ON THE ROAD'' YOUR CODE DOWN TO THE LAST CPU CYCLE!!!!!
386 Optimization
Well, nobody buys a 386 now, right? But still lots of people has one.... So if you wanna be 386 friendly remember .....
DECODE-AFTER-JUMP OVERHEAD
When a jump is performed, a 386 spends some extra time decoding the next instruction to execute and flushing the pipeline.
THE FIRST INSTRUCTION requires exactly ONE EXTRA CPU CYCLE for every COMPONENT ( prefixes, opcode, mod/rm , sib ,offset, immediate value ) of the instruction. Notice i said COMPONENT!!! An 8bit displacement or a 32bit one, count always as an 1 extra cycle.
So, in 32bit mode (where no prefixes are needed for 32bit instructions):
loopme: inc esi ; opcode
mov D§edi+01234 eax ; opcode, mod/rm , immediate offset
dec ecx ; opcode
jne short loopme ; opcode short_displacement
IS FASTER THAN:
loopme: mov D§edi+01234 eax
inc esi
dec ecx
jne short loopme
Because placing first the three component mov instruction adds 2 cycles to the instruction execution time, weird, uh? But if you remember this thing, you can boost 386 code a lot.
By the way, remember that ''pipeline overhead'' is not so obvious to calculate. Look at this:
add eax,ebx ; opcode, mod/rm
add eax,01234 ; opcode, immediate_value
stosd ; opcode <- these ''slow'' instruction pipes-in faster
pop eax ; opcode <- so if you can, put ''em first
SHORT INSTRUCTIONS
Short instructions are loaded and decoded faster than longer ones and since 386 has no internal cache and less memory access bandwidth than a plain 486, this helps a lot.
Well that''s all for a 386.
CACHE OPTIMIZATION TECHNIQUES (THEY CAN WORK ON ANY CPU!!!!!!!!!!!!)
Usually ''code optimization'' is cpu-based, but there more things up in the sky and down on earth than just a cpu... the cache for example!
Well, the difference between a cache hit and a cache miss means lots of cpu cycles lost, so better hit the cached locations to the max.
386 usually have and external 64k ... 128k cache (mine has none, sigh! :( )
486 have a 4k internal cache and a 64k...512k (usually 256k) second level cache
Pentiums have an Harvard 8k code + 8k data cache plus a 256k..1Mbyte second level cache.
Use ''short'' loops so there is more probability that the code to execute will reside fully into the cache.
Then remember you can count on an external cache of at least 64k (usually 128k..256k for a 486).
So, if you have to process big arrays or big bitmaps with multiple passages do not ''scan'' all the array for every pass, use a ''strip by strip'' strategy instead so every ''strip'' fully fits into the cache and is ready for the second and third pass without cache misses.
This technique is called STRIP-MINING, you can include into your program a routine that checks the optimal ''strip size'' trying a multi-pass test on 64k,128k,256k,512k strips and ''complete array'' and then sets the optimal ''strip size'' to use when perfoming ''strip-mining'' routines.
On a Pentium you can use 8k 'strips' so your 'strip mined' routines will work with the full speed of the internal data cache (remember the internal cache works at full cpu speed while the secondary cache may be runnin at half that).
The advantage of strip mining is caused by the fact that the additional jumping/looping needed to process arrays in ''strips'' of adiacent memory locations that can fit together into the cache is A LOT LESS than the time caused by a single cache miss.
NOT SO OBVIOUS OPTIMIZATIONS
COMPLEX INSTRUCTIONS
Intel says complex instruction are evil on 486 and up, but there are exceptions... expecially if want to make things run smooth on 386 too.
STRING instructions are FASTER on 386, expecially if they are the first in a loop.
If you have to move data in 32bit chunks you''d better use REP MOVSD if you can because it alone replaces this ''simple instructions only'' super-optimized sequence:
rep_loop:
mov eax D§esi]
add esi 4 ; or -4
mov D§edi eax
add edi 4 ; or -4
dec ecx
jne rep_loop
REP MOVSD takes 2+7*n cycles on a 386 or 486
While the ''optimized'' sequence uses EAX
and takes [(2+4+2+2+2+2+7)*n - 4 ] = [21*n - 4] cycles on a 386
and [ (1+1+1+1+1+3)*n - 2 ] = [ 8*n - 2] cycles on a 486
Cache-line aligning the ''optimized'' code on a Pentiumi think it takes [ 3*n ] cycles. So if 486s are your base system don''t throw away REP MOVSD
Also remember that REP MOVSD take 2 bytes instead of 13 bytes does not need to use EAX (one more register free for other things) and it does not use the Pentium''s BTB (Branch Target Buffer) so you can be sure it does not miss and the outer loops with entries in the BTB can be one level deeper.
What''s more, the AMD K5 automatically splits a complex instruction into an optimized sequence of simple instructions and issues them to fully utilize the four execution units it has.Guess what it means for poor old movsd :)
<Intel bashing ON>
Heck! I think those Intel engineers lost a big thing when they decided to not improve MOVS/LODS/STOS. A ''blitter'' unit directly controlled by string instructions (with ''fetch ahead'', ''bufferize'' and ''auto align'' capabilities) could be a great thing for us.
Think about this: a REP MOVSB/W/D ''translated'' to a ''head'' byte move (to align things) a ''central'' qword move (burst read/burst writes) and a ''tail'' byte move (to align things). And all this without trashing the processor data cache!!!! Can you immagine the boost your code may get? Heck! Sun engineers ADDED ''blitter'' support in their new 64bit sparc cpus because they seen their software moving data a lot, why Intel ones did not?
On a Pentium you can get the optimal 2-cycle memory to memory MOV by way of pipelining, but this cannot take advantage of the fact that when performing REP MOVS the processor KNOWS AHEAD how many locations will be moved (read: on a ''smarter'' cpu this can help optimize to the max the processor-to-memory bandwidth and avoid caching overhead) nor it can take advantage of the full power of burst read/writes neither it can take advantage of the fact that WHILE THE STRING MOVE IS GOING ON, the processor pipelines can execute OTHER instructions after the MOVS and ''not dealing'' with the memory range ''under transfert'' or with ECX,ESI,EDI and Direction Flag. I hope they will take note of this for P7.
<Intel bashing OFF>
THE ADDRESSING MODE ADVANTAGE
Don''t be fooled by the current Riscy trend, be cooler than the rest. Some Intel ''complex addressing'' modes are really powerful if you just have enough creativity. Lets suppose you need to add together data from ''four'' streams of data....
A riscy (risc-like) way to do this could be...
; 486 timings when looping
riscy_way:
mov eax D§esi ; 1
add esi 4 ; 1
add eax D§edx ; 2
add edx 4 ; 1
add eax D§ebx ; 2
add ebx,4 ; 1
add eax D§ecx ; 2
add ecx,4 ; 1
mov D§edi eax ; 1
add edi 4 ; 1
dec ebp ; 1
jne riscy_way ; 3
; loop cycles = 17
Now lets see the ''intelly'' way! :)
Let''s suppose the ''counter'' EBP won''t exceed 2^31 -1
we can do the following ...
; move pointers ahead ...
lea esi D§esi+ebp*4
lea edx D§edx+ebp*4
lea ebx D§ebx+ebp*4
lea ecx D§ecx+ebp*4
lea edi D§edi+ebp*4
neg ebp ; negate increment
; then you can fully use the address unit ALU
; 486 timing when looping
intelly_way:
mov eax D§esi+ebp*4 ; 1
add eax D§edx+ebp*4 ; 2
add eax D§ebx+ebp*4 ; 2
add eax D§ecx+ebp*4 ; 2
mov D§edi+ebp*4 eax ; 1
inc ebp ; 1
jnz intelly_way ; 3
; loop cycles = 12
On a Pentium, ''riscy'' and ''intelly'' runs at nearly the same speed BUT ON A 486, the ''riscy'' way runs 30% slower than ''intelly'' !!!!
This means that using the ''riscy'' code your 486 will look like a slow cow compared to a Pentium while using the ''intelly'' code your 486 will look good enough ( not to mention this helps to make the difference between ''needs a Pentium'' and ''this flies on 486 too!!'').
32bit ACCESS SPEAKS FOR ITSELF, just remember to fully use it
Everywhere you can, use 32bit instructions and if you can, align data on 32bit.
For example, let''s say you have to manipulate lots of strings, if you align them on dword boundaries (including string lenght) with null byte paddings, you can boost processing time MORE THAN 8 TIMES!!!!
The same thing applies to manipulating 8bit bitmaps and ''software'' sound mixing.
Into 386mixer coded a routine that mixed simultaneously 4 sound channels running only on internal register and mixing up to 4 samples at a time from the 4 channels (look at the ''intelly'' example above).
If you need speed, you can even tollerate small calculation errors or reduced precision and manipulate successive 8,16bit values in a single 32bit instruction.
Let''s say you have and array of unsigned words and you need to multiply them for a constant, say 45, how can you do that ? If you know the values won''t overflow the 16 bit they use (if they overflow you will have to choose another method), you can do the following:
mov ecx D§count
mov esi start_address
handle_two_words:
mov eax D§esi ; load two words at a time
; an AGI happens , but i can''t eliminate it
lea eax D§eax+eax*4 ; multiply by 5
add esi 4 ; increment pointer, avoid 486 AGI
; reduce by 1 the Pentium AGIs
lea eax D§eax+eax*8 ; then multiply by 9
dec ecx ; decrement counter & keep Pentium at full power
mov D§esi eax ;
jne handle_two_words
And if you have very big arrays, you can even unroll two or three times the loop to further speed up this code.
SELF COMPILED CODE
Sometimes you need to execute lots of times the same ''jump filled'' instruction sequence, and you know ahead what jumps will be taken and what won''t.
What''s worse if there are lots of conditional jumps (Jcc) ''in sequence'' they may be enough to overflow the capability of branch-prediction units!!!
So, what can you do? My answer to this is a SELF-COMPILER!!! A small finite state machine that instead of crunching data directly IT GENERATES SUPER-SPECIALIZED ROUTINES that will crunch the data in the fastest way the code generator knows.
I''ve done a thing like that for the _PageFLip1 routine in 386video.asm. At mode initialization a code generator generates all the 256 blit-skip sequences the _PageFLip1 routines may need when copying ''modified'' pixels on screen.
This way a call is performed only after 32 blitted pixels, instead of jumping every 2..4 pixels (or every pixels in the worst case situations).
By the way, this is NOT self-modifying code this is an ''integrated code compiler''.
I hope this information has been helpful for you.
Now make some coffee, brush your teeth and phone up your girlfriend and go and see a movie. This document will be here when you get back, and I imagine there is only so much of this you can take in one day.... :-) :-) :-)
Live long and code well...
Regards,
Michael Kunstelj.
Regards from me too,
Lorenzo Micheletto
Speed Optimization -Introduction- (Michael Kunstelj / Lorenzo Micheletto)
(Note from Betov: I include this paper without the Authors agreement, because I have not been able to get in touch with them. Betov@free.fr)
Brief intro about how a CPU works
Now a brief intro on the innards Intel CPUs.
Most processors these days have within in them a system termed a ''''pipeline''''. The 486 / 586 are certainly within this category. However, most people aren''t quite sure what exactly a pipeline is, here follows a complete explanation:
The execution of a machine code instruction can be roughtly split in five stages: [n.b. this is a generic pipeline]
1) FETCH instruction opcode and immediate data
2) DECODE opcode and align immediata data into temporary registers
3) CALCULATE ADDRESS OF OPERANDS and load ''em into memory.
(calculate the address of memory locations to access
and select the register operands to use)
(sometimes called DECODE 2)
4) EXECUTE instruction (loading memory operands if needed)
& write results to INTERNAL registers
5) WRITEBACK memory-operand results to memory
Every stage takes ONE OR MORE cpu cycles to be completed, but usually a modern cpu needs just one cpu cycle for every execution stage (excluding stages 1 and 5 that have to deal with memory/cache access
and stage 4 when ''''complex'''' microcoded instructions are executed).
A cpu-core ( the ''''real'''' cpu, not cache support nor the other things that are usually integrated into a modern cpu) has five ''''specialized'''' units, one for every stage of instruction execution, plus a ''''register file''''
(and ultra-fast on-chip memory where internal register values are stored) and a CONTROL UNIT.
The control unit ''''coordinates'''' the action of all the other units so they can work together.
Here follows an extended ascii drawing of ONE on the many ways these units can be connected:
MEMORY INPUT (''''memory load'''' unit)
¦ +------------+
+------->+------------+<-------->¦ ¦
+++ ¦ FETCH ¦ ¦ ¦
+---------------------+¦+>+---------------+ ¦ ¦
¦ (instruction pointer)¦ ¦ ¦ ¦
¦ ¦ +------------+<-+ ¦ ¦
¦ ¦ ¦ DECODE ¦<-------->¦ ¦
¦ ¦ +---------------+ ¦ ¦
¦ ¦ ¦ ¦ ¦
¦ ¦ +------------+<-+ ¦ CONTROL ¦
¦ ++ ¦ ADDRESS C. ¦<-------->¦ ¦
¦ +----+-->+---------------+ ¦ UNIT ¦
¦ ¦ ++ ¦ ¦ ¦
+------------------------+ +->+------------+<-+ ¦ ¦
¦ REGISTER FILE +----->¦ EXECUTE ¦<-------->¦ ¦
+------------------------+<---------------------+ ¦ ¦
¦ ¦ ¦
+------------+<-+ ¦ ¦
¦ WRITEBACK ¦<-------->¦ ¦
+---------------+ ¦ ¦
¦ +------------+
MEMORY OUTPUT <---+
(''''memory store'''' unit)
If you look the drawing, the control unit needs to communicate with all the other units to make ''em work together.
Usually the ''''control unit'''' is scattered in little units that coordinates intruction and data flow between adiacent units, but you can think at it as a single indipendent unit.
The five execution units are what gives the ''''raw'''' cpu power but the control unit is what lets you fully utilize ''em.
Let''s suppose every unit performs one operation in one step.
With a ''''cheap'''' control unit, to execute a complete machine language instruction you first enable FETCH, then you enable DECODE and so on until WRITEBACK is completed and the control unit enables FETCH again to execute the next instruction.
This is what happens into an ''''absolutely not pipelined'''' cpu with ''''one cycle'''' stages, the fastest instructions takes 5 cycles but the control unit is very simple (just a circular shift register that enables one unit at a time).
Of course this is the worst case, nobody is so jerk to build a cpu like that.
Every cpu stage-execution unit is a stand alone thing, it has its hardware and its ''''temporary registers'''' so it is capable to operate ''''alone''''.
So, IF THE CONTROL UNIT can SINCHRONIZE the operations of all the stage-execution units, it can make ''em WORK IN PIPELINE (like in a factory, where every worker/robot in a production line a) receives a partially refined product from the previous worker in line b) performs some simple operations to refine the product a little more c) pass the result to the next worker in line). A cpu with such a control unit is called a pipelined CPU.
If you have to execute in sequence instructions A,B,C,D,E on a pipelined processor ....
While the WRITEBACK unit is performing stage 5 of A
the EXECUTION unit can perform stage 4 of B
the ADDRESS CALCULATE unit can perform stage 3 of C
the DECODE unit can perform stage 2 of D
and the FETCH unit can perform stage 1 of E
So if you start the cpu at instruction A it looks like that instruction A takes 5 cycles while instructions B,C,D,E (immediately one stage behind) looks like they take JUST ONE CYCLE!!!
So if you execute a SEQUENCE of 8 instructions on a ''''not pipelined'''' processor with a five stage ''''pipe'''' it takes 40 steps to execute the sequence while on a pipelined processor this takes 5+7=12 steps!!! ( 5 steps for the first instruction to get thru the pipeline then 7 steps for the other instructions immediately ''''one step'''' behind) And the more instructions are executed in sequence, the more it looks like every instruction takes ''''just'''' one cycle to execute.
This is of course the optimal situation, when the processor pipeline is filled and WHEN EVERY INSTRUCTION DOES NOT USE MEMORY/REGISTER OPERANDS ''''still under processing'''' IN SUCCESSIVE EXECUTION-STAGES!!!!!!
THINGS A CPU MUST CHECK TO MAKE A PIPELINE WORK CORRECTLY
A pipelined processor control-unit must ''''check'''' :
A) at stage 1 (FETCH)
IF in stage 2..4 there are JUMP instructions [ they change the program counter (the same register used by the fetch unit)]
THEN stage 1 must ''''wait'''' until the jump is completed and then ''''restarts'''', loading the memory location pointed by the new program counter value.
Because the jump opcode must pass thru the decode stage ...if the DECODE unit ''detects'' a jump opcode, it makes the FETCH unit ''stops'' AT LEAST for two cycles until the jump is performed.This of course means the processor pipeline ''wastes'' some cpu cycles.
A 486 handles jumps in a smarter way, when the FETCH unit loads a jump instruction, it goes on assuming the jump WON''T BE TAKEN (it read the instructions following the jump). If the jump is taken, the values in the DECODE 1 and DECODE 2 units are ''discarded'' and the FETCH unit starts loading from the new
program counter value. This explains why on 486 relative jumps (the fastest jumps) takes 3 cycles if taken (two cycles ''lost'') 1 cycle if not taken (no cycles lost)
A ''smarter'' control unit can recognize in advance unconditional jumps and start immediately loading opcodes from the new program counter location AND/OR try to ''predict'' the jump destination of conditional jumps (like the Pentium does).
B) at step 3 (ADDRESS CALCULATION)
IF in stage 3 a register needed for address calculation is under processing in stage 4 (EXECUTE)
THEN the stage 3 unit generates a ''do nothing'' control sequence for one cpu cycle.
C) memory access conflicts
These can be caused by lots of different things, they are usually resolved by the memory load/store units.
Told in a short way, in a fully pipelined processor when the pipeline is ''working on a sequence'' the first instruction in the sequence takes the full execution time while the other instructions takes a shorter time (usually one cycle) because they are immediately behind, this means they ''look faster'' (and in fact they are, because the pipeline fully utilizes all the functional units it has).
If some instructions ''modify'' values needed in the previous cpu stages the control unit stops the unit needing those values (and the others behind it) and inserts ''do nothing'' codes into the successive units in the processor pipe to make things work as expected (this is called a PIPELINE STALL or a PIPELINE FLUSH)
This explains why usually a taken jump takes more time than one that''s not taken (when a jump is performed you must flush the pipeline).
The newer processors includes JUMP PREDICTION UNITS that handles jump faster, but the less you jump, the better.
Low Level Assembly vs High Level Assembly ..
For many older programmers, like me, 'true' assembly is Low Level Assembly, this is to say Assembly written in a style that does not make any use of advanced organizations like Procedures, If and friends, While, and so on. You can see bright examples of this style with all Test Department Demos. Not only does he make no use of any HLL style statement, but he does not even make use of any macro facilitated call for Api calls for example, and pushes the Parameters, one by one, in the reverse required order. When you look at his Source, you see more or less what a Disassembler would output from his EXE.
When I stated above 'For many older programmers, like me, ...', I would have better said that this was my position several years ago.
Maintaining long sources written in Low Level assembly is very painful, if not simply impossible. High level Assembly dramatically increases the Readability. As readability is the most important thing in writing, the choice is easy to make:
A source is not good when it only runs fine. A source is good only when it is highly readable.
High Level Assembly dramatically eases developing, debugging and maintaining.
High Level Statements do not (or very, very few...) decrease the Code Performance, which in any case, is of zero interest on actual modern Processors.
So, this is the only evident practical way to go. With an Assembler like RosAsm, with which all HLL Macros are located at the programmers' level (and not nested, the easy way, inside the Compiler itself, unless you explicitly ask for a PREPARSE Pre-Parser, and so forth, choose to not write true Assembly), there is no hurt and no shame.
You still control everything at any moment and define by yourself whatever more or less High a level you want, so that I do not see any possible counter-argument. As long as we are in the Bottom-Up Language construction logic, we are in the better, if not the best possible, programming world.
~~~~~~~
The Code scope ...
Code scope is what I call the distance evaluation between two different locations of Code. Not only the physical distance, at a Code Octets quantity point of view, but also at a human logical point of view. This concept is of major importance for Source organization, but it is never discussed, mainly because it is close to impossible to clearly describe.
Though, the logical and physical scope is the very true basis for Sources organizing.
All older programmers, pefectly know when to cut a chunk of Code into smaller Routines, and how to do it. But the truth is that this is the result of years of experience. Describing this is even more difficult, because this interacts with another problem, which is to know under what form the separations will be done: Sub-Routines / Procedures / Macros... So let us see the different points one after another:
The Code length
There is no real consideration to give to the length of a Code Chunk, per se. If the organization is simple, mono-purpose, even if the Chunk is hundreds of Lines long, and if there is no reason for you to cut it in smaller pieces, do not do it. This is a very exceptional case.
Most often, even if there is no logical requirement for dividing a Chunk of code into Parts, natural Blocks appear to the eye, that can then be broken into as many called sub-Routines as you might desire.
For example, if we have a long flow of Cases selections ('If' and friends), and that the treatments of the cases achieve in several screens of Source, the calls of each treatment through an independent Routine (that will be called from this given point only) is then accurate. This will have several advantages:
1) The whole Cases selections will hold in fewer screens, so the overview will be much better.
2) The independent Routines will be easier to maintain and develop.
3) Pointing out the Routine by Search features will be made easier.
4) If later, along your developments, one given sub-Routine reveals to be possibly called from another Source location, it will be there, ready for reuse, at no or low adaptation cost.
Out of the simplest cases, long chunks of Source are painful to maintain. Not only because of the screen moves required for reading it, but also because of the overall logical events, that you will have to understand and master again, at each modification.
The logical organization
The 'Pretty' theory of programming regularly recommeds to build an organization Table (representation) of our Applications before we start writing anything. In the real world, this does not make any sense. When you begin a new Project, usually, you have not the beginning of an idea of what it will look like when finished. And if you have any, you can be sure that this idea will reveal itself mostly wrong, in the end.
Defining the overall structure of a Source, before writing it, is nevertheless possible, but very uncommon. This happens, for example, to professional programmers who always do the same kind of work, with a very high level of specific experience.
In normal conditions, the overall organization of a Source appears by itself all along the developments. The natural attitude with this (and the proper way to go), is to keep our mind continuously open to structuring and re-structuring. That is, re-write. Re-writing is not so killing as the reward is great. As soon as a development becomes painful, this is because of a lack of logical organization.
While writing, a good rule to be applied is that, each time a Block of Code can be given some Name, this is the time to make it an independent Routine. The time eaten by the call and by the ret counts for nop, and there is no reason for not doing it.
If you can name it? Well, do it! Isn't it easy enough to cut and paste, define a Label Name and add a ret?
The Module concept
Historically, Modular programing has been the very first step that has driven the languages evolution to the disaster state it actually is. This concept is tightly bound to Code-Reuse and to multi-Files Management of the Sources, and results in what everybody can see everyday: Slow and fat Applications, difficult to maintain, difficult to debug and... unreadable Sources.
Nevertheless, the Module concept basic definition is interesting and may be really useful in Mono-File and specific programing. We can define a 'Module' as:
'A Module is a Set of Routines having a strong relationship between themselves and a low relationship with the other parts of the Application'.
As it is, this concept, derived from the Multi-File programing and of the Code-reuse procedural approach -which are the real hell- is very simple to understand and provides a very effective rule for the Sources logical scope definition.
The Forms
As a whole, we have the choice between 3 forms of Sources organizations: Sub-Routine, Procedure, Macro.
When to use each one and why?
As opposed to what you can see in many actual Demo Sources, the Procedure is not the default way for organizing Blocks of Code. Procedure -as opposed to simple sub-Routines- implement a Stack frame for Parameters and, eventually for Local Variables, Registers preservations and Local Tables. There is no reason to use a Procedure if you do not send any Parameter to it. More than this, sending Parameters on the Stack, to a Procedure, is not the only way to go, in Assembly. Often times, the direct definition of a couple of Registers before (and/or after) the call is much, faster and smaller. Procedures should be reserved for reusable chunks designed to be called from several locations, with different Parameters. This is a very interesting technique but it is not so often accurate.
The sub-Routine is the natural assembly Source organization unit. While the Procedures end with some EndP Macros restoring the Stack, the sub-Routines end with a simple ret. How to define the Parameters and elements used by a sub-Routine depends on what and when. The simple way is to set Registers to the expected Values or Pointers. As soon as these Registers attributions become difficult, for example, because of the required numbers of information, or because of lack of flexibility, this is time to use the second sub-Routines way: Variables in Memory, that the caller will set up, and that the Routine will use (or/and reverse). Despite its rigid organization, a Routine can be called from several locations of one Source. Nevertheless, if a sub-Routine really expects to be called from several points, it is much better to use a Procedure, in order to avoid development difficulties. Procedures are more flexible and secure than sub-Routines. Another great way for introducing flexibility in the sub-Routine technique, is to perform the Routine call through a Macro that holds the Parameters (in and/or out) adjustment. The usage is then made close as powerful and flexible as the one of Procedures.
The Macros concept introduces a choice problem: When to choose the Macro way? When to choose the Routine or Procedure way? We have to consider the simplicity and power of the Macro Evocation, vs the Macro increase of Code length. Each Macro Evocation, inserts the Macro Code in the dead File. Macros should be reserved for small and often used Block of instructions that, eventually, require some powerful level of flexibility. So said, macros should then not be considered as a real Sources organization technique, but, rather, as a simple way to High Level Assembly.
For very wide Code Scope, both logical and physical, the Procedure is the organization of choice.
For short logical scope and wide physical scope, Routines are the way.
For very wide logical scope, very short physical scope, and very small Blocks of Code, Macros are great.
~~~~~~~
The Spaghetti Style ..
This nomenclature refers to Sources performing wild jmps, backward and forward, every now and then, by authors who want to promote the structured programming style. It is always introduced as something you should never do.
Completely avoiding this style may be a good recommendation to beginners, but, for the Assembly Language, the spaghetti style is quite natural, as we are completely free to jump from any Instruction to any other one, and / or to play at will with the calls, jumps stack managements.
So, let us first see what is wrong with this style. The following example is taken from the RosAsm Source. This comes from one of the very first Routines I ever wrote for the RosAsm Assembler, years ago. At that time, I had had no experience with 32 Bit programming, and I applied the rules of programing I had retained from my A86 DOS days. Today, I would never write anything like this, but, I leave this Routine as is, inside the RosAsm Source, as a relic of my own writing evolution.
This Source abstract is 100% unreadable, as you can see, but it does a lot of very complex parsing of an asm text Source, with as few operations as possible. In some way we could say that it is optimized at a logical level :)). It is a pure example of what we should never do:
L3: cmp B$edi-1 NoSpaceAfterThis | jb L9>>
L4: cmp al LowSigns | ja L7>
mov B$InsideWinEqu &FALSE
L4: cmp al, CommaSign | jne L5>
cmp B$esi ',' | je L9>>
mov al, Space | jmp L8>
L5: cmp al, EOI | jne L7>
L6: cmp B$edi-1 Separators | jb L9>>
jmp L8>
L7: On W$edi e 0209, dec edi
cmp al Openbracket | jne L8>
cmp B$edi-1 EOI | jne L8>
cmp B$edi-2 Closebracket | jne L8>
dec edi | dec D$StripLen
L8: inc D$StripLen | stosb
L9: On esi b ecx, jmp L1<<
Nevertheless, in some cases, the spaghetti style may be accurate:
..Else
L6: movsb | cmp B$esi '|' | je L7>
cmp B$esi 13 | jne L6<
L7: mov al 13 | stosb | mov al 10 | stosb | mov al ' ' | stosb
..End_If
To perform the same computing with a better structured formulation, we could propose something like:
..Else
movsb
While B$esi <> '|'
If B$esi <> 13
movsb
Else
Exit_While
End_If
End_While
mov al 13 | stosb | mov al 10 | stosb | mov al ' ' | stosb
..End_If
As you may guess, the second version would be a little bit slower, because of the extra jmps involved by the HLL Macros, but, frankly, not that much more readable, is it? I even have some doubts it could be considered less readable...
For the Assembly Language, jmps are the natural way for routing the Code flow. Stating that we open the door to Spaghetti style as soon as we make use of wild jmps is both right and wrong: I think the good definition should state that we really open that bad door as soon as we make use of nested and crossed jmps, in cases when we could or should make use of calls and rets.
The general rule is that spaghetti style should be avoided, but a bit of this style is not a crime against purism. The only relevant criterium is readability. As soon as your hard branchings lead you to something you have pain to read, stop it. Under this limit, there is no shame, as long as it is simple, readable and very short scope.
This style is also particularly useful for implemeting very flexible calls to Routines flow organizations. Four examples:
Example 1
WriteSignedImm32:
mov ebx D$esi | add esi 4 | jmp L0>
WriteSignedImm16:
movsx ebx W$esi | add esi 2 | jmp L0>
WriteSignedImm8:
movsx ebx B$esi | inc esi
L0: push 0-1
.....
In this example, the same chunk of Code, the one after L0, is accessed from 3 different entry points. The very stupid way would be to re-write entirely 3 different Routines. Another way would be of making a separate sub-Routine begining at L0, which would be called from the 3 variants, which, in turn would be ended by a ret. But what for??? Isn't it simpler this way???
Example 2
The second example is quite simple: In all complex computations, you need error(s) management. As the Routines required for showing the error to the user will not be re-written as many times as error cases and locations, you will have, usually, only one error holding, for a wide range of Routines and sub-Routines. When the errors can occur at any depth level of your Application Routines' organization, you need something to restore the Stack Pointer, what ever Routines-calls depth the error happened. This is quite simple to do the wild way:
before any critical run, you save the Stack Pointer:
mov D$OldStackPointer esp
Then at the Entry Point of the Main Error Routine, something like:
mov esp D$OldStackPointer
Though it could be discussed, I range this technique inside the Spaghetti Style category because it breaks with the clean usual organization of calls and rets.
Example 3
The third example is something we should avoid as much as possible (but I used it several times inside RosAsm Source, because of its simplicity):
Say, we are in some FirstRoutine (call from somewhere else). Then, from inside this FirstRoutine, we call another one (SecondRoutine), that will ask something of the final user, and return either &TRUE or &FALSE. In the second case (&FALSE), we want to abort the whole process. That is, the proper way is to implement, inside FirstRoutine, some conditional Code to exit properly. If there is nothing else to do before leaving, I sometimes do this, inside SecondRoutine:
pop eax | ret
This is to say that the return Address (the one to FirstRoutine) is stripped off and that we return immediately to the Caller of FirstRoutine (the caller of the caller). No intermediate ret, no success Flag, no nothing. This technique is pretty bad because the source is later more difficult to maintain, and a later error introduction much more difficult to point out. Nevertheless, good in very simple cases...
Example 4
The fourth example is the routing I used in the RosAsm Disassembler. This feature is quite simple and linear, as each analysis of the Code Bytes simply routes the flow to a Routine for outputting the Source. Each time it was possible, I wrote this flow one way. This is to say that, instead of calling for some specific treatement Routine, I stated a jmp to it, so that multiple nested actions Routines are run in turn with only one final ret to the very first caller. This technique is completely accurate for very simple organization of Code flow. Though, it fully falls in the Spaghetti style definition, as it effectively makes use of jmps, in circumstances where, usually, we should make use of calls. This last example could be used as a demonstration that the Spaghetti style may be good and accurate, even if we have to firmly recommend that beginners should be careful to keep away from this.
~~~~~~~
Readability ..
Let us first look at two different versions of the same Routine:
Version 1:
ShowEquate:
mov ebx, eax
mov edi ShwEquHexa
L0:
cmp esi edx
jb L1>
movsb
jmp L0<
L1:
mov eax ' ='
stosd
mov eax ' '
mov ecx 3
rep stosd
push edi
std
mov ecx, 9
L1:
If ecx e 5
cmp ecx 5
jne L2>
mov al '_'
stosb
L2:
cmp ecx 1
jne L2>
mov al '_'
stosb
L2:
mov al bl
and al 0F
add al, '0'
cmp al '9'
jne L2>
add al 7
L2:
stosb
shr ebx, 4
loop L1<
cld
pop edi
inc edi
mov eax ' h'
stosd
mov al 0
stosb
push 01000
push ShwEquTitle
push ShwEquHex
push 0
call 'USER32.MessageBoxA'
ret
Version 2:
ShowEquate:
mov ebx eax
mov edi ShowEquateHexa
While esi < edx
movsb
End_While
mov eax ' =' | stosd | mov eax ' ', ecx 3 | rep stosd
push edi
std
mov ecx, 9
L1: If ecx = 5
mov al '_' | stosb
End_If
If ecx = 1
mov al '_' | stosb
End_If
mov al bl | and al 0F | add al, '0' | On al a '9', add al 7
stosb | shr ebx, 4 | loop L1<
cld
pop edi
inc edi
mov eax ' h' | stosd | mov al 0 | stosb
call 'USER32.MessageBoxA' 0, ShowEquateHexa, ShowEquateTitle, &MB_SYSTEMMODAL
ret
Indentations
Several x86 Instructions are to be 'paired', because they are particularly dangerous. This especially is the case for the PUSH / POP and for the STD / CLD pairs. Indenting Source in between these instructions pairs eases a lot the maintainence and prevents a lot of errors, like jumping out of the indented Instructions chunk.
Indenting HLL-like statements is, of course, now, an evident 'must-have'.
With a fixed font 16/8, like the ones in RosAsm Source Editor, the good indentation is 4 spaces long. 2 space indentations are too short for making it pretty, and 8 spaces indentations, when having to indent, for example many HLL Cases Levels, may go too far out of the screen width.
This 4 space indentation fits perfectly, too, with the use of RosAsm Local Labels (L0:...), because they leave just one space between the colon and the first Statement leading char.
Blank Lines
You may consider blank lines as some kind of wishable vertical indentations of your Sources.
Having all the Source of one given Routine entirely printed on the screen is such an ease for reading, that setting blank lines might seem a high cost. It is really not, because blank lines help a lot at holding the overall organization whereas losing a couple of screen lines is not a problem. Anyway, the fact of having a whole Routine on one single screen is not a good criteria for Source organization (only the building logic is).
Multi-Instuction Lines
The fact of writing several instructions on one single Line increases readability in two ways:
This feature allows having more Source Instructions on one screen, and so it is much easier to take an overall look at what a Routine does, with less painful moving.
This feature decreases the readability of the flow of instructions grouped upon the same line and, at the same time increases the readability of the overall action(s). No! I am not joking with you: When you read a Source, you cannot -and need not- be fully aware of all the tiny details of each little instruction. Sometimes, a set of Asm instructions may be considered as a sentence (and the inward instruction as the words of a sentence. For example, in:
mov eax ' h' | stosd | mov al 0 | stosb
We do not care of what is exactly going on inside this line. This is just to finish the writing (writes the ending 'h' char and the zero ending char). We could as well replace this by a Macro, we would name ''CloseHexa'', and the information retrieved at reading time would be about the same. So, decreasing the readability of little interest instructions increases, a lot, the readability of important statements and of the overall code organization.
There is no light when there is no shadow.
HLL Macros
HLL Macros do not -or very few- decrease the running speed of an Application. There is no reason to be afraid of not doing true Low Level Assembly while using them. Needless to say, they dramaticaly increase the readability.
Multi-Instruction Macros
What I call so is, for example,
[mov | mov #1 #2 | #+2]
mov eax D$Value, ecx 3, edx 0 | div ecx | mov D$Value eax
The details of the mov instruction are of zero interest to the reader. Every Asm programmer perfectly knows how to make an integer division. So, the mov edx 0, for example, falls into the same logic of desirable decrease of readability, I describe, in the upper Multi-Instruction Lines paragraph. All this line is nothing more than a full english sentence that would say ''Divide this Value by 3''. You do not need to raise the low meaning details up.
Jumps and Labels positions
In:
L1: If ecx = 5
mov al '_' | stosb
End_If
If ecx = 1
mov al '_' | stosb
End_If
mov al bl | and al 0F | add al, '0' | On al a '9', add al 7
stosb | shr ebx, 4 | loop L1<
... the fact of having the jumping instructions at the end of lines makes it much easier to hold the Low Level organizations of Code. Local Labels Declarations should never be in another place than first row. The readability increasing on this point is very important because bad jumps are an easy to make error in Asm, and these errors may be difficult to point out or find.
Standard Registers use
The x86 Registers are now a bit less specifically designed than they were in the good old days. Nevertheless, they still have usages, particularly devoted to each one:
eax : Scratch Register (most general purpose)
ebx : Base Register
ecx : Counter Register
... and so on.
Even in case when you could make use of any Register to perform a non specific operation, you will increase by a lot the readability by using the standard ones. Example, any quantity of Items should be preferably stored in ecx, even if it is not to be used in conjunction with any loop or rep Instruction. Later, when re-reading this ecx, in your source, you will more easier guess that this is a Counter, than if the same count is stored inside, say, edi. In the same manner, Esi and edi, should be preferred as Source and Destination Pointers Registers, even if you do not intend to perform any String Instruction, and so on.
This is of course not an obligation, but, if you can do it, why not do it the more standard way.
Registers vs Variables
The only clear thing about when to use a Variable instead of a Register is that the use of Variables produces much more readable sources than Registers, even when the Registers are used the more standard way. As opposed to this, Registers produce faster Code than Variables, but this is most often not a valid reason for abusing of Registers' use. Keeping a Register alive across several Routines and Procedures may quickly become a true hell, whereas, the rather low speed cost coming with a nicely named Variable contributes a lot to the Source Readability on long scopes, and is much easier to maintain and to develop.
The general rule I try to apply, in my own writing, is that the use of Registers should have a very short scope, that is, inside a Routine or Procedure. When this is not wishable -example, several Routines operating on the same Data Area(s)-, I do my best for using the Registers the standard way - usually esi and edi, for Source and Destination and ecx for Counter -.
I say 'I', here, because there is no conventional formulation under a fixed rule and the choices depend on several things: The context, the personal tastes, the experience. Nevertheless, abuse of Registers and abuse of Variables are evidently both bad and wrong: The first one, at a readability point of view, the second one, at the Code efficiency point of view.
Namings
We should avoid use of C-Like Names like 'ptr', 'sz', and so on. Instead, taking some time to write full talking names, that have only advantages:
Easier to read.
Easier to paste from Source to Source (much less naming conflicts).
Needs much less in Comments. With full talking names, a Source may be considered Auto-Commented.
How to write full talking Names is not only a matter of personal taste. The used typo may contribute (or not) to readability:
mov ecx D$totalnumberoflines
... is not very readable, because the components of the talking name are not visible, and worse if its in upper case:
mov ecx D$TOTALNUMBEROFLINES
Much more readable is:
.
mov ecx D$TotalNumberOfLines
We could think, too, of separating the full talking name components with dash lines:
mov ecx D$_total_number_of_lines
... But this way is bad too, because, at first sight, our eyes are unable to hold the different components of the whole instruction. Though, dash lines may be very useful for numbers writing:
mov eax 0_FFFF_0FFF
mov eax 1_645
mov eax 00_11010000_11111111_00011010_00000000
Do not use names that talk for saying nothing:
Third_Routine: ; Yes !!! I actually saw this in a source!!!
Avoiding optimizations
Both Size and speed optimizations tips and tricks hardly kill readability. As these tricks are most often no use on actual Computer, simply write what you think. If you want to turn ebx zero, do not write XOR ebx ebx, this is ridiculous. Write mov ebx 0. Or, even better, if that zero is not really a value, at the human meaning point of view, prefer the full talking forms: mov ebx &FALSE | mov ebx &NULL...
In some (rare) cases, optimizations tricks may be accurate. Example, for dividing / multiplying by a 2n Value, the Shifting Instructions are much simpler to write, much faster to run, much shorter in Code Size, require zero additional Register use (a lot of good points for one so simple a trick...), and are, finally, not more difficult to read.
Comments
Do not comment the instructions. Instead, comment what the program is doing.
Writing a few use comments after instructions should be avoided. If you are not able to understand what an instruction does in one given context, when re-reading, you can, of course, do it, but, as a general rule, the better way is to write full comments lines at the top of the Routine, or at the top of an Instructions Chunk. This way it is much easier to maintain, too.
It is most often better to comment a Block of Instructions than only one Instruction, as instructions in the program flow are more like words in a sentence than stand alone sentences. When we do not understand the action of one particular Instruction, most often, this is because we do not understand the overall purpose of the group of Instructions it belongs to.
Comment your critical Symbols at the Declarations
Often times, when reading an old Source of yours, or alien Sources, you will not understand a Chunk of Code because you will not understand what the Symbols are or hold. What do you intend to do, in such cases? Simple: Right-Click upon the mysterious Symbol... Unfortunately, there is no Comment at the Declaration! Too bad!... At least, each Table Declaration should have a clear Comment saying what it is supposed to hold and how the Data are organized inside.
Re-read your Comments from time to time
Inside our own Sources, once they are running fine, we all tend to not re-read our Comments. Very often, after months of development and successive modifications, the comments are no longer accurate. This is particulary killing for readers, and never forget that, one year later, you will be nothing more than a flat reader of your own writings... Worse than no comment at all: The wrong comment!!!
~~~~~~~
Forcing the Disassembler Flags .
After the Disassembler has produced a Source, you may find some wrong Interpretations, that are, most often, failure cases of the Automatic Recognitions of what is Code and of what is Data. The RosAsm Disassembler offers a bit of interactivity, through the Forced Map File, that is saved after each Disassembly. When you see, in the Source, say:
Code040102E: E6:
push ebp
mov ebp esp
... if you double-Left Click upon Code040102E , the Float Menu will come out with one more Option, saying [Bad Disassembly], the selection of which will open a Dialog. This Dialog can be used for forcing the Disssembly to your own interpretations, to some extent.
Note: This feature is new (V.2.022a). Do not expect miracles...
Limitations 1: Once you have Compiled a Disassembled File, you cannot Open it again, and go on editing with the Forced Interpretations Dialog. To do so, you have to Open the original PE, each time, and to overwrite the Disassembled Source. So, it is useless to modify the disassembled Source by hand, as long as you mean to use this Dialog.
Limitations 2: For working with the Forced Map File, do not customize your Disassembled File Name: If you Disassemble, say, Application.exe, let it be the default MyApplication.exe. The Mechanism for assuming the new Name is not yet implemented, other than for the default.
The Map concept
RosAsm Disassembler uses several Tables (Maps), that are the same size as the PE, the Bytes of which are filled with Flags, all along the Disassembly Process, depending on the various Recognitions Methods. Each Byte, in the PE has its counter-parts, in the Map Tables:
The Sections Map: Each identified Location (Byte) is Flagged as Code or Data, or Import, and so on (The Forced Interpretation Dialog shows only the Code and Data Flags).
The Routing Map: Each identified Location is Flagged as Node, Label, Evocated, Exported Node, and so on... (The Forced Interpretation Dialog shows only a simplified version of these Flags, that match more with what the user can understand, than with what the Disassembler really does).
The Size Map: Each Data Byte Location is flagged accordingly to the Size of the Data. For example, if the Disassembly Code says some fld F$Address , this Address will be flagged FP4.
Forcing the Sections Map
This forced Interpretation, to Code or Data, works in all cases. Notice that you should play with this feature on a one-to-one basis, because, once you have forced one given Location to be interpreted your way, this modification may have impact on many other parts of the Disassembly, because the forced Recognition may have cascading effects, on the logical Flow of the Code Analyses..
Hit the [OK] button, after having forced one single Sections Recognition, and not re-edit previously forced Records.
Forcing the Routing map
This Option is used to define what the given Location is, at a logical Flow point of view. Usually, you should leave the [Label] Flag On, in order to follow-up with what the next Disassembly Process will do from your modification. Without Label, you could be lost, in the Source, whereas having a not used Label does not hurt.
Forcing the Size Map
As it says, this Option can be use to redefine the Sizes of Data. If the selected Item is a String or a Uncicode String, the Edit Control for defining the End of the String is then enabled. Its value is the one of the next Label not belonging to the String.
~~~~~~~
Code_Viewer ....
With the Disassembler implementation, I have added a little tool from the [Tools] Menu option, for viewing at once the results of the Assembler Encoder and of the Disassembly Job. This has revealved itself to be a useful tool for myself in order to verify both coding and decoding integrity.
As the Encoder and the Decoder are completely different things, using no table (only a basic one for the disassembler), as these two parts of RosAsm have been written at very different times, as I have used separate Intel Documention for each, and, finally, as I have verified that all of the Mnemonics are correct after Compilation and Disassembling, we can now consider RosAsm encodings as highly reliable, compared to other Assemblers, including for the newer SSE2-SSE3 Mnemonics that I have never experienced.
For users the Code-Viewer may be a good educational tool too.
You just input an assembly intruction in the EditBox. Then, the Assembler outputs the Hexa of the generated Code. When done (if no error...), the Disassembler computes these Bytes and outputs again the Instruction in text format.
The Value of the 'Data' default Pointer is meaningless and is set to zero. Don't confuse with zero dWords Values. I will improve this later. Often times, typing errors will be treated as unknown Data Symbol and will appear in the translations under the form of zeroed dWords.
Only one Instruction can be given at a time.
You can use it in two ways:
Input an instruction in the first Edit Box, and hit [Encode]. This will run the Assembler, show the Hexa Code, run the Disassembler and show again the Instruction ([Encode] includes [Decode].
Input Hexa Code in the second Edit Box and hit [Decode]. This only runs the Disassembler.
~~~~~~~
Code_Viewer ....
With the Disassembler implementation, I have added a little tool from the [Tools] Menu option, for viewing at once the results of the Assembler Encoder and of the Disassembly Job. This has revealved itself to be a useful tool for myself in order to verify both coding and decoding integrity.
As the Encoder and the Decoder are completely different things, using no table (only a basic one for the disassembler), as these two parts of RosAsm have been written at very different times, as I have used separate Intel Documention for each, and, finally, as I have verified that all of the Mnemonics are correct after Compilation and Disassembling, we can now consider RosAsm encodings as highly reliable, compared to other Assemblers, including for the newer SSE2-SSE3 Mnemonics that I have never experienced.
For users the Code-Viewer may be a good educational tool too.
You just input an assembly intruction in the EditBox. Then, the Assembler outputs the Hexa of the generated Code. When done (if no error...), the Disassembler computes these Bytes and outputs again the Instruction in text format.
The Value of the 'Data' default Pointer is meaningless and is set to zero. Don't confuse with zero dWords Values. I will improve this later. Often times, typing errors will be treated as unknown Data Symbol and will appear in the translations under the form of zeroed dWords.
Only one Instruction can be given at a time.
You can use it in two ways:
Input an instruction in the first Edit Box, and hit [Encode]. This will run the Assembler, show the Hexa Code, run the Disassembler and show again the Instruction ([Encode] includes [Decode].
Input Hexa Code in the second Edit Box and hit [Decode]. This only runs the Disassembler.
~~~~~~~