C++ tip: Considering the use of exceptions, asserts, and bounds checking

Topics: C/C++
Technologies: C++

For public API methods, good programming practice recommends always checking incoming arguments for validity before using them. When a problem is found, code may return an error code, throw an exception, or abort with an assert. But when the arguments are valid, what is the runtime cost of this checking? Does declaring that a method can throw an exception slow down the method even if one isn't thrown? This article does quick benchmarking to look at some runtime costs associated with argument checking, exceptions, and asserts.

Exceptions and asserts

There are three widely-used styles for checking and reporting invalid method arguments:

  1. Use "if" statements to check arguments. On an error, return an error code.
  2. Use "if" statements to check arguments. On an error, throw an exception.
  3. Use "assert" statements to check arguments. On an error, abort the application.

There are advocates of each approach. For this article, it doesn't matter which is stylistically "best".

What is the runtime cost of these approaches for valid arguments?

By far, most method calls will have valid arguments. So, what's the cost of these approaches when arguments are good? Is there any difference at all?

To see, let's do a quick benchmark for all three approaches to checking arguments, with a few variations. For all cases, we'll benchmark a simple "get" method that returns a container object's internal state. This keeps the "get" method body cheap so that benchmarking reveals the different overheads of argument checking approaches.

Benchmarked approaches are:

  1. Direct access to the object's state value, without a method call. This is usually poor practice, but it provides a fastest-case for comparisons.
  2. Inlined method call without argument checking.
  3. Inlined method call with "if" statement argument checking and returning an error code.
  4. Inlined method call with "assert" macros that abort the application on an error.
  5. Inlined method call with a "throw" signature but no argument checking or "throw" statements.
  6. Inlined method call with a "throw" signature, "if" statement argument checking, and a "throw" on an error.

Note that the benchmark is not timing actually throwing an exception or aborting on an assert. We want to know the cost of not failing.

The "get" method tested does 3D array indexing. The exception-throwing version reads:

inline float get( const size_t i, const size_t j, const size_t k )
	const throw( std::runtime_error )
{
	if ( i >= width || j >= height || k >= depth )
		throw new std::runtime_error( "Out of bounds" );
	return this->data[ i + j*width + k*width*height ];
}

The "get" method version using the ISO standard "assert" macro from the <cassert> include reads:

inline float get( const size_t i, const size_t j, const size_t k )
	const noexcept
{
	assert( i < width );
	assert( j < height );
	assert( k < depth );
	return this->data[ i + j*width + k*width*height ];
}

The benchmark sweeps through all 3D array indexes for a large array, in storage order.

Results

The plot below shows the approaches benchmarked using g++, clang++, and icpc with maximum compiler optimizations. The vertical axis is run time and lower is better.

The axis numbers are intentionally omitted since the exact run time will vary based on the processor and "get" method. What matters is general notions of "high" and "low" for the approaches.

Observations

The "Direct" and "No check" approaches (left two sets of bars) are the same, as they should be. Neither one does bounds checking, exceptions, or asserts. The "Direct" approach accesses object state directly, while "No check" uses a "get" method call that's inlined to do the same thing.

The "No check, throw signature" case (second from end) is as fast as "Direct" and "No check". This code only differs by including a "throw" signature on the method declaration, and a try-catch block on the call. There is no "throw" within the method body. It is apparent that the signature and try-catch have no performance impact by themselves. The compiler ignores the "throw" signature if there is no throw in the method body (or in anything that body calls).

The "Check & return" approach (third from left) includes bounds checking, which makes it slower. The "Assert" approach (fourth from left) does the same bounds checking via the "assert" macros. Unexpectedly, it does much better with g++ and a little better with clang++. Since the method is inlined, and "assert" is a macro, it is possible for a compiler to rearrange code to move invariants outside of loops in the calling code. In this benchmark, the "get" method on a 3D array was tested in a loop over all array indexes. Moving "assert" bounds checking invariants out of inner loops is possible with aggressive compiler optimization.

Finally, the "Check & throw" approach (far right) is the most expensive by a little bit.

Conclusions

Good programming practice demands bounds checking, but it has a runtime cost. For simple methods, such as the "get" tested, the performance impact can be significant.

Throwing an exception is slightly more expensive than using an "assert" or error return code approach. The difference is not significant enough to be the main criteria for choosing between the approaches.

For debugged code, and in high-performance situations, it is useful to disable bounds checking. With "assert", this is done easily by not defining "NDEBUG" before including <cassert>. For the other approaches, this is easily done with an #ifdef around the bounds checking code. When the #ifdef removes exception code, the method's "throw" signature remains, but this benchmark shows its presence has no performance impact.

Comments

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.
  • Web page addresses and e-mail addresses turn into links automatically.

More information about formatting options

Nadeau software consulting
Nadeau software consulting