---------------------------------------------- Machinecode optimizing by John A. Toebes VIII. Translated and extended by John van Dijk from the German magazine Amiga (Markt und Technic) ---------------------------------------------- Maybe you know this guy as the programmer from the Lattice-C-compiler, indeed he did. He is manager from the Motorola-compiler division at the SAS institute. Nowadays he programs parts of the new kickstart 1.4D and as you also may know it is completely written in assembler. ('till Kickstart V1.3 it was written in C!) In this article you will find some of the well-known optimize tricks. Sometimes the routine will get bigger but the execution time faster. I've added some notes and tricks from my friends and me especially for the Amiga, because the author only talks in 'clean' 68000 assembler. The way to write optimal (optimized) code can be divided in 3 seperate groups : - peephole- or instructionoptimizing - local optimizing - global optimizing What John (when I talk about John, I don't mean myself ofcourse) means here is that you can rewrite a whole routine or just make it faster by changing to some faster instructions. John has a lot of experience with this because of the Lattice compiler. The faster the compiler and the object code, the more people want to use it. PEEPHOLE OPTIMIZING ------------------- The choice of a instruction with a 68000 processor isn't that simple because of its large instructionset. A lot of alternative instructions do the same work. In this section all tricks are based on the 68000 processor (also work on the later versions ofcourse) The name PEEPHOLE comes from the idea that only a small part of the code is visible. Some hints : - Avoid 32-Bit-Immediate values. - Avoid 32-Bit-Operands with Adresregisters. - Avoid 32-Bit-Absolute Addresses (I smell Commodore guidelines here!) - Avoid MUL- and DIV-Instructions. - Use QUICK-form when possible. - Forget the CLR instruction. Some common instructions : L1 move.l #1,d0 6 bytes 12 Cycles L2 add.l #2,a0 6 bytes 16 Cycles L3 move.l #3,Datavar 10 bytes 28 Cycles ! L4 muls.w #10,d0 4 bytes 70 Cycles ! L5 clr.l d0 2 bytes 6 Cycles More efficient : L1 moveq #1,d0 2 bytes 4 Cycles L2 addq.w #2,a0 2 bytes 8 Cycles L3a moveq #3,d0 2 bytes 4 Cycles move.l d0,Datavar 6 bytes 20 Cycles ------- --------- 8 bytes 24 Cycles L3b moveq #3,d0 2 bytes 4 Cycles move.l d0,Datavar(a4) 4 bytes 16 Cycles ------- --------- 6 bytes 20 Cycles L4 move.l d0,d1 2 bytes 4 Cycles lsl.l #2,d1 2 bytes 12 Cycles add.l d1,d0 2 bytes 8 Cycles add.l d0,d0 2 bytes 8 Cycles ------- --------- 8 bytes 32 Cycles L5 moveq #0,d0 2 bytes 4 Cycles These solutions make you give one way or the other ... larger code is on the whole not so bad because the execution speed is faster. L4 is a perfect example that both the speed and size can be optimized. L3 shows that more code can be faster. From this we can learn that : - Whenever you can use a MOVEQ for a 32-Bit value, do it ! A perfect example : and.l #15,d2 6 bytes 16 Cycles or.l #2,d3 6 bytes 16 Cycles sub.l #28,d4 6 bytes 16 Cycles cmp.l #1,d1 6 bytes 14 Cycles -------- --------- 24 bytes 62 Cycles Faster AND SHORTER : moveq #15,d0 2 bytes 4 Cycles and.l d0,d2 2 bytes 8 Cycles moveq #2,d0 2 bytes 4 Cycles or.l d0,d3 2 bytes 8 Cycles moveq #28,d0 2 bytes 4 Cycles sub.l d0,d4 2 bytes 8 Cycles moveq #1,d0 2 bytes 4 Cycles cmp.l d0,d1 2 bytes 8 Cycles -------- --------- 16 bytes 48 Cycles Substracting a small value from a register can be shorter if you do it with 2 SUBQ.L instructions (When the value is too big for one) sub.l #10,d0 6 bytes 16 Cycles becomes : subq.l #8,d0 2 bytes 8 Cycles subq.l #2,d0 2 bytes 8 Cycles ------- --------- 4 bytes 16 Cycles With C-routines a lot of Stackmanipulations are necessary. Cleaning after calling a C-routine with parameters on stack looks like this : add.l #12,SP 6 bytes 16 Cycles When it is not necessary to add more than 8 it is better to do this : addq.w #8,SP 2 bytes 8 Cycles For 9 bytes or more : lea 12(SP),SP 4 bytes 8 Cycles Think not only in simple moves but show some smart moves like : move.l #$10000,d0 6 bytes 12 Cycles can be very delaying but this is faster ! moveq #1,d0 2 bytes 4 Cycles swap d0 2 bytes 4 Cycles ------- -------- 4 bytes 8 Cycles this little gag works with all numbers between $7f0000-$10000 (only when the lower word is 0) With negative numbers you can also use the MOVEQ instruction, e.g. we want -200 in d0 : moveq #256-200,d0 2 bytes 4 Cycles neg.b d0 2 bytes 4 Cycles ------- -------- 4 bytes 8 Cycles This way you can load every negative 8-Bit value with MOVEQ and NEG.B/NOT.B in a dataregister. A few more directives : - Use the .W form whenever possible with addresses - Use addressregisters when possible for operations NOT: move.l #$502,a1 6 bytes 12 Cycles BUT: lea $502,a1 4 bytes 8 Cycles When you need A1 to be NULL then use : sub.l a1,a1 2 bytes 8 Cycles (Just look to any C-compiled program and you will find this trick) The .W form is also very useful preparing a call to a C-routine (writing data to stack) move.l #1,-(SP) 6 bytes 20 Cycles better: moveq #1,d0 2 bytes 4 Cycles move.l d0,-(SP) 2 bytes 12 Cycles ------- --------- 4 bytes 16 Cycles or the same : pea 1.w 4 bytes 16 Cycles Amiga DOS has some strange things like those BPTR's (eg. when you load a segment and get an address in return) This BPTR has to be multiplied by 4, and addressregisters can perform a bit-shift. Normally you would do this : exg a0,d0 2 bytes 6 Cycles lsl.l #2,d0 2 bytes 12 Cycles exg d0,a0 2 bytes 6 Cycles ------- --------- 6 bytes 24 Cycles EXG cuts back the cycles instead of using MOVE, even better is : add.l a0,a0 2 bytes 8 Cycles add.l a0,a0 2 bytes 8 Cycles ------- --------- 4 bytes 16 Cycles We could go on and on with these little tricks but it is always up to you to use them and when you are in time-need to use them. Sometimes you must use instructions like MUL or DIV (eg when you don't know in advance with what you are multiplying) Only the future will tell .... LOCAL OPTIMIZING Not only instructions can be optimized but also little routines which are frequently used like indexing an arrayelement (which are in word size steps) move.l index,d0 muls.w #4,d0 better move.l index,d0 lsl.l #2,d0 With shifts you can gain carloads of time : - When the shift is larger than 15, do a SWAP and substract 16 from the number of shifts. - When the shift is 8 or larger, make a shift with the immediatenumber 8 and substract 8 from the number of shifts. - When the number is 1 and you have to shift left then use the ADD instruction. Used for real : Shift D0 1 bit left add.l d0,d0 Shift D0 2 bits left lsl.l #2,d0 Shift D0 9 bits left lsl.l #8,d0 add.l d0,d0 Shift D0 16 bits left swap d0 clr.w d0 (Hey, and John told us to forget the CLR instr.?!?) Shift D0 24 bits left swap d0 clr.w d0 lsl.l #8,d0 etc. etc. etc. These shifts don't look for negative bits etc. so when you have special routines for calculating these things remember to use the EXT.L command instead of the CLR.W here. John goes on with several other tips that I think are quite simple and very specific on certain things. The best way to write fast and good code is to write a routine and then rewrite to get it faster and make changes where necessary. Don't use long calculations when you can put the answers in a simple table, maybe it takes some more space but it certainly speeds up the business. In large routines it is also good when you use correct Dataregisters and make sure that answers appear in the register you need. eg that you don't have to do things like: move.l d0,d1 move.l d2,d0 move.l d1,d2 to exchange registers etc. (THIS can take a lot of time in loop routines!) Don't use MOVEM to often because the standard rule using library routines is that only D0/D1/A0/A1 are changed, so don't put everything to stack everytime. Copying data can be done very fast with the MOVEM trick that Olli used in last month's issue (although I never have trouble with my BLITTER because it is complete multitasking ! Maybe you use the BLITTER NASTY the wrong way Olli ! bit #10 of $dff096 (DMACON)) Also cleaning memory is done very fast this way. To make re-entrant code it is a good habbit to clear every variable when you don't need it anymore (eg when exiting). (re-entrant code is needed to make programs resident) A good habbit also is to use an BSS (not BBS!) segment for all your undefined variables and workmemory. This way you don't have to reserve it and it doesn't take much space when saved on the disk. Writing utilities doesn't require super-fast coding but good structual code that can be altered a few months later and still stays readable. These optimized instructions do not make a contribution to the simplicity of a source. So a simple advice can be : Don't try it the hard way when it's not needed. An even better advice is : Get yourself a good 68000 reference manual with all instructionspeeds. Contact me for some tricks exchanging on BOIL, PO BOX 68, 3480 DB HARMELEN in the wonderful windy and wet country HOLLAND.