I often find myself writing a code that looks something like this:
if(a == nullptr) throw std::runtime_error("error at " __FILE__ ":" S__LINE__);
Should I prefer handling errors with if unlikely
?
if unlikely(a == nullptr) throw std::runtime_error("error at " __FILE__ ":" S__LINE__);
Will the compiler automatically deduce which part of the code should be cached or is this an actually useful thing to do? Why do I not see many people handling errors like this?
Yes you can do that. But even better is to move the throw
to a separate function, and mark it with __attribute__((cold, noreturn))
. This will remove the need to say unlikely()
at each call site, and may improve code generation by moving the exception throwing logic entirely outside the happy path, improving instruction cache efficiency and inlining possibilities.
If you prefer to use unlikely()
for semantic notation (to make the code easier to read), that's fine too, but it isn't optimal by itself.
Should I use "if unlikely" for hard crashing errors?
For cases like that I'd prefer moving code that throws to a standalone extern function that's marked as noreturn
. This way, your actual code isn't "polluted" with lots of exception-related code (or whatever your "hard crashing" code). Contrary to the accepted answer, you don't need to mark it as cold
, but you really need noreturn
to make compiler not to try generating code to preserve registers or whatever state and essentially assume that after going there there is no way back.
For example, if you write code this way:
#include <stdexcept>
#define _STR(x) #x
#define STR(x) _STR(x)
void test(const char* a)
{
if(a == nullptr)
throw std::runtime_error("error at " __FILE__ ":" STR(__LINE__));
}
compiler will generate lots of instructions that deal with constructing and throwing this exception. You also introduce dependency on std::runtime_error
. Check out how generated code will look like if you have just three checks like that in your test
function:
First improvement: to move it to a standalone function:
void my_runtime_error(const char* message);
#define _STR(x) #x
#define STR(x) _STR(x)
void test(const char* a)
{
if (a == nullptr)
my_runtime_error("error at " __FILE__ ":" STR(__LINE__));
}
this way you avoid generating all that exception related code inside your function. Right away generated instructions become simpler and cleaner and reduce effect on the instructions that are generated by your actual code where you perform checks:
There is still room for improvement. Since you know that your my_runtime_error
won't return you should let the compiler know about it, so that it wouldn't need to preserve registers before calling my_runtime_error
:
#if defined(_MSC_VER)
#define NORETURN __declspec(noreturn)
#else
#define NORETURN __attribute__((__noreturn__))
#endif
void NORETURN my_runtime_error(const char* message);
...
When you use it multiple times in your code you can see that generated code is much smaller and reduces effect on instructions that are generated by your actual code:
As you can see, this way compiler doesn't need to preserve registers before calling your my_runtime_error
.
I would also suggest against concatenating error strings with __FILE__
and __LINE__
into monolithic error message strings. Pass them as standalone parameters and simply make a macro that passes them along!
void NORETURN my_runtime_error(const char* message, const char* file, int line);
#define MY_ERROR(msg) my_runtime_error(msg, __FILE__, __LINE__)
void test(const char* a)
{
if (a == nullptr)
MY_ERROR("error");
if (a[0] == 'a')
MY_ERROR("first letter is 'a'");
if (a[0] == 'b')
MY_ERROR("first letter is 'b'");
}
It may seem like there is more code generated per each my_runtime_error call (2 more instructions in case of x64 build), but the total size is actually smaller, as the saved size on constant strings is way larger than the extra code size.
Also, note that these code examples are good for showing benefit of making your "hard crashing" function an extern. Need for noreturn
becomes more obvious in real code, for example:
#include <math.h>
#if defined(_MSC_VER)
#define NORETURN __declspec(noreturn)
#else
#define NORETURN __attribute__((noreturn))
#endif
void NORETURN my_runtime_error(const char* message, const char* file, int line);
#define MY_ERROR(msg) my_runtime_error(msg, __FILE__, __LINE__)
double test(double x)
{
int i = floor(x);
if (i < 10)
MY_ERROR("error!");
return 1.0*sqrt(i);
}
Generated assembly:
Try to remove NORETURN
, or change __attribute__((noreturn))
to __attribute__((cold))
and you'll see completely different generated assembly!
As a last point (which is obvious IMO and was omitted). You need to define your
my_runtime_error
function in some cpp file. Since it's going to be one copy only, you can put whatever code you want in this function.
void NORETURN my_runtime_error(const char* message, const char* file, int line)
{
// you can log the message over network,
// save it to a file and finally you can throw it an error:
std::string msg = message;
msg += " at ";
msg += file;
msg += ":";
msg += std::to_string(line);
throw std::runtime_error(msg);
}
One more point: clang actually recognizes that this type of function would benefit from noreturn
and warns about it if -Wmissing-noreturn
warning was enabled:
warning: function 'my_runtime_error' could be declared with attribute 'noreturn' [-Wmissing-noreturn] { ^
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With