14
\$\begingroup\$

I have worked on developing a feature on a particular product of ours. There has been a request to port the same feature to another product. This product is based on a M16C microcontroller, which traditionally has 64K Flash and 2k of RAM.

It is a mature product, and therefore, only has 132 Bytes of Flash and 2 Bytes of RAM left.

To port the feature requested (the feature itself has been optimised), I need 1400 bytes of Flash and ~200 Bytes of RAM.

Does anyone have any suggestions on how to retrieve these Bytes by code compaction? What specific things do I look for when I am trying to compact already existing working code?

Any ideas will really be appreciated.

Thanks.

\$\endgroup\$
  • 1
    \$\begingroup\$ Thanks everyone for the suggestions. I will keep you updated with my progress and list the steps that worked, and those that didnt. \$\endgroup\$ – IntelliChick Nov 8 '10 at 22:53
  • \$\begingroup\$ Ok so here are the things I have tried that worked: Moved up compiler versions. The optimisation had improved drastically which gave me roughly 2K of Flash. Went through the list files to check for redundant and unused functionality (inherited because of the common code base) for the particular product and gained some more Flash. \$\endgroup\$ – IntelliChick Nov 29 '10 at 0:07
  • \$\begingroup\$ For RAM I did the following: Went through the map file, to check functions/modules that were using the most RAM. I found a really heavy function (12 channels, each with a fixed amount of allocated memory), of legacy code, understood what it was trying to achieve, and optimised the RAM usage, by sharing information between the channels that was common. This gave me ~200 Bytes that I needed. \$\endgroup\$ – IntelliChick Nov 29 '10 at 0:14
  • \$\begingroup\$ If you have ascii files you can use 8 to 7 bit compression. Saves you 12.5%. Using a zip file would take more code to zip and un-zip it than to just let it be. \$\endgroup\$ – Sparky256 Jan 24 '18 at 3:39

17 Answers 17

18
\$\begingroup\$

You have a couple of options: first is to look for redundant code and move it to a single call to get rid of the duplication; the second is to remove functionality.

Take a good look at your .map file and see if there are functions that you can get rid of or rewrite. Also make sure that library calls which are being used are really needed.

Certain things like division and multiplications can bring in a lot of code but using shifts and a better use of constants can make the code smaller. Also have a look at things like string constants and printfs. For example each printf will eat up your rom but you might be able to have a couple of shared format strings instead of repeating that string constant over and over again.

For memory see if you can get rid of globals and use autos in a function instead. Also avoid as many variables in the main function as possible, as these eat up memory just like globals do.

\$\endgroup\$
  • 1
    \$\begingroup\$ Thanks for the suggestions, I can definitely try most of them, except the one for string constants. It is purely an embedded device, with no UI and so there are no calls to printf() within the code. Hoping that those suggestions should tide me over to get my 1400 Bytes Flash/200 bytes RAM that I need. \$\endgroup\$ – IntelliChick Nov 8 '10 at 4:00
  • 1
    \$\begingroup\$ @IntelliChick you would be amazed at how many people use printf() inside of an embedded device to print for either debugging or sending to a peripheral. It seems as you know better then this, but if anyone has written code on the project before you, it wouldn't hurt to check for it. \$\endgroup\$ – Kellenjb Nov 8 '10 at 5:06
  • 5
    \$\begingroup\$ And just to expand on my previous comment, you would also be amazed at how many people add debugging statements, but never remove them. Even people who do #ifdefs still get lazy at times. \$\endgroup\$ – Kellenjb Nov 8 '10 at 5:08
  • 1
    \$\begingroup\$ Awesome, Thanks! I have inherited this codebase, so ill definitely have a look for those. I will keep you guys posted, on the progress, and try to keep track of how many bytes of memory or Flash I gained by doing what, just as a reference for anyone else who might need to do this in the future. \$\endgroup\$ – IntelliChick Nov 8 '10 at 5:20
  • \$\begingroup\$ Just a question on this - what about nested function call hops from layer to layer. How much overhead does that add? Is it better to keep the modularity by having the multiple function calls or reduce the function calls, and save some bytes. And is that significant? \$\endgroup\$ – IntelliChick Nov 8 '10 at 5:25
8
\$\begingroup\$

It's always worth looking at listing file (assembler) output to look for things which your particular compiler is particularly bad at.

For example, you may find that local variables are very expensive, and if the application is simple enough to be worth the risk, moving a few loop counters into static variables might save a lot of code.

Or array indexing might be very expensive but pointer operations much cheaper. Or vice versa.

But looking at the assembly language is the first step.

\$\endgroup\$
  • 3
    \$\begingroup\$ It is very important that you know what your compiler does. You should see what divide is on my compiler. It makes babies cry(myself included). \$\endgroup\$ – Kortuk Nov 8 '10 at 17:42
8
\$\begingroup\$

Compiler optimisations, for example, -Os in GCC gives the best balance between speed and code size. Avoid -O3, as it may increase code size.

\$\endgroup\$
  • 3
    \$\begingroup\$ If you do this, you will need to retest EVERYTHING! Optimizations can cause working code to not work because of new assumptions the compiler makes. \$\endgroup\$ – Robert Nov 8 '10 at 16:10
  • \$\begingroup\$ @Robert, that is only true if you use undefined statements: e.g. a = a++ will compile differently in -O0 and -O3. \$\endgroup\$ – Thomas O Nov 8 '10 at 16:53
  • 5
    \$\begingroup\$ @Thomas not true. If you have a for loop to delay clock cycles, many optimizers will realize your not doing anything in it and remove it. This is just 1 example. \$\endgroup\$ – Kellenjb Nov 8 '10 at 17:47
  • 1
    \$\begingroup\$ @thomas O,You also need to make sure that you are careful about volatile function definitions. Optimizers will blow up those that think they know C well but do not understand the complexities of atomic operations. \$\endgroup\$ – Kortuk Nov 8 '10 at 18:00
  • 1
    \$\begingroup\$ All good points. Volatile functions/variables, by definition, must NOT be optimised. Any optimiser which performs optimisations on such (including call-time and inlining) is broken. \$\endgroup\$ – Thomas O Nov 8 '10 at 18:15
8
\$\begingroup\$

For RAM, check the range of all of your variables - are you using ints where you could use a char? Are buffers bigger than they need to be?

Code squeezing is very application and coding-style dependent. Your amounts left suggest that maybe the code has already gone though some squeezing, which may mean there's little left to be had.

Also take a hard look at the overall functionality - is there something that's not really used and can be jettisoned?

\$\endgroup\$
8
\$\begingroup\$

If it's an old project but the compiler has been developed since, it could be that a more recent compiler may produce smaller code

\$\endgroup\$
  • \$\begingroup\$ Thanks Mike! I have tried this in the past, and the space gained has already been used! :) Moved up from IAR C compiler 3.21d to 3.40. \$\endgroup\$ – IntelliChick Nov 8 '10 at 22:45
  • 1
    \$\begingroup\$ I moved up one more version, and have managed to get some more Flash to fit the feature in. I am really struggling with the RAM though, that has remained unchanged. :( \$\endgroup\$ – IntelliChick Nov 9 '10 at 4:14
7
\$\begingroup\$

It's always worth checking your compiler manual for options to optimise space.

For gcc -ffunction-sections and -fdata-sections with the --gc-sections linker flag are good for stripping dead code.

Here are some other excellent tips (geared towards AVR)

\$\endgroup\$
  • \$\begingroup\$ Does this actually work? The docs say "When you specify these options, the assembler and linker will create larger object and executable files and will also be slower." I understand that having separate sections makes sense for a micro with Flash and RAM sections - Is this statement in docs not applicable to microcontrollers? \$\endgroup\$ – Kevin Vermeer Nov 8 '10 at 15:55
  • \$\begingroup\$ My experience is that it works well for AVR \$\endgroup\$ – Toby Jaffey Nov 8 '10 at 16:50
  • 1
    \$\begingroup\$ This does not work well in most of the compilers I have used. It is like using the register keyword. You can tell the compiler that a variable goes into a register, but a good optimizer will do this much better than a human (argue as some may, it is considered not acceptable to do this in practice). \$\endgroup\$ – Kortuk Nov 8 '10 at 17:47
  • 1
    \$\begingroup\$ When you start assigning locations you are forcing the compiler to place things in certain locations, very important for advanced boot-loader code, but awful to deal with in the optimizer, as you make decisions for it you are taking away a step of optimization it could do. In some compilers they design it to have sections for what code is used for, this is a case of telling the compiler more information to understand your use, this will help. If the compiler does not suggest it, do not do it. \$\endgroup\$ – Kortuk Nov 8 '10 at 17:50
6
\$\begingroup\$

You can examine the amount of stack space and heap space that are allocated. You may be able to get a substantial amount of RAM back if either or both of these are over allocated.

My guess is for a project that fits into 2k of RAM to start with there is no dynamic memory allocation (use of malloc, calloc, etc.). If this is the case you can get rid of you heap altogether assuming the original author left some RAM allocated for the heap.

You have to be very careful reducing the stack size as this can cause bugs that are very difficult to find. It may be helpful to start by initializing the entire stack space to a known value (something other than 0x00 or 0xff as these values occur commonly already) then run the system for a while to see how much stack space is unused.

\$\endgroup\$
  • \$\begingroup\$ These are very good choices. I will note you should not ever ever use malloc in an embedded system. \$\endgroup\$ – Kortuk Nov 8 '10 at 17:37
  • 1
    \$\begingroup\$ @Kortuk That depends on your definition of embedded and the task being performed \$\endgroup\$ – Toby Jaffey Nov 9 '10 at 0:40
  • 1
    \$\begingroup\$ @joby, Yeah, I understand that. In a system with 0 restarts and the absence of an OS like linux, Malloc can be very very bad. \$\endgroup\$ – Kortuk Nov 9 '10 at 2:29
  • \$\begingroup\$ There is no dynamic memory allocation, no place where malloc, calloc is being used. I have also checked the heap allocation, and it has been set to 0 already, so there is no heap allocation. Currently allocated Stack size is 254 Bytes and the interrupt stack size in 128 bytes. \$\endgroup\$ – IntelliChick Nov 9 '10 at 4:16
5
\$\begingroup\$

Does your code use floating point math? You may be able to re-implement your algorithms using integer math only, and eliminate the overheads of using the C floating point library. E.g. in some applications, such functions as sine, log, exp can be replaced by integer polynomial approximations.

Does your code use large look-up tables for any algorithms, such as CRC calculations? You can try substituting a different version of the algorithm that calculates values on-the-fly, instead of using the look-up tables. The caveat is that the smaller algorithm is most likely slower, so make sure you have enough CPU cycles.

Does your code have large amounts of constant data, such as string tables, HTML pages, or pixel graphics (icons)? If it's large enough (say 10 kB), it could be worth implementing a very simple compression scheme to shrink the data and decompress it on-the-fly when needed.

\$\endgroup\$
  • \$\begingroup\$ There are 2 small lookup tables, neither of which will amount to 10K unfortunately. And no floating point maths is being used either. :( Thanks for the suggestions though. They are good. \$\endgroup\$ – IntelliChick Nov 9 '10 at 6:41
2
\$\begingroup\$

You can try to rearrange to code a lot, to a more compact style. It depends a lot on what the code is doing. The key is to find similar things and re-implement them in terms of each other. An extreme would be to use a higher level language, like Forth, with which it can be easier to achieve a higher code density than in C or assembler.

Here is Forth for M16C.

\$\endgroup\$
2
\$\begingroup\$

Set the optimization level of the compiler. Many IDE's have settings that allow for code-size optimizations at the expense of compile-time (or maybe even processing time in some cases). They can accomplish code compacting by rerunning their optimizer a couple of times, searching for less-common optimize-able patterns, and a whole other host of tricks that may not be necessary for the casual/debug compilation. Usually, by default, compilers are set to a medium level of optimization. Dig around in the settings an you should be able to find some integer-based optimization scale.

\$\endgroup\$
  • 1
    \$\begingroup\$ Currently optimised to the Maximum for Size. :) Thanks for the suggestion though. :) \$\endgroup\$ – IntelliChick Nov 8 '10 at 22:47
2
\$\begingroup\$

If you're already using a professional-level compiler like IAR, I think you're going to struggle to get any serious savings by minor low-level code-tweaking - you'll need to be looking more towards removing functionality or doing major rewrites of parts in a more efficient way. You'll need to be a smarter coder than whoever wrote the original version... As for RAM you need to take a very hard look at how it is currently used, and see if there is scope for overlaying usage of the same RAM for different things at different times (unions are handy for this). IAR's default heap and stack sizes in the ARM/AVR ones I've have tended to be over-generous, so these would be the first thing to look at.

\$\endgroup\$
  • \$\begingroup\$ Thanks Mike. The code is already using unions in most places, but I will have a look at some other places, where this might still help. I will also have a look at the stack size chosen and see if that can be optimised at all. \$\endgroup\$ – IntelliChick Nov 9 '10 at 0:59
  • \$\begingroup\$ How do I know what Stack size size is appropriate? \$\endgroup\$ – IntelliChick Nov 9 '10 at 3:56
2
\$\begingroup\$

Something else to check - some compilers on some architectures copy constants to RAM - typically used when access to flash constants is slow/difficult (e.g. AVR) e.g. IAR's AVR compiler requires a _ _flash qualifer to not copy a constant to RAM)

\$\endgroup\$
  • \$\begingroup\$ Thanks Mike. Yea I had already checked that - its called the 'Writeable constants' option for the M16C IAR C compiler. It copies the constants from ROM to RAM. This option is unchecked for my project. But a really valid check! Thanks. \$\endgroup\$ – IntelliChick Nov 9 '10 at 20:45
1
\$\begingroup\$

If your processor doesn't have hardware support for a parameter/local stack but the compiler tries to implement a run-time parameter stack anyway, and if your code doesn't need to be re-entrant, you may be able to save code space by statically allocating auto variables. In some cases, this must be done manually; in other cases, compiler directives can do it. Efficient manual allocation will require sharing of variables between routines. Such sharing must be done carefully, to ensure that no routine uses a variable which another routine considers to be "in scope", but in some cases the code-size benefits may be significant.

Some processors have calling conventions that may make some parameter-passing styles more efficient than others. For example, on the PIC18 controllers, if a routine takes a single one-byte parameter, it may be passed in a register; if it takes more than that, all parameters must be passed in RAM. If a routine would take two one-byte parameters, it may be most efficient to "pass" one in a global variable, and then pass the other as a parameter. With widely-used routines, the savings can add up. They can be especially significant if the parameter passed via global is a single-bit flag, or if it will usually have a value of 0 or 255 (since special instructions exist to store a 0 or 255 into RAM).

On the ARM, putting global variables which are frequently used together into a structure may significantly reduce code size and improve performance. If A, B, C, D, and E are separate global variables, then code which uses all of them must load the address of each into a register; if there aren't enough registers, it may be necessary to reload those addresses multiple times. By contrast, if they are part of the same global structure MyStuff, then code which uses MyStuff.A, MyStuff.B, etc. can simply load the address of MyStuff once. Big win.

\$\endgroup\$
1
\$\begingroup\$

1.If your code relies on a lot of structures, make sure the structure members are ordered from the ones that occupy most memory to the least.

Ex: "uint32_t uint16_t uint8_t" instead of "uint16_t uint8_t uint32_t"

This will ensure minimum structure padding.

2.Use const for variables where applicable. This will ensure that those variables will be in ROM and not eat up RAM

\$\endgroup\$
1
\$\begingroup\$

A few (perhaps obvious) tricks I've used successfully in compressing some customer's code:

  1. Compact flags into bit fields or bit masks. This may be beneficial as usually booleans are stored as integers thus wasting memory. This will save both RAM and ROM and isn't usually done by the compiler.

  2. Look for redundancy in the code, and use loops or functions to execute repeated statements.

  3. I've also saved some ROM by replacing many if(x==enum_entry) <assignment> statements from constants with an indexed array, by taking care that the enum entries could be used as the array index

\$\endgroup\$
0
\$\begingroup\$

If you can, use inline functions or compiler macros instead of small functions. There's size and speed overhead with passing arguments and such that can be remedied by making the function inline.

\$\endgroup\$
  • 1
    \$\begingroup\$ Any decent compiler should do this automatically for funcitons that are called only once. \$\endgroup\$ – mikeselectricstuff Nov 8 '10 at 15:39
  • 5
    \$\begingroup\$ I've found inlining is usually more useful for speed optimisations, and usually at the cost of increased size. \$\endgroup\$ – Craig McQueen Nov 8 '10 at 23:35
  • \$\begingroup\$ inlining will typically increase code size, except with trivial functions like int get_a(struct x) {return x.a;} \$\endgroup\$ – Dmitry Grigoryev Jan 23 '18 at 9:03
0
\$\begingroup\$

Change the local variables to be the same size of your CPU registers.

If the CPU is 32-bit, use 32-bit variables even if the max value will never get above 255. I you used an 8-bit variable, the compiler will add code to mask off the upper 24 bits.

The first place I would look is at the for-loop variables.

for( i = 0; i < 100; i++ )

This might seem like a good place for an 8-bit variable, but a 32-bit variable might produce less code.

\$\endgroup\$
  • \$\begingroup\$ That may save code but it will eat RAM. \$\endgroup\$ – mikeselectricstuff Nov 9 '10 at 14:46
  • \$\begingroup\$ It will only eat RAM if that function call is in the longest branch of the call trace. Otherwise, it is reusing stack space that some other function already needs. \$\endgroup\$ – Robert Nov 9 '10 at 15:16
  • 2
    \$\begingroup\$ Usually true providing it's a local variable. If short on RAM, the size of global vars, especially arrays, is a good place to start looking for savings. \$\endgroup\$ – mikeselectricstuff Nov 9 '10 at 15:19
  • 1
    \$\begingroup\$ Another possibility, interestingly, is to replace unsigned variables with signed. If a compiler optimizes an unsigned short to a 32-bit register, it must add code to ensure that its value wraps from 65535 to zero. If, however, the compiler optimizes a signed short to a register, no such code is required. Because there is no guarantee what will happen if a short is incremented beyond 32767, compilers are not required to emit code to deal with that. On at least two ARM compilers I've looked at, signed-short code can be smaller than unsigned-short code for that reason. \$\endgroup\$ – supercat Aug 29 '11 at 15:25

Not the answer you're looking for? Browse other questions tagged or ask your own question.