Introduction
|
This guide is targeted toward experienced programmers who would like to learn how to write programs with optimal efficiency. The basic concepts and elementals of the languages presented here are beyond the scope of this guide. If you'd like to learn Z80 Assembly, click here.
Often times Z80 programmers use inefficient code because they have no way of knowing that certain instructions will use less memory or less time than others. You can use what's called a code counter to test how many clock cycles and bytes a set of instructions will use. There are several code counters available on the internet. You could test your code outside of the editor that you use to write it, or you could do it within the editor, using Assebly Studio 8x. I personally always use Assembly Studio 8x when I write in ASM, and I highly suggest you do the same. Using its built-in code counter, you'll find that writing efficient programs is far easier than you ever though possible. Assembly Studio also features an easy-to-use sprite editor, and allows you to edit and compile your programs all in a user-friendly Windows environment. To get Assembly Studio, click here
Note that all the optimization tricks presented here coincide with all the Z80-based calculators (TI-73, 82, 83, 83 Plus, 84, 84 Plus, 85, and the 86). Most of these tricks can, in fact, be used on any Z80-based platform, calculator or not (it will say if the trick is just for TI calculators)
|
|
Z80 Assembly Optimization Tricks Version 2.1
|
by Adam Ziembaother contributors listed at bottom
For easy reference, you can use these links to jump to a specific topic on this page:
Note on Z80 Assembly
|
Z80 Assembly programs run at an extremely fast pace. The tricks listed below may show how many cycles and bytes they conserve over the conventional method. CPU time is measured in clock cycles, or as it is sometimes called, t-states. The TI-83 Plus CPU runs at about 6 MHz, which is equivilent to 6 million clock cycles per second. There are many optimizations listed below that only save but a few clock cycles. So if it saves 3 clock cycles, this means that you will save about three six-millionths of second! Not much a difference, but even the most precise variations can accumulate to be very time consuming, and always using the most efficient route possible is an indication of a good programmer.
|
Z80 Assembly Literacy
|
In order for you to completely comprehend how Z80 Assembly works, you must be sure to always communicate about it correctly. Below are several rules you need to comply with so as to avoid complications when conveying Z80 Assembly notion:
- Never say This instruction sets the NZ flag. There's no such thing as the NZ flag. What you meant to say was, This instruction resets the Z flag.
- Generally, you should never dictate values in octal bases in Z80 ASM. Although most assemblers allow you to use them easily, there's really no point. Binary is used to indicate how sprites will appear on the screen, and hexadecimal is used to show how values will be stored in the memory. Decimal, of coarse, is only used for simplicity reasons. However, octal doesn't serve any relevant purpose, so avoid using it at all costs.
- Many assemblers allow you to use the $ symbol to precede hexadecimal values and % to precede binary values. Alternatively, assemblers also allow you to place h or b after the value to indicate the base being used. It is preferred that you always use $ and % for two reasons. First of all, when you're reading the code and you see that symbol, you know what base is being used beforehand, instead of having to look at the value and then the base. But more importantly, $ and % are much easier on the eyes.
- Never use
ld A,$5 in assembly. If you're going to use hexadecimal values, be sure to always include leading and trailing zeros. Using just $5 is a bit ambigious, as someone might misinterpret it as being $50. So to avoid complications, include the leading zero. This goes for 16-bit loads as well. So ld BC,$D should be written as ld BC,$000D .
- Generally, always use decimal values for the first argument of a
set , res , or bit instruction. It is understood that valid bit values range from 0 to 7, so there's no need to use hex or binary.
- Never use unneccessary expressions as arguments in ASM. For instance:
- ld HL,appBackUpScreen-12-(-(12*64)+1)-2*4
This expression serves no relevant purpose, so to save avoid confusion, simply evaluate it to ld HL,appBackUpScreen-771 . However, expressions may be used so long as they prove relevant. For instance:- ld HL,12*256+45
- ld (_penCol),HL
This expression tells us that pen coordinates will be located at (12,45).
|
Time Consumption
|
Sometimes you might need to optimize for sake of time, even at the expense of memory. For an example, if you writing a 1-paged FLASH application, you have no choice but to use exactly 16384 bytes. So if you end finishing your program and you still have used way less than 16384 bytes, you should optimize it so that it runs faster. The tricks listed below show many ways to do this.
- When adding or subtracting from the accumulator, you shouldn't use the
inc or dec more than once. So you shouldn't ever use:- inc A
- inc A
Instead, use- add A,$02
The same goes for dec A as well. This trick will save no memory, but it does take up one less clock cycle. This means that on the TI-83 Plus (6 MHz), you'll be conserving about one six-millionths of a second!
- For 8-bit subtraction of registers other than the accumulator, you shouldn't use the
dec instruction more than 3 times. For example:- dec D
- dec D
- dec D
- dec D
- dec C
- dec C
- dec C
This should be optimized into- ld A,D
- sub $04
- ld D,A
- dec C
- dec C
- dec C
Using the sub instruction here saved 1 clock cycle. But the dec C statements remained, because using dec three times uses one less byte than:- ld A,C
- sub $03
- ld C,A
- Use
jp instead of jr . jp is faster, but uses one extra byte. So if you are making an application, you should probably always use jp .
- Never use:
- or A
- ret Z
- jp loop
Use:- or A
- jp NZ,loop
- ret
This saves 1 clock cycle.
- Never use:
- ld BC,$0501
- ...
- ld B,$08
- dec C
Whenever you find yourself in a situation such as this (assuming BC is not destroyed within the '... '), you can save 1 clock cycle by simply doing this:- ld BC,$0501
- ...
- ld BC,$0800
- Whenever you need to fill area of memory with a specific value, you might opt to do it this way:
- ld HL,area
- ld B,100
- fill_loop:
- ld (HL),A
- inc HL
- djnz fill_loop
You should instead use the ldir instruction. In most cases, ldir uses a little more memory, but much less time. The example above used 9 bytes, with 26 cycles per byte. Whereas with the following example, which uses ldir , only takes 12 bytes, with 21 cycles per byte:- ld BC,100
- ld HL,area
- ld DE,area+1
- ld (HL),A
- ldir
|
Memory Consumption
|
The first and foremost aspect of a good ASM programmer is his ability to use as little memory as possible while still achieving the desired effect. The tricks listed below show many ways on how to conserve memory, as well as time.
- Never use:
- ld B,5
- ld C,12
You should instead load the values simultaneously into the BC register pair, saving you 1 byte and 4 clock cycles:- ld BC,$050C
If you do not want to have to go through the trouble of converting the values into hexadecimal, you can use this formula instead:- ld BC,5*256+12
There's really no need to ever convert to hexadecimal. The reason why I always do is because it dictates how large of a load is taking place. Mnemonic values are almost always dictated in hexadecimal, because every two digits of hex corresponds as one byte in memory consumption. This way you can easily tell how the values will be stored in memory.
- Never use:
- ld A,$00
Use:- xor A
The xor instruction performs a binary exclusive OR operation on the argument and the accumulator. So if the argument is the accumulator, the result will always be 0. This is because when the XOR operation is performed on two equal numbers, the condition for each bit is evaluated as false. You can also use xor A to set the zero flag.
- Never use:
- cp $00
Use- or A
The or instruction performs a binary inclusive OR operation on the argument and the accumulator, and effects the zero flag. So if the argument is the accumulator, an inclusive OR operation will make no changes, but the zero flag will still be effected. So there's no need to compare A to 0.
- Note that the
cp instruction subtracts the argument from the accumulator but doesn't store it anywhere. If you do not need to use the accumulator later on, than just use the sub or dec instruction, because they both effect the zero flag. Here's an example:- B_CALL GetKey
- cp kRight
- jr Z,right_pressed
- cp kLeft
- jr Z,left_pressed
- cp kEnter
- jr Z,enter_pressed
Since kRight=$01, kLeft=$02, and kEnter=$05, you can optimize the code into this:- B_CALL GetKey
- dec A
- jr Z,right_pressed
- dec A
- jr Z,left_pressed
- sub $03
- jr Z,enter_pressed
In this example, we saved 3 bytes, 9 cycles.
- Never use:
- xor A
- ld BC,$0000
Use:- xor A
- ld B,A
- ld C,A
Always use registers to assign values rather than constant expressions whenever possible. This saves both memory and time.
- Never use:
- or A ; Reset carry flag
- ld BC,$0015
- sbc HL,BC
Instead, add the negative:- ld BC,-$0015
- add HL,BC
This will prevent you from having to use or A to reset the carry flag, and the add instruction uses only 1 byte, whereas the sbc instruction uses 2 bytes. In all, this trick saves you 2 bytes, 8 clock cycles.
- When you need to save temporary data DO NOT define bytes or words within the program itself! All the Z80-based calculators have ample amounts of free, safe RAM for you to use. TI-83 Plus programmers can use all the RAM within
appBackUpScreen , and SaveSScreen . Both of these are 768 bytes in size. If that's not enough, you can also use tempSwapArea (232 bytes) and cmdShadow (128 bytes). These RAM locations contain temporary data used by TI-OS, but are free to use while ASM programs are running. This memory may be used to save temporary data only. Think of them as the variables A through Z used in BASIC, except for ASM, and much larger in size! Here's an example:- ld (score),A
- ld BC,(temp)
- ld (high_score),DE
- score:
- .db 0,0
- temp:
- .db 0,0
- high_score:
- .db 0,0
This should be optimized into- score EQU appBackUpScreen
- temp EQU appBackUpScreen+$02
- ...
- ld (score),A
- ld BC,(temp)
- ld (high_score),DE
- high_score:
- .db 0,0
'score' and 'temp' are instead saved within appBackUpScreen , thus making the program 4 bytes smaller. Because a high score is saved for later use, it should be stored within the program itself. NOTE: if you are developing a program for MirageOS or ION, cmdShadow must not be altered, as it is used by some of the ION routines (MirageOS uses these same ION routines).
- Whenever DE is not being used and you need to temporarily save HL (or vice versa), use
ex DE,HL and then use it again to exchange it back. But you must make sure that the code inbetween the two ex statements does not contain any instructions or call any routines that destroy DE, or if you're saving DE in HL, than make sure HL is not destroyed. The best example of this trick is when you want to save the contents of HL while calling Getkey, or so it seems. You may have noticed that in the TI-83 Plus Developer Guide, it says that DE is not destroyed by the GetKey routine. This is, however, a misprint. Thus, don't attempt to use ex DE,HL to save HL while calling GetKey!
- Often times you might need to conditionally use certain instructions that can't be conditionally used. For instance, let's say you need to check to see if split screen mode is active, and if so, deactivate it. One way is to just call a subroutine that sets the splitOverride flag, that way you can call it conditionally:
- B_CALL CheckSplitFlag
- call Z,setSplitOverride
- ...
- setSplitOveride:
- set grfSplitOverride,(IY+sGrFlags)
- ret
But let's optimize this and instead use jr , which uses 2 bytes when called conditionally, whereas call uses 3 bytes when called conditionally:- B_CALL ChkSplitOverride
- jr NZ,skipSplitOverride
- set splitOverride,(IY+sGrFlags)
- skipSplitOverride:
- ...
Plus, you don't need a ret instruction when you use jr .
- Never use:
- ld A,D
- or A
- jr Z,DEqualsZero
Instead, use:- inc D
- dec D
- jr Z,DEqualsZero
- Use
- push DE
- ...
- pop DE
instead of - ld (ADDR),DE
- ...
- ld DE,(ADDR)
whenever possible. The ld (ADDR),rr instruction uses 3 bytes, 16 cycles, whereas push uses only 1 byte, 11 cycles.
- Never use:
- push BC
- ...
- pop BC
- ld D,B
- ld E,C
The pop instruction pops the value on the top of the stack into the specified register pair, so it doesn't have to be the same register pair as the corresponding push statement:- push BC
- ...
- pop DE
- Whenever you want to reset the carry flag, never use:
- scf
- ccf
Instead, use:- or A
The or instruction resets the carry flag.
- This should be common sense, but never use:
- sub $01
- ld BC,$0003
- add HL,BC
Use the inc and dec instructions!- dec A
- inc HL
- inc HL
- inc HL
- In response to trick #14, you shouldn't use
inc HL more than three times. Beyond that you should use add HL,$0004 . This rule applys only when adding to HL. When subtracting from HL, you shouldn't use dec HL more than 4 times. Beyond that use:- ld DE,-$0006
- add HL,DE
When it comes to 8-bit subtraction, there are no optimization tricks that save memory, but there are some that save time - see rule #1 and #2 under Time Consumption.
- Whenever you need to store a value into a RAM location and then recall the value later on, you might consider using the conventional method, which is as follows:
- score = appBackUpScreen
- ld A,$12
- ld (score),A
- ...
- ld BC,$0505
- ld (curRow),BC
- ld A,(score)
- B_CALL PutC
In the example above, $15 is stored into (score) and then after being recalled later on in the program, the corresponding character for the value of (score) is displayed at row 5, column 5. But rather than store the value in some RAM location, it would more efficient to store the value within the code itself. So the example above could be optimized into:- ld A,$12
- ld (score),A
- ...
- ld BC,$0505
- ld (curRow),BC
- score = $ + 1
- ld A,$00 ; ld A,(score)
- B_CALL PutC
This is a technique called code mutation. Basically, what we are attempting do here is alter the parameters of the coded instructions. This a quite complex concept to grasp, so read carefully. First, let's examine how the assembler will compile the object code. Every ASM instruction has its own unique value that corresponds to it. This value is known as the opcode. For instance, $3E,nn is the opcode for the ld A,nn instruction (where nn is the 8-bit value that's being loaded into A). So what the assembler does is translates all the instructions into their corresponding opcode. But what about labels? For instance, in the isntruction jp main , how does the assembler know what to value to substitute for main ? All labels correspond to where they will be stored in the calculator's memory. The assembler starts storing opcode at the memory location specified by the .org instruction.
But that is beside the point of this trick. What this trick does is alters, or mutates, the opcode itself. Notice in the first example above, we loaded (score) into A. But instead of having score point to appBackUpScreen, we could make score point to the nn parameter of the ld A,nn statement. To do this, we made a label that would point to that location in the memory. The $ symbol, when without a value next to it, tells the compiler to substitute it with the current memory location that the following instruction will be stored at. In the example above, we used score = $ + 1 . Notice the instruction following this is ld A,$00 . So score = $ + 1 tells the assembler to let the label score point to the $00 part of the ld A,$00 statement. So when we are writing the code, it wouldn't matter what value we put for nn of the ld A,nn instruction, as it would eventually change anyway. It could as well of been $D6 or something. So that's the reason why we used ld A,$00 instead of xor A in the 2nd example above. Code mutations can sometimes be difficult to recognize when they are being used in programs. Since we know to always use xor A instead of ld A,$00 , we can use ld A,$00 to indicate that a code mutation is being taken place, as that would be the only case that you wouldn't use xor A instead of ld A,$00 . Note that you can also use this trick for 16-bit loads as well. Also, you can't use another ld A,$00 statement and attempt to mutate it using the same label. This should be common sense. After you've used one code
mutation, you'll have to use the conventional method if you wish to load it somewhere else in the program. Code mutations are a lot to take in when you first learn them, but it's quite easy once you get the hang of it. The gain here is that ld A,nn is smaller and faster than ld A,(ADDR) , and even more so is when you need to load the value into a register other than the accumulator. Conventionally, it would take 4 bytes, 17 cycles to do this. But using code mutation, you will save 2 bytes, 10 cycles. Regardless of how much work is required, you should always use as little memory possible. Every byte counts!
- One important thing to point out about code mutation is that the parameter that is to be modified will have the same initial value everytime the program is first executed†. So the initial value for the a variable can be assigned immediately in the code. The following example causes the
lives variable to start off with a value of 3 every time the game is started:- lives = $ + 1
- ld A,$03
This saves you from having to assign the value at the beggining of the game seperately, as shown below:- ld A,$03
- ld (lives),A
†NOTICE: Some shells will allow write-back in assembly programs. This means that any code mutation will be reflected the next time the program is ran. If you are developing an ASM program for TI-OS, than you need not worry about this. But if you are developing an ASM program for a shell, such as MirageOS, than you will need to assign the value seperately at the beggining of the program.
- Whenever you need to call subroutines, you might consider using:
- ld HL,param1
- call sub1
- ld HL,param2
- call sub2
- ld HL,param3
- call sub1
- ...
- sub1:
- ...
- ret
- sub2:
- ...
- ret
This is the conventional way to do this. However, there is a much more efficient way to do this using what's known as call tables. Here's how they work:- ld SP,callTable
- ret ; Jump to sub1
- sub1:
- pop HL
- ...
- ret
- sub2:
- pop HL
- ...
- ret
- callTable:
- .dw sub1,param1
- .dw sub2,param2
- .dw sub1,param3
In the first line, callTable is stored into the Stack Pointer (SP). Please note that altering the Stack Pointer can be potentially damaging. You'll probably want to save the contents of SP before using a call table. If used properly, call tables will prove to be very efficient. It is, as you can see, a very complex way of calling routines. And in fact, they don't always make your programs more efficient. Generally, you will only want to use call tables when you are passing a different parameters to several consecutive subroutines.
- Never use:
- dec B
- jr NZ,loop
Use:- djnz loop
The djnz instruction decreases B by one, and if the resulting value is nonzero, makes a relative jump to the specified label.
- Try to rearrange code so that you can use more efficient instructions. For instance:
- push BC
- ld B,H
- ld C,L
- pop HL
- dec D
- jr NZ,loop
This should be optimized into- ex DE,HL
- djnz loop
Here, we switched BC and DE so that we could use ex DE,HL and djnz instead.
- Never use:
- ld A,B
- neg
Instead use:- xor A
- sub B
- Never use:
- ld A,D
- sub $D3
- neg
Instead use:- ld A,$D3
- sub D
- Try to avoid using the ASM_FLAGs (ASM_FLAG1 through ASM_FLAG6) whenever possible. If there are any unused registers, you could simply use the register to store flags. You could also let HL point to a memory location at which the flags could be stored. For an example:
- res 5,(IY+ASM_FLAG1)
- set 2,(IY+ASM_FLAG3)
- ...
- bit 2,(IY+ASM_FLAG3)
- jr NZ,bitIsSet
- bit 5,(IY+ASM_FLAG1)
- ret NZ
- set 5,(IY+ASM_FLAG1)
In this example, we will assume D and HL are not used nor destroyed in the program. In such case, we would optimize the code by using D and HL to store the flags:- ld HL,appBackUpScreen
- set 5,(HL)
- set 2,D
- ...
- bit 2,D
- jr NZ,bitIsSet
- bit 5,(HL)
- ret NZ
- set 5,(HL)
In this example, it would actually be more efficient to just use D to store all the flags (there are 8 bits in D, and only 1 is being used here), rather than bother with the ld HL,appBackUpScreen stuff. But the point is, you should always try to use (HL) or an 8-bit register instead of ASM_FLAGs.
- Always use
jr instead of jp whenever possible. jr is actually slower (by only 2 cycles), but uses only 2 bytes, whereas jp uses 3. Your compiler should let you know if a jump is too far for a relative jump.
- Never use:
- ld A,(ADDR)
- inc A
- ld (ADDR),A
Use:- ld HL,ADDR
- inc (HL)
The same goes for dec as well.
- Consider this:
- ld A,$12
- ...
- xor A
- inc A ; Reset zero flag
In this example, xor A and or A are used to reset the zero flag. But if A is not destroyed within the ... then you could instead just use and A . Using and A will make no changes to A, but will effect the flags.
- Please remember that (HL) can be used instead of an 8-bit register in nearly all the instructions. Here is are some examples:
ld r,(HL) | ld (HL),r | ld (HL),nn |
inc (HL) | dec (HL) |
adc A,(HL) | sbc A,(HL) |
bit b,(HL) | set b,(HL) | res b,(HL) |
sra (HL) | sla (HL) | srl (HL) |
rlc (HL) | rrc (HL) |
rl (HL) | rr (HL) |
and (HL) | or (HL) | xor (HL) |
add A,(HL) | sub (HL) |
cp (HL) |
jp (HL) |
b is a valid bit number 0 to 7 r is any 8-bit register nn is any 8-bit number from 0 to 255 NOTE: jp (HL) jumps to the address stored in HL, not the address stored in the indirect contents of (HL) .
- If you need to change one bit at one of the index registers, you might consider using doing this:
- bit 3,(IY+$05) ; check bit
- jr Z,bit_reset ; set bit if bit is reset
- res 3,(IY+$05) ; reset bit if it was set
- jr bit_changed ; omit the following instruction...
- bit_reset:
- set 3,(IY+$05)
- bit_changed:
- ...
In all, this uses 16 bytes. You could use half the amount of memory by instead doing this:- ld A,(IY+$05) ; store the byte in A
- xor %00001000 ; change the state of bit 3
- ld (IY+$05),A ; save the altered byte
8 bytes saved!
- If you wish to change any of the system flags for a TI calculator, you could make the process used in trick #28 even shorter, using HL to point to the location:
- ld HL,_flags+$05
- ld A,%00001000 ; mask
- xor (HL)
- ld (HL),A
This trick uses only 7 bytes.
- Often times you'll have several variables that need to have specific value stored in them at the same point in the program. Conventionally, you would do it this way:
- var1 = temp
- var2 = temp + 1
- var3 = temp + 2
- var4 = temp + 3
- ld A,56
- ld (var1),A
- ld A,1
- ld A,(var2)
- ld A,18
- ld (var3),A
- ld A,255
- ld (var4),A
As long as there are more than two variables, and all the variables are sequentially placed in the memory, you could instead use this:- ld HL,temp
- ld (HL),56
- inc HL
- ld (HL),1
- inc HL
- ld (HL),18
- inc HL
- ld (HL),255
|
Useful Tricks
|
These tricks don't save memory or time, but they can still be useful in certain situations.
- Whenever you want to multiply A by 2, you might consider using:
- add A,A
But what if you don't want it to effect the zero flag, you can instead use:- rlca
The rlca instruction rotates the accumulator one bit to the left, which if you do the math, you'll find it multiplies A by 2. Please note that the rlca instruction does effect the carry flag, however.
- In trick #1, you learned that rotating the accumulator one bit to the left will multiply it by 2. So it would make since to say that by rotating the accumulator one bit to the right, you will divide A by 2. Indeed, this is true. However, dividing numbers using rotations can have arbitrary results. For instance, what if A is odd? Well, any number is odd if and only if bit 0 is set. So by rotating an odd number one bit to the right, you will have have simply moved bit 0 to bit 7, making the number very large (or if you use two's complement, the number will be negative). Here's an example:
- ld E,$00
- DivideLoop:
- inc E
- sub $02 ; C is set if result is < 0
- jr NC,DivideLoop
- ld A,E
This is extremely inefficient way of division! You should instead simply use this:- rrca
That's it! But remember, this trick only works when bit 0 of A is reset, or in other words, A is even. So long as bit 0 remains reset, rrca is the best method of division. Should the case arise that you need to divide by 2 (or a multiple of 2) and not have to worry about odd numbers, you can afterwards mask out the unwanted bits using the and instruction. Here's an example:- rrca
- rrca ; A has been divided by 4
- and %10000000 ; Mask out unwanted sign bit
- The last trick showed how to divide the accumulator with respect to the sign of the number (signed is when bit 0 is set). To avoid complications involved when rotating bits, you can alternatively use shifts. For instance, the
srl instruction does the same as rrca , except it can be used on any register. More importantly, it resets bit 0, thus avoiding the complications associated with carry and odd numbers. However, srl uses 2 bytes, 8 cycles, whereas rrca uses only 1 byte, 4 cycles. So whenever you need to divide by two and still keep the number unsigned, then you should always use srl . However, if you need to divide by 4 or more, then you should always use rrca however many times necessary, and then mask out the sign bit.
- If you are writing a program for any of the Z80-based calculators (73,82,83,83 Plus,85,86), there is a very useful trick you can use when working with characters. There's only one bit in the character value differing between upper and lower case for the same letter. All you need to do is complement bit 5 using
xor %00100000 (given the character value is stored in the accumulator). This is very useful if you want to have a case insensitive search or text input routine. Even better, the TI-OS flag for lowercase is also stored in bit 5, so you could take that bit and mask it with the character to change the character to reflect the state of the flag:- ld A,(IY+shiftFlags)
- and %00100000
- or C
Assuming the character value is stored in C, after this code is executed, the accumulator will hold the value of the character with the same case as the current setting. This trick will save you much time and effort when working with case sensistivity!
- Similar to trick #4, it is possible to make a letter out of a number (1 for A, 2 for B, etc.) by simply changing the state of bit 6. This will change the number in to the corresponding upper case letter. If you wanted to change it to the corresponding lower case letter, you would simply mask both bit 5 and bit 6. Very useful in text input routines!
|
Please note that this list is NOT complete. New tricks are continually being added. For a list of the most up-to-date tricks, view the online version of this guide. The following is a list of contributors.
Contributors:
- Leif Åstrand
- Memory Consumption - #17, 27, 28, 30
- Useful Tricks - #4, 5
| |
If you have any additions and/or changes you think should be made to this guide, please e-mail me. Of coarse, you will receive full credit for your contributions. Thanks!
|
|
|
|