Embedded C++ Yields Faster Smaller Code - Embedded.com

Embedded C++ Yields Faster Smaller Code

In many embedded system designs today, programmers can leverage all the features of C++ such as classes, templates, exception handling, and class inheritance that have proven so useful for mainstream computer applications. Some of these C++ features, however, bloat the compiled code significantly increasing memory requirements while reducing execution speed. A new C++ dialect called Embedded C++ (EC++) has been developed by an industry standards committee to address the limitations of C++ in some embedded applications where memory is limited and 32-bit processors are prevalent. EC++ proposes to maintain the most valuable C++ concepts while eliminating those most responsible for boosting memory requirements and reducing efficiency. Ideally, designers will be able to choose whether to use EC++, C++, or a hybrid of the two to match specific application requirements.

Generally, the object-oriented features of C++ simplify the source code and thereby the development process, both by allowing reuse of many code modules and by placing the somewhat burdensome housekeeping functions such as memory allocation and range checking in the class definitions and away from the mainline application. While C++ code is typically more readable than standard C code, the compiled C++ code can swell by a factor of five or more relative to a standard C implementation. This factor caused a group of companies led largely by Japanese microprocessor vendors to develop the EC++ specification, and to prompt the development of the first EC++ compiler by Green Hills Software.

EC++ is a proper subset of C++. Among the C++ features that EC++ omits are multiple inheritance, virtual base classes, templates, exceptions, runtime type identification, virtual function tables, and mutable specifiers. While each of these features is useful in its own right, none is compelling for a sufficiently broad range of embedded applications. Support for some of these features tends to bloat the generated code, whether or not the features are actually used in an application. Exception handling proves to be one of the worst offenders, and can adversely affect the deterministic response to external events required in real-time systems. Eliminating support for a number of C++ features yields substantial reductions in the size of the compiled code and corresponding improvements in runtime efficiency.

To understand the range of features that differentiate standard C, EC++, and C++, consider a series of examples that start with a baseline EC++ implementation and progress to a full C++ implementation. The code fragment in Example 1 illustrates some of the key advantages that EC++ offers over the straight C language. The concept of classes is probably the single most important concept originally brought about by C++ and one that is supported in EC++.

Classes build on the data structures found in the standard C language. In addition to allocating memory for a number of variables of mixed types, classes can be used to initialize variables, dynamically allocate additional memory for variables and arrays, perform range checking, and perform many other useful duties. In C programs, these tasks have typically been scattered throughout the main code.

Classes and Object Definition

Example 1 illustrates the use of classes for an array operation. An embedded application such as a data-acquisition system might use such a class to create arrays for storage of data samples. The array class named Array includes two integer members. The first is a pointer named “elements” to members of an array and the second is used to track the array size.

As the main portion of the code fragment implies, you can use the declaration, Array a(6), to create an array object with the name “a” that contains 6 elements. The class definition includes several important features such as the constructor code necessary to create the array and to ensure that the size specified is greater than zero. The constructor is located in the public section of the class definition which allows code located outside the class definition a window through which it can access the elements and element_cnt class members. Each time an array of type Array is created, the compiler automatically calls the constructor function Array(int n). This function first assigns the value passed in the array declaration to element_cnt, then checks for a valid size, and finally allocates space in main memory for the array by calling “new”.

In this simple example, a bad array size such as zero or a negative number causes the constructor to call a simple “die” function that outputs an error message. An embedded system would likely use a more elaborate scheme to handle runtime errors.

The Array class also demonstrates two other key features of classes in EC++ or C++—function definitions within a class and overloaded operators. First consider the “size()” function which illustrates the simpler of the two concepts. Because element_cnt is a private member of the class, code outside the class can't directly access the counter. The size() function, however, allows the two “for” loops located in the main() section to indirectly access element_cnt for use as an upper limit of the loop.

Overloaded Operators

The class definition also includes an example of overloaded operators. Operator overloading allows the programmer to develop new definitions of standard C/C++ operators such as “=”, “>”, or “+” that are customized for the type of object defined in a class. For example, you could develop a class that defined an object such as a circle or sphere. Having done so, you might want to compare the size of two objects that were created using the class definition—objects A and B. The expression A>B or A=B have well understood meanings when A and B are integers, but the compiler could have trouble evaluating the expressions when A and B are spheres of given size or composition. To eliminate ambiguity, EC++ and C++ allows the programmer to include a new definition for such operators that works in a way that's advantageous to the specific application and object type.

The sample code in Example 1 overloads the subscripting operator “[ ]” used to store the index for an array. In this case, the overloaded function gets called each time an indexed array reference occurs—for example “a[i]=i. Rather than changing the effective meaning of the subscripting operator, the example uses the overloaded operator to automatically detect for out-of-range array indices. Note that should you execute the sample program, the output statement used in the second loop would generate an error on the sixth pass through the loop because a[7] would exceed the valid index test.

As you can see, classes significantly streamline the mainline code in a EC++ or C++ program. For example, a C program would require explicit data-structure definitions for every array declared while EC++ or C++ handles creation of all similar objects with a single class. Moreover, C programs would require memory-allocation, error-checking, and element-count code in the main part of the program or in dedicated C functions. The compiled code overhead of EC++ relative to standard C code is minimal as well. Adding classes and overloaded operators only marginally increases the generated code size.

Why You Need EC++

Since EC++ is a proper subset of C++, you might think that you can achieve the efficiency promised by EC++ by simply avoiding certain C++ features. You can certainly compile EC++ code on a C++ compiler but you will find that the resulting code still requires 5 or 6 times more memory than the same code compiled with an EC++ compiler.

Three problems arise when you try to increase efficiency using EC++ code and a standard C++ compiler. First, the compiler will still use C++ libraries and link a multitude of code into the finished product that isn't required for your application. EC++, conversely, uses libraries that are optimized for the new dialect and thereby generates smaller more efficient code.

Second, EC++ compilers can achieve superior optimization relative to C++ compilers. EC++ compilers can optimize code without presuming the possibility that complex features such as exception handling may be used at some point. Third, standard C++ compilers have no way of enforcing EC++ compliance within a programming team. With a standard C++ compiler, one out of 10 or 20 programmers on a team can use an offending feature and spoil the efficiency of the entire code base.

Green Hills developed a sample EC++ program to demonstrate memory efficiency. The program solves a form of the classical “traveling salesman” problem that's often use to teach programming. When compiled on Green Hills C++/EC++ compiler using EC++ mode the total code size is 57 kbytes. When compiled using the identical EC++ source file but in C++ mode with no exception handling library, the code size is 322 kbytes. Adding an exception handling library brings the code size to 378 kbytes.

Finding the Right Mix for Your Application

EC++ promises to provide embedded-system programmers a valuable path to leverage the most significant aspects of an object-oriented language. With formal approval of the EC++ standard imminent, programmers should demand C++ compilers that include EC++ support. To minimize development time and simplify code maintenance, however, programmers shouldn't dismiss all of the C++ features that were eliminated in EC++. By carefully choosing which features to use on an application-by-application basis, programmers can both simplify development and maintain reasonable runtime efficiency. Moreover, complex embedded systems easily benefit from the robust libraries and features of C++. Green Hills' C++/EC++, at beta sites now, offers full support for the EC++ specification, and the flexibility to re-activate individual features of the C++ language. Moreover, programmers can select from a variety of libraries to match the feature set being used.

Understand that you can't realize these code savings simply by not using the memory-hungry C++ features in an application and then compiling the code with a standard C++ compiler. The sidebar, “Why you need an EC++ compiler” details the consequences of trying this approach.

Adding Template Support

Despite the advantages offered by EC++, some omitted C++ functions prove to be extremely attractive for given applications. Compiler vendors can provide programmers some flexibility as to what C++ functions are available for use in each application. For example, a programmer can use a compiler directive with Green Hills' C++/EC++ compiler to strictly limit the source code to the EC++ subset and fully realize the savings in code size and the boost in efficiency. Additionally, a programmer can use compiler switches to add support for one or a few specific C++ functions that were left out of EC++. The granular support for optional C++ features allows the programmer to trade off compiled code size with ease of development and maintainability. Furthermore, Green Hills' C++/EC++ allows the programmer to choose libraries appropriate to the application thereby eliminating significant amounts of unneeded library code.

Templates provide a good example of a C++ feature that is not included in EC++ yet provides significant advantages in development with only a modest increase in memory requirements when used with care. Example 2 illustrates the benefit of templates. In Example 1, the Array class was defined in such a way that all members of the array had to be integers. With EC++, you would have to define additional classes simply to handle arrays for short, long, char or floating-point data types. Templates allow a single class definition to support creation of arrays for any valid C++ data type.

The only real addition to the Array class definition with a template is the template label that precedes the class definition and the use of the “T” specifier each time the code addresses an element of the array. Consider the main() code, however, and you will see that Array works equally well to instantiate array “a1” to store integer data types and array “a2” to store short data types. You could just as easily define more arrays to store other data types. An embedded system performing data acquisition, for example, could very well require floating-point arrays.

The use of templates illustrated here would result in little or no increase in compiled code size, so programmers that don't need to strictly meet the EC++ spec can leverage a valuable tool. Beware, however, that the code size you realize will depend specifically on your application. Much of the code bloat found in C++ code comes not from using a feature such as a template but from referencing templates that are found in large C++ libraries. Reference one of these standard templates, and you end up compiling many things in the library that you don't need.

Experienced C++ programmers will find they can enable a number of C++ features in a EC++ environment, and not decrease efficiency. You might call such an environment extended EC++. The EC++ development effort eliminated features such as templates, namespaces, mutable specifiers, and new-style casts more so due to the complexity of using the features properly than due to inherent inefficiency. C programmers may have a tough time using the features correctly, but careful, experienced C++ programmers can leverage the benefits of these features with no penalty. Moreover, compliers such as Green Hills' C++/EC++ makes extending EC++ as simple as using compiler switches.

Full C++ with Exception Handling

You may wonder what other goodies become available as you move toward full C++. Exception handling proves to be among the most valuable features to embedded system designers yet is also among the leading causes of compiled code bloat. Exception handling provides a systematic approach to trapping errors caused by operator input or even out-of-range errors in a data-acquisition environment.

The code fragment in Example 3 illustrates C++ exception handling. Moreover, the example is more typical of the kind of error handling required in complex embedded systems than was the simple die() function used in the first two examples.

C++ defines the keywords “try”, “throw”, and “catch” for use in exception handling. Typically, programmers organize code within blocks called try blocks that are enclosed within braces—{ }. A second block of code called the catch block is dedicated as a centralized runtime exception/error dispatch service. Anywhere within the try block, a throw directive can originate an exception condition and transfer control to the catch block based on evaluation of a C++ “if” statement. The throw statements can be in-line within the try block or located in functions within the class definition.

Example 3 uses the throw mechanism at two different places in the Array class definition. The first throws to the catch block when an Array instantiation has zero or a negative number of elements. This throw is located in the constructor function. The second throw is used to handle array index errors detected by the overloaded array subscripting operator.

C++ offers significant flexibility in how exceptions are handled and in all cases allows you to separate the exception handling code from the mainline application. As in the example, you can dedicate a catch block to each try block. Alternatively, you can define a single catch block to service an entire main() program. The code in a catch block only executes when a throw evaluation fails. For example, should the code within the first try block in Example 3 execute with no exception the following catch block will be skipped and the second try block will execute.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.