C++ exceptions and overhead

There are numerous ways for error handling in C++. From day one AnKi used an almost C-like way to handle errors and the standard error to report them. The truth is that we never cared to recover from errors, the only concern was and still is to report the errors and be able to identify the cause of the problem without the use of a debugger. What about exceptions then? Are they a nice way for error handling? Lets see…

The C-like way of error handling

The main idea is to return true on success and if an error happens it will be reported by the function that caught it first.

bool doSomething(...)
{
	...
	if(error)
	{
		cerr << "Something went wrong" << endl;
		return false;
	}
	...
	return true;
}

If someone calls the function “doSomething” it needs to check the return flag to check is an error happened. It is possible to report the error again and have a nice backtrace of the error.

Error (Texture.cpp:126 createEmpty2D): OpenGL Err: invalid value
Fatal (Ms.cpp:28 init): Failed to create one MS FAI. See prev error. Bye!

In the above example the function createEmpty2D failed for some reason, reported the error and returned false. The init function, who is the caller of the createEmpty2D, caught the false and re-reported the error. With this simple trick we have a way to keep a backtrace and immediately we know who failed and who started it.

Exceptions

For some subsystems this C-like way of handling errors is ok but its very dirty for library-like subsystems. Exceptions is a nice alternative. We throw an exception and the caller is responsible of handling it. I wont go on with details of how exceptions work, this is not a tutorial but this is how doSomething looks like:

void doSomething(...)
{
	...
	if(error)
	{
		throw std::runtime_error("Something went wrong");
	}
	...
}

Obviously the code is less cause you save the returns, also you wont find yourself in the unpleasant position of forgetting a return or returning the incorrect boolean. There is an issue of a certain overhead when using exceptions. Lets see how much overhead exceptions add.

I’ve created a simple example and tried to read the assembly code.

try
{
	printf("in\n");
	int r = rand() % 2 + 1;
	doSomething(r);
	printf("out\n");
}
catch(std::exception& e)
{
	printf("in\n");
	fprintf(stderr, "%s\n", e.what());
	printf("out\n");
}

The assembly:

			printf("in\n");
0x000000000040263a <main+42>:  mov    $0x405a82,%edi
0x000000000040263f <main+47>:  callq  0x402280 <puts@plt>
			int r = rand() % 2 + 1;
0x0000000000402644 <main+52>:  callq  0x402040 <rand@plt>
0x0000000000402649 <main+57>:  mov    %eax,%edx
0x000000000040264b <main+59>:  sar    $0x1f,%edx
0x000000000040264e <main+62>:  shr    $0x1f,%edx
0x0000000000402651 <main+65>:  add    %edx,%eax
0x0000000000402653 <main+67>:  and    $0x1,%eax
0x0000000000402656 <main+70>:  sub    %edx,%eax
0x0000000000402658 <main+72>:  add    $0x1,%eax
0x000000000040265b <main+75>:  mov    %eax,-0x18(%rbp)
			doSomething(r);
0x000000000040265e <main+78>:  mov    -0x18(%rbp),%eax
0x0000000000402661 <main+81>:  mov    %eax,%edi
0x0000000000402663 <main+83>:  callq  0x402494 <_Z11doSomethingi>
			printf("out\n");
0x0000000000402668 <main+88>:  mov    $0x405a85,%edi
0x000000000040266d <main+93>:  callq  0x402280 <puts@plt>
0x0000000000402672 <main+98>:  jmpq   0x4026f7 <main+231>
0x0000000000402677 <main+103>: cmp    $0x1,%rdx
0x000000000040267b <main+107>: je     0x402685 <main+117>
0x000000000040267d <main+109>: mov    %rax,%rdi
0x0000000000402680 <main+112>: callq  0x402330 <_Unwind_Resume@plt>
		catch(std::exception& e)
0x0000000000402685 <main+117>: mov    %rax,%rdi
0x0000000000402688 <main+120>: callq  0x402060 <__cxa_begin_catch@plt>
0x000000000040268d <main+125>: mov    %rax,-0x20(%rbp)
0x00000000004026d8 <main+200>: callq  0x4022c0 <__cxa_end_catch@plt>
0x00000000004026dd <main+205>: jmp    0x4026f7 <main+231>
0x00000000004026df <main+207>: mov    %edx,%ebx
0x00000000004026e1 <main+209>: mov    %rax,%r12
0x00000000004026e4 <main+212>: callq  0x4022c0 <__cxa_end_catch@plt>
0x00000000004026e9 <main+217>: mov    %r12,%rax
0x00000000004026ec <main+220>: movslq %ebx,%rdx
0x00000000004026ef <main+223>: mov    %rax,%rdi
0x00000000004026f2 <main+226>: callq  0x402330 <_Unwind_Resume@plt>
			printf("in\n");
0x0000000000402691 <main+129>: mov    $0x405a82,%edi
0x0000000000402696 <main+134>: callq  0x402280 <puts@plt>
			fprintf(stderr, "%s\n", e.what());
0x000000000040269b <main+139>: mov    -0x20(%rbp),%rax
0x000000000040269f <main+143>: mov    (%rax),%rax
0x00000000004026a2 <main+146>: add    $0x10,%rax
0x00000000004026a6 <main+150>: mov    (%rax),%rdx
0x00000000004026a9 <main+153>: mov    -0x20(%rbp),%rax
0x00000000004026ad <main+157>: mov    %rax,%rdi
0x00000000004026b0 <main+160>: callq  *%rdx
0x00000000004026b2 <main+162>: mov    %rax,%rdx
0x00000000004026b5 <main+165>: mov    0x205ddc(%rip),%rax        # 0x608498 <stderr@@GLIBC_2.2.5>
0x00000000004026bc <main+172>: mov    $0x405a89,%esi
0x00000000004026c1 <main+177>: mov    %rax,%rdi
0x00000000004026c4 <main+180>: mov    $0x0,%eax
0x00000000004026c9 <main+185>: callq  0x402240 <fprintf@plt>
			printf("out\n");
0x00000000004026ce <main+190>: mov    $0x405a85,%edi
0x00000000004026d3 <main+195>: callq  0x402280 <puts@plt>

For doSomething there is practically no difference, its like a normal call. The try branch does not add any overhead, nevertheless, the catch does.

Lets try another test case. We have the two different samples to compare them. Note that in both cases we wont get an exception, this means that we will never go into a catch block.

for(int i=0; i<BIG_NUMBER; i++)
{
	try
	{
		int r = rand() % 2 + 1;
		doSomething(r);
	}
	catch(std::exception& e)
	{
		fprintf(stderr, "%s\n", e.what());
		break;
	}
}
try
{
	for(int i=0; i<BIG_NUMBER; i++)
	{
		int r = rand() % 2 + 1;
		doSomething(r);
	}
}
catch(std::exception& e)
{
	fprintf(stderr, "%s\n", e.what());
}

The thing to now is where the try/catch block is located. In the first case we have the try/catch inside the loop and in the second outside. After some benchmarks we saw that practically there is no speed difference. This indicates that the try block does not add any significant overhead.

Conclusion

In AnKi we dont care to recover from an error. The only thing that matters is to report the error correctly and nothing else. Exceptions make the code more simple, I guess, and in our case they dont add any noticeable overhead.

  1. eastlandgrl

    interesting, thanks

Leave a Comment


NOTE - You can use these HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>