Optimizing an application after analysis

The memory-analyzing tools tell you how much total memory a process is using, the sizes of its memory segments, and the history and breakdown of its heap usage. This knowledge helps you determine what programming steps are needed to reduce an application's memory footprint, which can greatly improve performance.

Memory efficiency is often critical in embedded systems, where memory is limited (especially with the absence of swapping) and many processes need to run continuously. The optimization steps you'll want to take depend on what the analysis results reveal about memory type distribution. For example, you can spend considerable time optimizing the heap but if your program uses more static memory than it should, this other problem must be dealt with.

Memory distribution of processes

Virtual memory occupied by a process is separated into these categories:
  • Code — Executable code (instructions) belonging to the application or static libraries.
  • Shared Code — Executable code from shared libraries. If many processes use the same library, their virtual segments containing its code are mapped to the same physical segment.
  • Data — A data segment for the application and data segments for the shared libraries. This memory type is usually referred to as static memory.
  • Stack — Memory required for function stacks (there's one stack per thread).
  • Heap — All memory dynamically allocated by the process.
  • Shared Heap — Other memory allocated by different means, including shared and mapped memory.

The IDE has several tools for viewing process memory distribution. In the System Information, the Memory Information view shows the memory breakdown by type and provides details about individual segments. Note that “type” is different from virtual memory category; the correspondance is given in How memory types relate to virtual memory categories.

You can view the heap distribution through the Malloc Information view, which displays the used, overhead, and free heap memory sizes. The Memory Analysis tool graphs this same information as well as all heap allocations and deallocations, in an interactive editor window. Through the Valgrind UI controls, you can run Massif to collect heap snapshots, then analyze the heap breakdown measured at the detailed snapshots.

After examining the memory distribution data with these tools, you should focus on the areas of high consumption for nonshared memory. Note that “nonshared memory” can include stack and heap memory used by shared libraries. This term covers anything not created as a shared memory object; this last concept is explained in the Shared memory entry of the System Architecture guide. Optimizing shared memory is unlikely to notably reduce the overall memory consumption on the target machine.

The techniques for improving memory efficiency greatly vary for different memory types. We outline some of these techniques below.

Heap optimizations

You can use the following techniques to optimize the heap:
Eliminate explicit memory leaks
The easiest way to begin optimizing the heap is to eliminate explicit memory leaks, which occur when blocks become inaccessible because their pointer values aren't kept properly. Memory Analysis lets you check for leaks at fixed intervals and outputs a list of memory errors and tags any leaks with a keyword. Valgrind Memcheck can check for specific leak types, to identify leaks resulting from incorrect pointer values or broken pointer chains.
Eliminate implicit memory leaks
After fixing the explicit leaks, you should fix the implicit leaks. These are leaks caused by heap objects that keep growing in size but remain accessible through pointers. To find such cases, Memory Analysis lets you filter the results to see only events for unmatched allocations or deallocations or for blocks that remain in memory for the program's duration. Viewing these events lets you find places where the program is steadily accumulating memory.
Valgrind Massif gathers heap data that reveal the change in heap breakdown over time, which helps you spot increasing memory usage at precise locations. Note that the Valgrind User Manual refers to these situations as space leaks.
Reduce heap fragmentation
Heap fragmentation occurs when a process accumulates many free blocks of varying size in noncontiguous addresses. In this case, the process will often allocate another physical page even if it seems to have enough free memory.
The QNX Neutrino memory allocator already solves most of this problem by preallocating many small, fixed-size blocks known as bands. Using bands lets the allocator quickly find a free block that fits the request size well, thereby minimizing fragmentation.
In the Memory Analysis editor, you can inspect the heap fragmentation by reviewing the Bins or Bands graphs. An indication of serious fragmentation is if the number of free blocks of smaller sizes grows over time. To deal with this, you can reorder heap allocations in your program. By allocating the largest blocks first, you'll reduce how often the allocator must divide large blocks into smaller ones. Whenever this happens, the smaller blocks can't be used later for bigger blocks because the address space is not contiguous.
If your program logic allows for it, you can store data in multiple smaller structures that each fit within the largest preallocated band size (typically, 128 bytes). Whenever a request exceeds this size, the block is allocated in the general heap list, which means a slower allocation and more fragmentation.
Reduce the overhead of allocated objects
There are several sources of overhead for heap-allocated objects:
  • User overhead — The application might request more heap memory than it really needs. This often results from predictive algorithms, such as those used by realloc(). You can reduce this overhead by better estimating the average data size. To do this for a particular call chain, examine the related allocation backtraces in the Memory Backtrace view. Or, if your data model allows it, truncate the memory to fit into the actual size of the object, after the data growth stops.
  • Padding overhead — In programs that run on processors with alignment restrictions, the fields in a struct type can get arranged in a way that makes the overall size of the structure larger than the sum of the sizes of its individual fields. You can save some space by rearranging the fields; usually, it's better to put fields of the same type together. You can measure the result by writing a sizeof test. Typically, this task is valuable when the resulting overall size matches a preallocated band size (see below).
  • Block overhead — Sometimes there's extra space in heap blocks because the memory allocated is more than what's requested. In the Memory Analysis results, the Memory Events view shows the requested versus actual allocation sizes and the Usage tab shows what percentage of the heap is overhead (extra space). Whenever possible, choose an allocation size that matches a size for preallocated bands (you can see their sizes in the Bands tab), especially for realloc() calls. Also, if you can, try to align data structures with these band sizes.
Tune the allocator
Occasionally, application-driven data structures have fixed sizes and you can improve memory efficiency by customizing the allocated block sizes. Or, your application may experience free blocks overhead, when a lot of memory has been freed by the code but the process hasn't returned many pages. This happens if the process doesn't reach the “low watermark” on heap usage, which causes it to return some pages. In these two cases, you must either write your own allocator or contact QNX Software Systems to obtain a customizable allocator.
To estimate the benefits of custom block sizes, configure Memory Analysis to report the allocation counts for the appropriate size ranges, by setting the Bins counters field in the Memory Snapshots controls. Then, examine the Bins tab in the analysis results to see the distribution of heap objects within the bins (size ranges) that you specified.

Code optimizations

In embedded systems, it's very important to optimize the size of an executable or library binary because it uses not only RAM memory but expensive flash memory. You can use the following techniques:
  • Ensure that the binary file is compiled without debug information when you measure it. Debug information is the largest contributor to file size.
  • Strip the binary to remove any remaining symbol information.
  • Remove any unused functions.
  • Find and eliminate code clones.
  • Try setting compiler optimization flags (e.g., -O, -O2). Note that there is no guarantee that the code will be smaller; it can actually be larger in some cases.
  • Don't use the char type to perform int arithmetics, particularly for local variables. Converting between these types requires the compiler to insert code, which affects performance and code size, especially on ARM processors.
  • Bit fields are also very expensive in arithmetics on all platforms; it's better to use bit arithmetics explicitly to avoid hidden costs of conversions.

Data optimizations

Static memory can produce significant overhead, similar to heap or stack memory. You can take some steps to reduce the size of an application's data segments:
  • Inspect global arrays that consume a lot of static memory. It may be better to use the heap, particularly for objects that aren't used throughout the program's entire lifetime.
  • Find and remove unused global variables.
  • Determine if any structures have padding overhead. If so, consider rearranging their fields to achieve a smaller overall size.

Stack optimizations

Sometimes, it's worth the effort to optimize the stack. For example, your application may have frequent high peaks in stack activity, meaning that large stack segments constantly get mapped to physical memory. These situations can be hard to detect through conventional testing. Although the program might run properly during testing, the system could fail in the field, likely when it's busiest and needed the most.

You can watch the Memory Information view for stack allocation statistics and then locate and fix code that uses the stack heavily. Typically, heavy stack usage occurs in two situations: recursive calls, which should be avoided in embedded systems, and usage of many large local variables, such as arrays kept on the stack.