David Jeske
2013-07-29 22:17:03 UTC
If we are to achieve a successful combination of type-safety and
run-time-binding modularity, I think we need to look very carefully at
error-condition-mechanisms, as they can be a significant cause of ABI
backward-compatibility challenges when components are updated. Performance
of error-condition mechanisms is also an important issue.
The simple analogy I choose for this problem is to consider how much
traction or trust the standard C library would have ever achieved if it
contained calls to exit(1) after runtime events, if error return codes did
not have a reliable backward-compatible success/fail test, such as the
zero-as-single-success-value pattern, or if certain error conditions
executed sleep(1) or spin-wait before they were returned. This would have
been untenable for programming in the large.
In my view, a successful systems programming environment must (a) rely on
an error-signaling mechanism with a backward-and-forward compatible
success/fail test, must (b) not change the error-generating profile of a
function in successive supposedly-compatible versions (for this is a break
of the ABI), and should (c) not mandate excessive performance overhead for
error-signaling. Further, if we wish to aid the programmer, we should
consider (zz) mechanisms which help assure exhaustive success/fail-handling
and/or exhaustive condition handling when desired.
IMO, most (all?) modular-compilation stack-unroll exception implementations
fail *both* (a) and (b). This includes not only the older C++, but also the
very modern CLR. While catch-all exception handlers might seem like a
backward compatible success/fail test, in the context-of stack-unroll and
non-local exception handling they practically are not. In order to meet
criteria (a), one would need to wrap *every* statement in a catchall try
block, and never handle exceptions non-locally.
Here is a look at some specific mechanisms..
C++: Fails both (a), and (b) and does not provide (zz). Shap recently
quoted 20% as an overhead for it's exception mechanism, which is slower
than return values but nowhere near as bad as some exception unroll costs.
CLR: Fails both (a) and (b) and does not provide (zz). Further, because the
CLR exception unroll mechanism is *very* expensive, library functions often
provide exception and return-value versions. However, if the library author
did not choose to create a return-value version, the library becomes
unusable for high-performance use.
Java: Fails (a) and (b) despite substantial support for (zz), through it's
checked-exception system. It fails (a) and (b) due to frequent user-defined
RuntimeExceptions, which are exacerbated by the cumbersome nature of it's
checked-exception system. -- IMO..there are less-cumbersome ways to
achieve the same level of checked-exception-safety through compiler
inference, requiring throws declaration only at module boundaries (see my
old brainstorm C# checked exception
proposal<http://dj1.willowmail.com/~jeske/unsolicitedDave/csharp_checked_exception_proposal.html>)
If this were fixed, IMO the use of RuntimeExceptions could be minimized or
eliminated. This solution however, is unviable for JVM, as it has no such
assembly/module boundary over which to perform inference.
Google-Go: has no stack-unroll exception mechanism, instead relying on
error-return-values and the backward-compatible zero-as-success-value test.
Google-Go meets both criteria (a) and (b), though it is unfortunate that
so-far it offers no compiler assistance to assure exhaustive
error-condition checking as in (zz). It also carries all the
Objective-C: Contains a C++ like exception mechanism, but standard
MacOS/iOS libraries do not make use of exceptions, instead returning
error-codes. These standard system libraries meet (a) and (b), though if
they used Objective-C exceptions they would not.
I think there are two attractive paths forward for error-condition
signaling:
(1) Structured error return handling. By which I mean a system based on
error-return codes with more try/catch/raise like language syntax, to help
reduce confusion and compiler-assisted exhaustive checking and
declarations, to increase safety. This has the advantage that return-values
are very fast, and the disadvantage of potentially more complex compilers
and module-internal sub-compilation unit invalidation (slower compile
times).
(2) Fully checked-exceptions, with performant setup and unroll, and
module-level inference. By which I mean a system substantially like Java's
checked exception system, but where throws declarations are only required
on module boundaries.
I think it's also worth noting that there was some ML research on
type-polymorphic exceptions implemented using regular-return-values, which
not only solved some interesting exception polymorphism cases but was also
faster than stack-unroll equivalents. It's going to take me some time to
dig up the research reference.
run-time-binding modularity, I think we need to look very carefully at
error-condition-mechanisms, as they can be a significant cause of ABI
backward-compatibility challenges when components are updated. Performance
of error-condition mechanisms is also an important issue.
The simple analogy I choose for this problem is to consider how much
traction or trust the standard C library would have ever achieved if it
contained calls to exit(1) after runtime events, if error return codes did
not have a reliable backward-compatible success/fail test, such as the
zero-as-single-success-value pattern, or if certain error conditions
executed sleep(1) or spin-wait before they were returned. This would have
been untenable for programming in the large.
In my view, a successful systems programming environment must (a) rely on
an error-signaling mechanism with a backward-and-forward compatible
success/fail test, must (b) not change the error-generating profile of a
function in successive supposedly-compatible versions (for this is a break
of the ABI), and should (c) not mandate excessive performance overhead for
error-signaling. Further, if we wish to aid the programmer, we should
consider (zz) mechanisms which help assure exhaustive success/fail-handling
and/or exhaustive condition handling when desired.
IMO, most (all?) modular-compilation stack-unroll exception implementations
fail *both* (a) and (b). This includes not only the older C++, but also the
very modern CLR. While catch-all exception handlers might seem like a
backward compatible success/fail test, in the context-of stack-unroll and
non-local exception handling they practically are not. In order to meet
criteria (a), one would need to wrap *every* statement in a catchall try
block, and never handle exceptions non-locally.
Here is a look at some specific mechanisms..
C++: Fails both (a), and (b) and does not provide (zz). Shap recently
quoted 20% as an overhead for it's exception mechanism, which is slower
than return values but nowhere near as bad as some exception unroll costs.
CLR: Fails both (a) and (b) and does not provide (zz). Further, because the
CLR exception unroll mechanism is *very* expensive, library functions often
provide exception and return-value versions. However, if the library author
did not choose to create a return-value version, the library becomes
unusable for high-performance use.
Java: Fails (a) and (b) despite substantial support for (zz), through it's
checked-exception system. It fails (a) and (b) due to frequent user-defined
RuntimeExceptions, which are exacerbated by the cumbersome nature of it's
checked-exception system. -- IMO..there are less-cumbersome ways to
achieve the same level of checked-exception-safety through compiler
inference, requiring throws declaration only at module boundaries (see my
old brainstorm C# checked exception
proposal<http://dj1.willowmail.com/~jeske/unsolicitedDave/csharp_checked_exception_proposal.html>)
If this were fixed, IMO the use of RuntimeExceptions could be minimized or
eliminated. This solution however, is unviable for JVM, as it has no such
assembly/module boundary over which to perform inference.
Google-Go: has no stack-unroll exception mechanism, instead relying on
error-return-values and the backward-compatible zero-as-success-value test.
Google-Go meets both criteria (a) and (b), though it is unfortunate that
so-far it offers no compiler assistance to assure exhaustive
error-condition checking as in (zz). It also carries all the
Objective-C: Contains a C++ like exception mechanism, but standard
MacOS/iOS libraries do not make use of exceptions, instead returning
error-codes. These standard system libraries meet (a) and (b), though if
they used Objective-C exceptions they would not.
I think there are two attractive paths forward for error-condition
signaling:
(1) Structured error return handling. By which I mean a system based on
error-return codes with more try/catch/raise like language syntax, to help
reduce confusion and compiler-assisted exhaustive checking and
declarations, to increase safety. This has the advantage that return-values
are very fast, and the disadvantage of potentially more complex compilers
and module-internal sub-compilation unit invalidation (slower compile
times).
(2) Fully checked-exceptions, with performant setup and unroll, and
module-level inference. By which I mean a system substantially like Java's
checked exception system, but where throws declarations are only required
on module boundaries.
I think it's also worth noting that there was some ML research on
type-polymorphic exceptions implemented using regular-return-values, which
not only solved some interesting exception polymorphism cases but was also
faster than stack-unroll equivalents. It's going to take me some time to
dig up the research reference.