This is an archive of the discontinued LLVM Phabricator instance.

[FPEnv] Apply dynamic rounding to function body only
AbandonedPublic

Authored by sepavloff on Sep 29 2020, 9:02 AM.

Details

Summary

Support of rounding mode in constant evaluation resulted in some
number of compile errors for constructs that previously were compiled
successfully. For example, the following code starts failing at
compilation if FP exception behavior is set to strict:

struct S { float f; };
static struct S x = {0.63};

This happenes because setting strict behavior sets rounding mode to
dymanic. In this case the value of initializer depends on the current
rounding mode and cannot be evaluated in compile time.

Using dynamic as rounding mode make no sense outside function bodies.
For example, even if the initializer is evaluated dynamically, this happens
before the execution of main, so there is no possibility to set dynamic
rounding mode for it. C requires that initializers are evaluated using
constant rounding mode. It makes sense to apply this rule to C++ as well.

With this change dynamic rounding mode is applied to function bodies. In
other cases, like evaluation of initializers the constant rounding mode
is applied, which is 'tonearest' unless it was changed by '#pragma STDC
FENV_ROUND'.

Diff Detail

Event Timeline

sepavloff created this revision.Sep 29 2020, 9:02 AM
Herald added a project: Restricted Project. · View Herald TranscriptSep 29 2020, 9:02 AM
sepavloff requested review of this revision.Sep 29 2020, 9:02 AM
mibintc accepted this revision.Sep 29 2020, 9:44 AM

thanks for this!

This revision is now accepted and ready to land.Sep 29 2020, 9:44 AM
rsmith requested changes to this revision.Sep 29 2020, 10:15 AM
rsmith added inline comments.
clang/lib/Parse/ParseDecl.cpp
2290 ↗(On Diff #295011)

It's far from clear to me that this is correct in C++. In principle, for a dynamic initializer, the rounding mode could have been set by an earlier initializer.

Perhaps we can make an argument that, due to the permission to interleave initializers from different TUs, every dynamic initializer must leave the program in the default rounding mode, but I don't think even that makes this approach correct, because an initializer could do this:

double d;
double e = (fesetround(...), d = some calculation, fesetround(...default...), d);

I think we can only do this in C and will need something different for C++.

(I think this also has problems in C++ due to constexpr functions: it's not the case that all floating point operations that will be evaluated as part of the initializer lexically appear within it.)

This revision now requires changes to proceed.Sep 29 2020, 10:15 AM
mibintc added inline comments.Sep 29 2020, 1:15 PM
clang/test/AST/const-fpfeatures-strict.c
40

I think the initializer for var_04 is evaluated at translation time, therefore the initialization expression would look the same as var_01 above, using default rounding mode. Same for var_05 and var_06. They have "static duration"

Updated patch

sepavloff added inline comments.Sep 30 2020, 12:57 AM
clang/lib/Parse/ParseDecl.cpp
2290 ↗(On Diff #295011)

I changed the code to confine it with C and constexpr only. Hopefully this would be enough to enable build of SPECS attempted by @mibintc (https://reviews.llvm.org/D87528#2295015). However in long-term perspective we should return to this code.

The intent was to align behavior of C and C++. If an initializer is valid in C, then it should produce the same code in C++. If the source code like

float appx_coeffs_fp32[3 * 256] = {
    SEGMENT_BIAS + 1.4426950216,
…

produces compact table in C mode and huge initialization code in C++, it would be strange from user viewpoint and would not give any advantage.

C in C2x presents pretty consistent model, provided that #pragma STDC FENV_ROUND FE_DYNAMIC does not set constant rounding mode. Initializers for variables with static allocation are always evaluated in constant rounding mode and user can chose the mode using pragma FENV_ROUND.

When extending this model to C++ we must solve the problem of dynamic initialization. It obviously occurs in runtime rounding mode, so changing between static and dynamic initialization may change semantics. If however dynamic initialization of global variable occurs in constant rounding mode (changing FP control modes in initializers without restoring them is UB), static and dynamic initialization would be semantically equivalent.

We cannot apply the same rule to local static variables, as they are treated differently in C and C++. So the code:

float func_01(float x) {
  #pragma STDC FENV_ACCESS ON
  static float val = 1.0/3.0;
  return val;
}

Would be compiled differently in C and C++.

sepavloff added inline comments.Sep 30 2020, 1:01 AM
clang/test/AST/const-fpfeatures-strict.c
40

The statement #pragma STDC FENV_ROUND FE_UPWARD changes constant rounding mode, so evaluation of the initializer for var_04 produces different result than in the case of var_01, which indeed is evaluated using default rounding mode.

sepavloff added inline comments.Sep 30 2020, 10:54 PM
clang/lib/Parse/ParseDecl.cpp
2290 ↗(On Diff #295011)

Additional note:

If initialization is dynamic and constant rounding mode is not default, the body of initializer is executed with dynamic rounding mode set to the constant mode. That is, the code:

#pragma STDC FENV_ROUND FE_UPWARD
float var = some_init_expr;

generates code similar to:

float var = []()->float {
  #pragma STDC FENV_ROUND FE_UPWARD
  return some_init_expr;
}

So initializers of global variable must conform to:

  1. If the initialization is dynamic and dynamic rounding mode is changed in the initializer, it must be restored upon finishing the initializer.
  2. The initializer is evaluated in constant rounding mode. If the initialization is dynamic, the initializing code is executed in dynamic rounding mode set to the constant rounding mode.

These seems enough to provide compatibility with C and the same semantics for static and dynamic initialization.

ping!

clang/lib/Parse/ParseDecl.cpp
2290 ↗(On Diff #295011)

@rsmith Serge changed the patch to confine it with C and constexpr only, is that adequate to move forward with this patch, and we can return to C++ at some point down the road?

rsmith added inline comments.Oct 7 2020, 1:57 PM
clang/lib/Parse/ParseDecl.cpp
2290 ↗(On Diff #295011)

This still seems inappropriate for the constexpr case -- we shouldn't have different behavior based on whether an expression appears directly in the initializer or in a constexpr function invoked by that initializer. (It also violates the C++ standard's recommendation that floating-point evaluation during translation and at runtime produce the same value.) Please see the discussion in D87528. Let's continue the conversation there; we shouldn't be splitting this discussion across two separate review threads.

sepavloff updated this revision to Diff 298119.Oct 14 2020, 4:53 AM

Updated patch

  • Reverted check to the previous version, in which it applied to C++ file level variables also.
  • Added workaround for constexpr functions. Now they are parsed with constant rounding mode, which allows to use them with option -frounding-math.

Setting call-site rounding mode is not implemented yet.

rsmith requested changes to this revision.Oct 14 2020, 5:08 PM
  • Reverted check to the previous version, in which it applied to C++ file level variables also.

This presumably reintroduces the misbehavior for

double d;
double e = (fesetround(...), d = some calculation, fesetround(...default...), d);

in which some calculation will be treated as being in the default rounding mode, right?

  • Added workaround for constexpr functions. Now they are parsed with constant rounding mode, which allows to use them with option -frounding-math.

This is inappropriate. When a constexpr function is invoked at runtime, it should behave exactly like any other function. Marking a function as constexpr should not cause it to round differently when used outside of constant expressions.

This revision now requires changes to proceed.Oct 14 2020, 5:08 PM
sepavloff updated this revision to Diff 298375.Oct 15 2020, 7:21 AM

Remade the patch

sepavloff retitled this revision from [FPEnv] Evaluate initializers in constant rounding mode to [FPEnv] Apply dynamic rounding to function body only.Oct 15 2020, 7:23 AM
sepavloff edited the summary of this revision. (Show Details)

Actually this solution also behaves wrong in some cases.

The tests in this patch exhibit the same behavior with and without the patch applied; I think almost all the functionality changes from here are superseded by the change to consider whether we're in a manifestly constant evaluated context. As far as I can tell, it only affects the behavior of C++ dynamic initializers in FENV_ACCESS ON regions by making calls to feset* be undefined behavior. I'm unconvinced that's the right way to extend the behavior of the FENV_* functionality to C++. Consider this example:

#include <fenv.h>
#pragma STDC FENV_ACCESS ON
struct InRoundingMode {
  int mode;
  int oldmode = fegetround();
  int ok1 = fesetround(mode);
  double value;
  int ok2 = fesetround(oldmode);
};
double d1 = InRoundingMode{.mode = FE_UPWARD, .value = 1.0 / 3.0}.value;
double d2 = InRoundingMode{.mode = FE_DOWNWARD, .value = 1.0 / 3.0}.value;

I don't think this is unreasonable: this code changes the rounding mode, performs a calculation, and then changes it back, all within a dynamic initializer, and all in an FENV_ACCESS ON region. I think it would be unreasonable to say that the FENV_ACCESS doesn't apply to the initializer, and the initializer therefore has undefined behavior.

So my inclination is to say that the status quo (prior to this patch) is preferable behavior. The new tests look valuable.

clang/test/CodeGenCXX/rounding-math.cpp
11

As discussed in other review threads, this should be a constant initializer with value 1.0; indeed, that's what we get both with and without the code change from this patch (this test fails when the patch is applied to Clang trunk for this reason).

clang/test/SemaCXX/rounding-math-diag.cpp
7

This is not the diagnostic we give with this patch applied (applying this patch results in this test failing for me): we say "read of non-constexpr variable is not allowed in a constant expression". (We don't consider the initializer of the variable.)

clang/test/SemaCXX/rounding-math.cpp
1

This patch needs to be rebased; a test file with this name already exists.

sepavloff abandoned this revision.Oct 20 2020, 4:49 AM

The tests in this patch exhibit the same behavior with and without the patch applied; I think almost all the functionality changes from here are superseded by the change to consider whether we're in a manifestly constant evaluated context.

Thank you for review and explanations!
So I am abandoning the patch.

So my inclination is to say that the status quo (prior to this patch) is preferable behavior. The new tests look valuable.

I'll try to extract the tests to a new review item.