mwm@eris.UUCP (04/12/87)
I know there are others out there using Lattice. You'll appreciate the hints to be found herein. <mike "Oh, Say Can You C" by John Toebes This issue, I detour from further improvements to PRINT to examine a problem common to many programs and programmers--how to produce small code modules in C. A number of programs out there for Amiga(tm) take up extraordinary amounts of disk space (and memory) only because the authors didn't know or failed to use some simple tricks to reduce the size of finished code. I'll show the techniques on small programs (anything under 32K falls into this category); they're equally useful on larger programs. Fundamental: Believe it or not, the fastest way to reduce the size of a program is to recompile and relink it! Many people forget to delete the debugging data (pulled in from the libraries and included in compiled code by default). It's handy during development but should be stripped from every final version. This can be done with the utility STRIPA or by using the NODEBUG option in BLINK. Debug code can account for 25 percent or more of program size. Merging: Use the SMALLCODE and SMALLDATA options when you BLINK; they force all otherwise separate code hunks of a single type to be merged into a single large hunk. By default, the linker puts all hunks of the same name into a single hunk, but those hunks that are not named (or whose name you cannot control) are put into separate hunks. This is nice for scatter loading of the hunks and uses small fragments of memory well, but for code under 32K one may readily argue that large hunks fragment memory less because there are fewer hunks. By using the SMALLCODE and SMALLDATA options, you tell BLINK to combine all like hunks regardless of the name. Although this doesn't reduce the size of the executable code, it does reduce the amount of overhead for the loader and the amount of data stored in the disk load file. Compiler Options: With the Lattice(R) C compiler, Release 3.10, you can employ compiler options such as -b (base relative data) -r (base relative subroutine calls) and -v (no stack checking). For a small program or utility, all of these options are generally reasonable. Stack checking is only important if programs make many levels of subroutine calls and may overflow the stack. In reality, one can usually live without the extra handholding for a debugged and running application. The Effects: The options above reduce the code WITHOUT making any change to the source itself. To illustrate the changes in size, we can take the program hello0.c (shown below) and compile it with different options: /* hello0.c */ Default SmallC&D NoDB -b-r -v #include <stdio.h> LISTed Size: 10232 9856 8572 8560 8260 main() { printf("hello world\n"); } Using SMALLCODE and SMALLDATA options, we drop the code size about 400 bytes; NODEBUG eats up another 1.3K. The -b and -r options in LC only eat 12 bytes in this tiny program, but the -v option grabs another 300. Overall, we drop the code by 2K (20%) simply by recompiling and relinking. Changes in Code: We have a good start, but we can achieve even better results by a few simple changes to the source. In doing so, we must remember two rules: 1) Use only what you need. General purpose functions should be avoided unless their capabilities are essential. 2) Write code for the Amiga, not for a UNIX(R) machine! Rule 1 is easy to forget. How does one normally write a string with C? With printf(), of course. printf(), however, is capabable of MUCH more than output of simple strings--it can format, in complex forms, every data type known to C. Why use such a length and powerful function just to print a simple message? In the program below, we substitute puts() and immediately recover 2K of code. /* hello2.c */ Default SmallC&D NoDB -b-r -v #include <stdio.h> LISTed Size: 8086 7692 6596 6584 6284 main() { puts("hello world"); } Our program now is only 61 percent of the size of the original. Can we make it smaller still? We note that the program needn't provide for command line arguments (argc and argv). So we conveniently rename our program _main instead of "main"; the standard _main.c program isn't pulled in. We save another 1.4K. Rule 2 and UNIX. We ruefully learn that the last program doesn't work. The Amiga program _main.c is responsible for setting up the default UNIX input and output file handles. So our last move fails. But it show us how much overhead exists if we set up for the UNIX environment. Can we avoid that environment? [NOTE: Do not rename to _main.c if your program uses ARGV or ARGC, for there will now be only one argument parm: char *argp, which points to a single, null-terminated command string. It's useful for some programs (for an echo command, perhaps?), which require only one argument.] Doing It With AmigaDOS: To get rid of the UNIX environment, we use AmigaDOS for output. This step drops our program all the way down to 2.2K (that's an 80 percent reduction) with the same results: /* hello4.c */ Default SmallC&D NoDB -b-r -v #include <stdio.h> LISTed Size: 4276 3968 3256 3240 2220 _main() { Write(Output(), "hello world\n", 12); } Sadly, we have to count the number of characters in the string--but we can add a small routine to do this, as shown below in hello5.c: If you call the output routine more than once this extra cost for a subroutine is quickly eliminated. /* hello5.c */ Default SmallC&D NoDB -b-r -v #include <stdio.h> LISTed Size: 4388 4080 3336 3312 2272 _main() { myputs("hello world\n"); } myputs(str) char *str; { Write(Output(), str, strlen(str)); } Have we reached the limit? No, there is still one more trick to pull. Using the MAP option of BLINK, we see a couple of routines have been pulled from LC.LIB, including MEM1.O. This is quite odd, for our program allocates no memory at all. A check shows that c.a (the assembler code for c.o, the startup code from Lattice) pulls in a function called MemCleanup upon the assumption that we have allocated memory--and that we must return it to the system at end of program. Because we may need MemCleanup often, we certain shouldn't edit and reassemble c.a. Instead, we add a stub line (below) which creates a dummy MemCleanup. Our program drops out the code, reducing itself to a mere 1088 bytes. MemCleanup(){} Default SmallC&D NoDB -b-r -v LISTed Size: 2860 2572 2144 2128 1088 Do you always know when your program allocates memory? No. It may do so indirectly even if you don't. If you stub out the MemCleanup (as above), can you fail to return some memory to the system when your program ends? Yes. How do you tell when you need MemCleanup and when you don't? Simple enough: Use the stub MemCleanup when you compile and link. If the MAP file from BLINK shows a reference to MEM1.o, you MUST remove the stub MemCleanup! Your program has--directly or indirectly--allocated memory, and you must return it to the system. What Other Tricks can we use? a) Look at the MAP. When a routine is pulled in from the library, see if you can understand why it is needed and look for ways around it. A good example: if both MEMCPY and MOVMEM are used, one can easily be eliminated by recoding to call the other. Some functions you can't and shouldn't eliminate, such as STRCPY, STRLEN, and STRICMP; it is to your advantage to use these, for they are very specific purpose functions coded in assembler. b) Use register variables whenever possible. If a variable is referenced frequently, it's a good candidate for a register variable. However, if it is assigned often, code can grow somewhat. If you aren't sure, try it both ways see what happens to the code size. c) Use subroutines for common functions. If you find yourself repeating three lines or more of code, they're good candidates for a subroutine, ESPECIALLY if you can make the parameters register variables. Not a single one of these techniques demands substitution of assembly language, although that alternative remains open. How Far Can We Go? Is 1K too small? In general, you can cut a small program to as little as 2 ot 3K. POPCLI, WBRUN, and MEMWATCH, all coded using such methods, list at less than 4K. A stripped-down version of PRINT wieghs in at 2.2K. The size of BLINK is due in large part to the methods above. Summarized below are the command by which each program was compiled and linked: Default versions: LC hello BLINK lib:c.o hello.o LIB lib:lc.lib lib:amiga.lib [BLINK link above is called <standard link> below. It was used in all linkings] SMALLDATA and SMALLCODE: LC hello BLINK <standard link> SMALLCODE SMALLDATA NODEBUG: LC HELLO BLINK <standard link> SMALLCODE SMALLDATA NODEBUG OPTION -b and -r: LC HELLO -b -r BLINK <standard link> SMALLCODE SMALLDATA NODEBUG OPTION -v: LC HELLO -b -r -v BLINK <standard link> SMALLCODE SMALLDATA NODEBUG ----------------------------------------------------------------------- The above was excerpted with permission from Volume I, Number 6 of: The Amigan Apprentice & Journeyman. Published 6 times a year by The Amigans, a not-for-profit association of those who employ the Amiga computer. Purpose of the association is the interchange of useful information. Membership in The Amigans is $24 (U.S.) per year in the United States and Canada; $34 (U.S.) elsewhere. To join, Send membership fee to: The Amigans Box 411 Hatteras, NC 27943 Copyright (c) 1986 by The Amigans. All Rights Reserved. Amiga is a tademark of Commodore-Amiga, Inc. Lattice is a registered trademark of Lattice, Inc. Unix is a registered trademark of AT&T -- Here's a song about absolutely nothing. Mike Meyer It's not about me, not about anyone else, ucbvax!mwm Not about love, not about being young. mwm@berkeley.edu Not about anything else, either. mwm@ucbjade.BITNET