Exceptions proposal

@everythingfunctional The code you mentioned is very good and it indeed highlights an issue with Fortran, but I don’t think exceptions are the answer in this particular case:

pure function mean(vals)
  real, intent(in) :: vals(:)
  real :: mean

  mean = sum(vals) / size(vals)
end function

Here what I personally would like is something like:

pure function mean(vals)
  real, intent(in) :: vals(:)
  require :: size(vals) > 0
  real :: mean

  mean = sum(vals) / size(vals)
end function

Then in Debug mode I get a beautiful runtime stacktrace if this condition is not satisfied. In Release mode this will not be checked and the result is undefined, but performance is high (no exceptions or other overhead). One can also consider a ReleaseSafe mode, where this check remains, for safety reasons, if that is what you need.

My workflow is that if I get an error in Debug mode here, I have to adjust the caller to ensure it never passes an empty array into this function.

In existing Fortran, you can define an ASSERT macro to do something similar.

The feature that is missing are these pre conditions (contracts) for functions, automatically enforced by the compiler. See these two issues for further information and a proposal:

Regarding:

While some overhead may be incurred by such a feature, it is not as much as would be incurred by any of the other mechanisms of error handling currently available to us.

I think the approach I showed above indeed has zero overhead in Release mode.

3 Likes

I do think assertions (both static and run-time), are a useful feature and would be a good idea, but wouldn’t be sufficient.

And now you’re incurring more run-time overhead, potentially in more places, than would have been the case with exceptions.

Not if you had to put the run-time checks in elsewhere.

I think it boils down to the same thing. The cost of exceptions is not only in the handling but in detecting also. If it’s enabled, every executable statement must run in a try/except-like mechanism (which is really an if/else) under the hood. The difference between @certik’s pre-condition approach vs. exception handling inside the function seems to me more a matter of style–is the exception handled in the caller or in the function.

My understanding is there are exception handling technologies with better performance than wrapping every sub-expression evaluation in an if/else block under the hood. And if anything, C++ has demonstrated you can turn exception handling off too. I’ll admit that runs the risk of bifurcating the ecosystem like happened there, but I’m hopeful we can design Fortran’s exception handling to have better performance than C++ and not have that problem. We should at least explore the possibility.

You move the check out of the inner most function into the outer functions. And also in the outer function you often know this cannot happen, for example:

a = [1, 2, 3]
print *, mean(a)

So for performance reasons, that is a win. In Fortran, I think it’s essential that we preserve the current advantage that you can build your application out of smaller building blocks (such as the function you posted) that do not have any overhead.

I agree that ultimately the check / guarantee that the array is not zero length has to be somewhere, but it should not be in the inner most function for performance reasons.

It is just like array bounds checking. You ultimately have to ensure that array access does not go out of bounds, but you do not want to be doing these checks in the inner most functions in loops. You want to do these checks in higher level; and often the algorithm itself that you use ensures the array access cannot go out of bounds, so you don’t even have to do any checks at all. But you don’t know that in the inner most function.

I don’t think many users care about IEEE exceptions. To be honest, disabling them (for example, using -funsafe-math-optimizations or -ffast-math in gfortran) is the first thing you do to get your code to vectorize nicely and run at competitive speed. We need real exceptions, now, not in 15 years.

This exactly the problem with this language: focusing on things almost nobody cares about (cosd? really? lol), while still staying irrelevant by not having the most basic constructs (templates, exceptions, interfaces/traits). Because “it’s too hard to implement for compiler vendors”. In 2023. Only so that people like @certik can show to these “experts” at Intel, Cray or whoever impacts the decision of the regulating body to keep Fortran 20 years behind that it is possible to write an almost complete Fortran compiler in nearly one-person effort in a couple years. Almost as if these big “compiler vendors” who “want the best for the language” make more money on customers that still want their old F77 and F90 codes to run rather than invest in developers to implement new language features that nobody of their payers want anyway, because most projects rewrite their codes in C++ rather than Modern Fortran. I wonder if this is why there is such a resistance from the top to fix anything in Fortran, hmmm…

Amen.

4 Likes

I think that’s exactly what’s happening.

2 Likes

Current Intel Fortran IFORT and IFX compiler drivers support all of Fortran 2018 standard and ostensibly Intel appears committed to implementing all of Fortran 2023 ASAP, per Intel’s presentation. So there is one implementation that disproves @gronki’s above claim and per certain dominant “school of thought” one exception is all it takes to invalidate the whole of @gronki’s rather broad brush of a claim.

However it is to be noted the problem is more nuanced, there are clearly some compiler implementations who require much, much longer time to “catch up” to the current standard and even then they may not bother with reasonably workable implementations of certain features such as parameterized derived types and coarrays.

So then there is tendency, very, very strong tendency to “go slow”, really, really slow with feature additions to Fortran standard revisions to be nice and kind to these other implementations play “catch up”, damn the rest of the user community if the missing facilities lead to code base after code base migrate away from Fortran. Cue “minor” Fortran 2018 after nearly 10 years following 2008, and another very, very minor revision in 2023 version where even a minor feature such as ENUMs is half-baked and barely usable in modern code and this happened also because one nonmember of a compiler vendor with a highly influential attendee wanted a minimalist feature that will be easier to implement in said compiler.

Such is the sad reality. And this will persist because a near 100% of the users will not be able to afford to become voting members of INCITS org. This is why I keep asking, “for whom Fortran, for what?”

However this is not relevant to the exceptions feature. With exceptions facility, the situation is indeed an exception (bad pun intended!).

It is indeed because the last 5 to 10 years have been revealing of deeply problematic issues with block-structured exception handling and especially of late with code designs avoiding such handling that it makes total sense for Fortran to go slow and figure out one major aspect first, the one with ERROR STOP and error termination semantics.

Solve the ERROR STOP issue first, that will akin to learning to walk, Fortran can then learn to run exceptionally!

Additionally note an overwhelming fraction of the exception handling scenarios involve floating-point exceptions, even the contrived scenario presented upthread here by @everythingfunctional with mean is one such. And here, unlike most other languages, Fortran has already made the investment with IEEE arithmetic of which most people are either inadequately aware (@gronki’s comments imply as such) or they have barely made enough attempts to employ them profitably in codes. Again, this calls for those advocating exception handling immediately to make use of IEEE floating point exception handling features in the Fortran standard for several years first and to enumerate any gaps and deficiencies in them first before coming back to this proposal. Now, try that and it will again come back to ERROR STOP because eventually someone upstack will need to invoke it and at that point, current Fortran processor based error termination ain’t good enough.

1 Like

Opinions on this differ, but just like @gronki, I personally also recommend for application developers to turn off floating point exceptions and enable “fast-math” for performance and write your code in such a way that it works with it.

@FortranFan, if Intel can already compile all of modern Fortran, then it should be easier for them to prototype some of these new features such as generics or exceptions and drive the process forward, than it is for us in LFortran, as our main focus is compiling all Fortran codes first. @gronki thanks for your support, I appreciate it. We are making excellent progress (legacy Minpack now compiles and runs correctly, new Minpack almost works, all of fpm can compile to our intermediate representation, etc.), and we are on the way to deliver a working compiler.

3 Likes

TL;DR: The standards committee members are mostly trying to do the right thing.

INCITS Fortran’s standards meeting just ended yesterday. The main focus was wrapping up the issues for the Fortran 2023 (202x) standard first, and then examining the US organization’s request for additional features to be added to Fortran 202y.

The next meeting, the ISO WG5 meeting in Manchester UK, will be when we review each of the country lists. We hope to create the consolidated 202y work items list there for the version after 2023.

With respect to what we choose to work on, things generally fall into two buckets:

  1. Extensions that most compilers already support. These are often relatively easy to add and improve portability between the implementations.

  2. New features to enhance the power and reliability of Fortran. These (generics, error handling, rank-generic programming) are harder, as they (a) have to be defined without the benefit of existing Fortran practice; (b) have to incorporate best practices from other languages in a way that doesn’t break existing semantics; and (c) have a programmer benefit that is worth the implementation cost for implementations that intend to keep up with the standard.

With respect to catering to old codes over modernizing codes, I don’t have more than a smidgen of data yet. But a repo of 200+ projects and 45 million lines of public Fortran codes show about 2/3rds of the lines of code are still in fixed form. I don’t have an analysis yet of what percentage conforms to which version of the standard, but that would be really interesting to see. (As would adding more real Fortran projects.)

1 Like

With respect to getting involved in the process for improving the standard, I want to mention a couple things.

It’s easier to participate in the committee than it might seem.

  • Voting membership (one vote per organization): Organizations with more than $3m in revenue can join INCITS as voting members for $2275 per year. Bring in less than $3M? Voting rights can be secured for $1340/year. Educational organization? $495/year. These memberships apply to INCITS as a whole, so you can participate in Fortran, C, C++, Ada, etc., all with that one membership.

  • Alternate membership: Each organization can have an unlimited number of alternates. Many decisions are decided based on straw votes, where alternates can vote. Much work is done in subgroups, and alternates are very welcome there. Your ability to influence is largely based on your ability to contribute to the work that needs to get done. (For example, @milancurcic and @certik are alternates for my mythical organization Dancing Bear Software.)

3 Likes

Error handling and exception handling is good example. The Journal of Record subgroup put it on it’s “not for Fortran 202y” list because it

  • could be big, having potential implications in the dynamic semantics of lots of things (such as coarrays, teams, DO CONCURRENT);
  • should align with Fortran’s reliance on static semantics for performance (think PURE and SIMPLE);
  • should work well in hybrid system environments (CPU + GPU);
  • should align with modern best practices (we have lots of old proposals);
  • needs a dedicated group of dedicated people willing to drive consensus. (Generics Subgroup has been really good at this, I think, if you’re looking for an example found in nature.)

Even just getting consensus on a list like the above is real work… (I’m sure I left off a half-dozen things already.)

2 Likes

@gak , it will be cool if JoR and the committee overall can consider work on enhanced error termination semantics toward Fortran 202Y. Error termination is already part of the standard, but it’s practically from “ancient” times, something that hasn’t considered the aspects you list above and as such it needs rehauling for modern apps. Enhanced error termination will be a nice baby step forward.

Don’t worry, there’s plenty of time to get it right. Fortran is still on top, and none of the other programming languages people are likely to use for technical programming have a good solution for error or exception handling.

Oh wait, I’m being told that it’s 2023 and not 1983…

Yikes, we are in trouble. :man_facepalming:

4 Likes

Times are already changing for error and exception handling, which must go through a fault tolerant execution, because of upcoming extreme levels of parallelism. Error termination must be avoided as well.

I am describing this initially for Fortran as such:

8.2 Fault Tolerant Execution with Pairwise Independent Forward Progress

The execution sequence of the kernels in a coroutine are sequentially ordered through the channel’s i_chstat argument, even if the ordering may change during execution. A crucial property is a constant swap of channel send and receive for successive execution of kernels on the coarray images. This is to ensure pairwise (independent) forward progress for parallel kernel execution.

The execution control of the coroutines uses the atomic operation behind this send and receive. And, if something goes wrong within kernel execution, execution control uses a missing atomic operation as a signal for a potential failure. (Coarray images can then remain in a short ‘wait mode’ to see if the ‘failed’ coarray image was only delayed and not really a failure).

Strategies for handling such failures could be to recover by leaving and reentering a coarray team, or to simply continue execution with other channel instances (and other asynchronous coroutines) if this is still possible. (We may check for that possibility in the encompassing parallel loop).

I was thinking about this more, and there are situations where this cannot be done. Let’s look at something like a Newton-Rhapson method.

pure real function find_root(intial_guess, f, df) result(root)
  real, intent(in) :: initial_guess
  interface
    pure real function f(x) ! the function
      real, intent(in) :: x
    end function
    pure real function df(x) ! the derivative
      real, intent(in) :: x
    end function
  end interface

  real, parameter :: tolerance = epsilon(initial_guess)
  integer, parameter :: max_iterations = 1000
  real :: guess
  integer :: i

  guess = initial_guess
  do i = 1, max_iterations
    root = guess - f(guess)/df(guess)
    if (abs(root - guess)/root < tolerance) return
    guess = root
  end do
  error stop "Failed to converge"
end function

I can not think of any (simple) test that could be performed ahead of time to know for sure whether this will or won’t converge. So exceptions really do seem like the most appropriate thing to do here.

I’m more than a little tired of seeing these lazy digs at the standards committee. In the case of exceptions or “enhanced error handling”, as I have written before here, there is broad disagreement on what it should look like even at a stratospheric level. Most of the committee are users, not vendors - it is not vendors slowing things down.

If you have a specific proposal, please post it here and let’s see what people think.

There was an idea I heard recently that involved passing a special derived type from ISO_FORTRAN_ENV (we have several of these already) as an argument that would tell the called procedure to provide some sort of error handling that was then returned through the argument to the caller. There’s no propogation and code that doesn’t use the feature doesn’t pay any penalty. Obviously this would need to be fleshed out, but it is more in line with Fortran than some other ideas I have seen.

8 Likes

Preface: it’s Raphson.

Why does find_root here have to be a Fortran function at all? Given a subroutine with an error code argument (say nonzero implies nonconvergence, something which has been in use since the early days of FORTRAN) is a reasonably good KISS approach and which seems more appropriate than a function here.

Secondly, at some stage up the stack (or down, depending on how one looks at it), error termination semantics (shown here with error stop) will need to come into play. Increasingly Fortran subprograms are invoked by means other than a Fortran processor and the rudimentary error termination semantics currently in the language which are around a Fortran processor only is a big drawback.

Can some better control around the error termination aspect be addressed first, say Fortran 202Y, before jumping into exceptions?

How is the person calling the Newton method meant to respond to such an exception? Call the bisection method instead (maybe we can’t determine a good bracket either)? Rerun the routine with a larger iteration count? Gracefully terminate the program?

I was curious if other libraries launch an exception for the Newton-Raphson method:

  • Boost newton_raphson_iterate - seems to only propagate exceptions stemming from the user-function F, or related to operations with a custom user type T. In case of floating point numbers, the function is noexcept. To determine if the iterations converged they leave it to the user to check if the number of iterations reached the max_iter value as a post-hoc test. It’s up to you to decide if the root approximation was good enough or not.
  • SciPy newton - only raises warnings if a disp flag is set to true, otherwise it will only print a run-time warning.
  • GSL gsl_root_fdfsolver_iterate - returns an error flag in case the evaluated function is NaN, Inf, or division with zero occured. The stopping conditions are completely up to the user.
  • NAG c05awf - sets the ifail flag to 4, with the following recommendation “Increase nfmax, check the coding of f or increase eps. Check the coding of f is something I probably can’t do at runtime; how am I supposed to know if I should increase nfmax or eps at runtime? Should I just try both? What if still doesn’t converge?

Even in languages which already have exception handling (C++ and Python), the library authors did not use it in this specific scenario. They expect the caller will perform his own investigation.

I recently bought the book “A Philosophy of Software Design” by John Ousterhout (creator of Tcl and professor of CS at Stanford). It also includes a chapter on error handling. Here are some excerpts which stuck with me:

Exception handling is one of the worst sources of complexity in software systems. Code that deals with special conditions is inherently harder to write than code that deals with normal cases, and developers often define exceptions without considering how they will be handled.

It’s tempting to use exceptions to avoid dealing with difficult situations: rather than figuring out a clean way to handle it, just throw an exception and punt the problem to the caller. Some might argue that this approach empowers the callers, since it allow each caller to handle the exception in a different way. However if you are having trouble figuring out what to do for the particular situation, there’s a good chance that the caller won’t know what to do either. Generating an exception in a situation like this just passes the problem to someone else and adds to the system’s complexity.

Throwing exceptions is easy; handling them is hard.

1 Like

One use of newton’s method is a nonlinear solve within an implicit ODE solver. If your newton’s method hits an exception you want to try with a smaller timestep.