May 28, 2014
Last year Google announced support for C++ exceptions in the PNaCL tool chain of their Native Client technology. Since my experimental project Osoasso uses exceptions for some error handling, this was excellent news. I decided to start working to build my project using PNaCL. However, I was intrigued by this statement in the announcement:
As before C++ exception handling is off by default, to ensure better performance and smaller code size. It is possible to use set-jump long-jump exception handling on stable pexes with the –pnacl-exceptions=sjlj compiler flag. Full zero-cost exceptions are currently only supported for unstable pexes…
I was unfamiliar with the concept of set-jump long-jump exception handling, and I wondered why it had some cost associated with it. Here I’ll attempt to explain that cost.
What is set-jump long-jump exception handling?##
Set-jump long-jump exception handling is a simple method of exception handling implementation that uses the C methods
longjmp (or something like them). The
setjmp method is used to store all of the processor state at a given location, and the
longjmp method is used used to return to that processor state and location if necessary. In LLVM (which is used for the PNaCL implementation), the
llvm.eh.sjlj.longjmp intrinsics are used.
This type of exception handling is not considered zero-cost, since some additional code must be executed for each
try block or
throw statement, even if the no exception occurs. As the LLVM exception handling documentation states:
…SJLJ exception handling builds and removes the unwind frame context at runtime. This results in faster exception handling at the expense of slower execution when no exceptions are thrown. As exceptions are, by their nature, intended for uncommon code paths, DWARF exception handling is generally preferred to SJLJ.
How can exceptions be enabled with PNaCL?##
Exception handling can be enabled with PNaCL at compile and link time use the
--pnacl-exceptions=sjlj flag. This flag must be passed to both the compiler and the linker. So in my Makefile, I have the following lines:
I spent a good bit of time wondering why the code in the first two lines did not work initially. Then I later realized that the
LDFLAGS variable was not being passed to the
LINK_RULE by default. I had to manually add it!
What is the cost of SJLJ exceptions?##
First, I measured the cost of SJLJ exceptions in two ways:
- Compile time
- Executable size
The following table shows the data for both of these cases with and without exceptions.
|Without Exceptions||With SJLJ Exceptions|
|Compile time||2m 54 s||3m 1s|
|Executable size||394,000 bytes||583,748 bytes|
The most important cost of these exceptions is executable size. Osoasso is distributed via the web. Every extra byte matters, since it is an extra byte the user will have to download and extra time the user will wait to begin using the application.
What is the run-time cost of SJLJ exceptions?##
To measure the run-time cost of SJLJ exceptions, I wanted to execute some code that includes
throw statements but does not actually throw any exceptions. Also, the time to execute the code to perform the actual math should not be too significant, so it will not trump the exception handling code. I choose to use the
add command to add two 100x100 matrices. This code throws exceptions if the matrices are not of the correct size.
|Without Exceptions||With SJLJ Exceptions|
|Time to add matrices||0.01317 s||0.01551 s|
So although there is a run-time cost for SJLJ exception handling, it not too significant. For most operations in Osoasso, exception handling is not a part of the code which performs the math, so SJLJ exception handling does not cause run-time overhead that will have a significant impact.
How are SJLJ exceptions implemented?##
To better understand why this exception handling scheme has any run-time cost for the non-exceptional path, I decided to investigate the implementation details.
The normal build for PNaCL generates a LLVM bitcode file and a .pexe file. Following the instructions here I was able to generate a human-readable text file of the LLVM bitcode using this command:
As an example, I took a look at the generated code for the
jacobi_eigen_solver command, which requires that its input matrix be symmetric. The section of the code which makes this check looks like this:
Unfortunately, the generated LLVM bitcode for even this simple example is rather large, so I’ll attempt to summarize it.
Without exceptions enabled, these lines represent the
if check in C++:
icmp instruction is performing the comparison and the
br instruction actually branches to the exceptional path if the value in
%22 is false. The exceptional path is in the
if.then label, where eventually the code will call
abort because exception handling is disabled. Note however that the non-exceptional path requires no additional instructions. All of the exception handling code in this case is on the exceptional path.
If, instead, SJLJ exceptions are enabled, then the following code is generated for the
if statement in C++:
This code starts with a call to allocate the exception, then ends up calling the
setjmp_caller method, which actually makes the decision about whether or not the exception should be thrown. Here is the content of that method:
The only conditional here is used to determine the return value of the
setmp_caller method, so all of this code will be executed even when on the non-exceptional path. Note that
str414 is the actual exception string that will be the passed to the exception constructor.
So it is clear that SJLJ exception handling requires at least two additional function calls, plus some additional code on the non-exceptional path. Hence, it is not considered no-cost exception handling.
Why was SJLJ exception handling chosen?##
According to the design document for this feature, SJLJ exception handling was chosen as a stop-gap solution to implement exception handling for PNaCL since it required no changes to the ABI. Zero-cost exception handling for PNaCL is coming in a future release, but it is not ready yet.