This is an archive of the discontinued LLVM Phabricator instance.

[Clang] Support constexpr builtin ilogb
Needs RevisionPublic

Authored by Izaron on Oct 23 2022, 3:52 PM.

Details

Summary

Support constexpr version of __builtin_ilogb and its variations.

Diff Detail

Event Timeline

Izaron created this revision.Oct 23 2022, 3:52 PM
Herald added a project: Restricted Project. · View Herald TranscriptOct 23 2022, 3:52 PM
Herald added a subscriber: hiraditya. · View Herald Transcript
Izaron requested review of this revision.Oct 23 2022, 3:52 PM
Herald added projects: Restricted Project, Restricted Project. · View Herald TranscriptOct 23 2022, 3:52 PM

This patch is similar to __bultin_fmax: https://reviews.llvm.org/D134369

The constexpr version of ilogb matches the libc realization, this is verified with the same tests:
https://github.com/llvm/llvm-project/blob/main/libc/test/src/math/ILogbTest.h

test_special_numbers -> ILOGB_TEST_SPECIAL_NUMBERS
test_powers_of_two -> ILOGB_TEST_POWERS_OF_TWO
test_some_integers -> ILOGB_TEST_SOME_INTEGERS

https://eel.is/c++draft/library.c#3 says that a floating-point exception other than FE_INEXACT causes it to not be a constant expression.
I check it with small ilog function refactoring and the new function isConstantOpStatus.

The online documentation (https://en.cppreference.com/w/cpp/numeric/math/ilogb) says:

1. If the correct result is greater than INT_MAX or smaller than INT_MIN, FE_INVALID is raised.
2. If arg is ±0, ±∞, or NaN, FE_INVALID is raised.
3. In all other cases, the result is exact (FE_INEXACT is never raised) and the current rounding mode is ignored

The first point seemingly never occur, because llvm's ilogb return type is int.
The second point is handled as expected (APFloatTest.cpp checks it)

The online documentation (https://en.cppreference.com/w/cpp/numeric/math/ilogb) says:

1. If the correct result is greater than INT_MAX or smaller than INT_MIN, FE_INVALID is raised.
2. If arg is ±0, ±∞, or NaN, FE_INVALID is raised.
3. In all other cases, the result is exact (FE_INEXACT is never raised) and the current rounding mode is ignored

The first point seemingly never occur, because llvm's ilogb return type is int.
The second point is handled as expected (APFloatTest.cpp checks it)

FWIW, I would be slightly wary of relying on cppreference as definitive for niche semantic issues like this, although it is correct in this case. C2x is actually pretty well-organized in Annex F, and enumerates most of the corner cases specifically in every function.

clang/lib/AST/ExprConstant.cpp
12452

long double is ppc_fp128 on at least some PPC targets, and while I'm not entirely certain of what ilogb properly returns in the corner cases of the ppc_fp128, I'm not entirely confident that the implementation of APFloat is correct in those cases. I'd like someone with PPC background to comment in here.

clang/test/Sema/constant-builtins-ilogb.cpp
14

Test of smallest subnormal and largest finite number would also be useful.

FWIW, I would be slightly wary of relying on cppreference as definitive for niche semantic issues like this, although it is correct in this case. C2x is actually pretty well-organized in Annex F, and enumerates most of the corner cases specifically in every function.

+1 to this point; the edge cases are pretty tricky and the standard is the source of truth for how we should be implementing this stuff.

clang/lib/AST/ExprConstant.cpp
12452

Paging @hubert.reinterpretcast for help finding a good person to comment on the PPC questions.

llvm/include/llvm/ADT/APFloat.h
450–454

Is this part of the comment still accurate?

Izaron added inline comments.Oct 26 2022, 6:53 AM
clang/lib/AST/ExprConstant.cpp
12452

@jcranmer-intel constexpr evaluation is quite machine-/target-independent. Clang evaluates it based on its internal representation of float variables.

int ilogb uses Arg.getIEEE(), that returns Clang's internal float representation.

Whichever float semantics is being used, minExponent and maxExponent are representable as APFloatBase::ExponentType, which is int32_t:

/// A signed type to represent a floating point numbers unbiased exponent.
typedef int32_t ExponentType;

We already use int ilogb in some constexpr evaluation code: link, it is working correct because it is working on Clang's float representations.

jcranmer-intel added inline comments.Oct 26 2022, 7:40 AM
clang/lib/AST/ExprConstant.cpp
12452

We already use int ilogb in some constexpr evaluation code: link, it is working correct because it is working on Clang's float representations.

APFloat::getIEEE(), if I'm following it correctly, only returns the details of the high double in ppc_fp128 floats, and I'm not sufficiently well-versed in the ppc_fp128 format to establish whether or not the low double comes into play here. glibc seems to think that the low double comes into play in at least one case: https://github.com/bminor/glibc/blob/30891f35fa7da832b66d80d0807610df361851f3/sysdeps/ieee754/ldbl-128ibm/e_ilogbl.c

Izaron updated this revision to Diff 470962.Oct 26 2022, 3:51 PM

Add test for min/max value. Fix comment for ilog.

Izaron marked 2 inline comments as done.Oct 26 2022, 3:52 PM
Izaron added inline comments.
clang/lib/AST/ExprConstant.cpp
12452

Thanks for the link to the glibc code! It helped me to understand the IEEE754 standard better.

I did some research and it seems like AST supports a fixed set of float types, each working good with ilogb:

half (__fp16, only for OpenCL), float16, float, double, long double, float128

link to SemaExpr.cpp

It means that the constant evaluator doesn't deal with other float types including ppc_fp128.
If ppc_fp128 was supported on the AST level, it would anyway come through type casting, and __builtin_ilog<SUFFIX> would deal with a value of a known type.

I checked the list of builtins - each builtin argument of float type also supports only common float types:
link to Builtins.def 1
link to Builtins.def 2

Izaron updated this revision to Diff 471101.Oct 27 2022, 3:07 AM

Deal with MSVC where sizeof(long double) == sizeof(double)

majnemer added inline comments.
clang/lib/AST/ExprConstant.cpp
12452

Won't long double map to ppc_fp128 for some targets?

Izaron added inline comments.Oct 27 2022, 9:32 AM
clang/lib/AST/ExprConstant.cpp
12452

Hi! It will map, but only after all the constant (constexpr) calculations are done (that is, after the AST parsing stage) - in the Codegen stage.

The Clang's constant evaluator itself never deals with ppc_fp128 and doesn't care about the target.
While parsing the AST, the constant evaluator works on the same level with it, providing calculated values to the AST being built on-the-fly. At the moment AST is built, constant evaluation is over.
The evaluator is target-independent and uses the internal representation for long double, in the form of emulated 80-bit (x86) format.

The Codegen can map the AST's long double to ppc_fp128 on some targets.
It doesn't cause problems because x87 80-bit float is convertible to ppc_fp128 without precision loss.
But the constexpr long double values itself were calculated using the Clang's 80-bit format emulation, before the Codegen stage.

I'm sorry if I'm not describing it clearly. It's important to me that everyone understands what the trick is =)
So, the constant evaluator does everything with 80-bit floats and at the end they can be mapped on ppc_fp128 floats if the target requires it.

clang/lib/AST/ExprConstant.cpp
12452

That's kind-of terrible, but at least that means that what ilogb can do within the scope of this patch is very clear.
I am surprised to find that ilogb({2, -0x1p-64}) seems to return 1 on ppc64le Linux.

clang/lib/AST/ExprConstant.cpp
12452

Of course I should have used ilogbl...
So, yes, ilogbl({2, -0x1p-64}) returns 0 on ppc64le Linux.

clang/lib/AST/ExprConstant.cpp
12452

@Izaron,

The evaluator is target-independent and uses the internal representation for long double, in the form of emulated 80-bit (x86) format.

This the internal representation still 80-bit for platforms where long double uses the IEEE binary 128-bit format?

Izaron added inline comments.Oct 27 2022, 12:56 PM
clang/lib/AST/ExprConstant.cpp
12452

ilogbl({2, -0x1p-64}) returns 0 on ppc64le Linux.

How did you use ilogbl? I built local Clang with PowerPC support, this worked fine to me:

static_assert(__builtin_ilogbl(-0x1p-64) == -64);
static_assert(__builtin_ilogbl(-0x1p-1074L) == -1074);
// smaller exponents (< -1074) don't work, too small number

I would be glad for any example where constant ilogb would work incorrectly but runtime ilogb correctly. Unfortunately I can't make up such number because I navigate not so good enough in IEEE754 corner cases =(

This the internal representation still 80-bit for platforms where long double uses the IEEE binary 128-bit format?

I just checked with the debugger, the -target ppc64le parameter actually makes the constant evaluator use internal 128-bit format for long double. Yeah, this contradicts to some points what I wrote earlier... =(

jcranmer-intel added inline comments.Oct 27 2022, 1:13 PM
clang/test/Sema/constant-builtins-ilogb.cpp
53–63

You can simplify at least the float, double, and especially long double tests using something like this:

static_assert(__builtin_ilogbf(__FLT_MIN__) == __FLT_MIN_EXP__ + 1);
static_assert(__builtin_ilogbf(__FLT_MAX__) == __FLT_MAX_EXP__ - 1);
hubert.reinterpretcast requested changes to this revision.Oct 30 2022, 5:35 PM
hubert.reinterpretcast added inline comments.
clang/lib/AST/ExprConstant.cpp
12452

I would be glad for any example where constant ilogb would work incorrectly but runtime ilogb correctly. Unfortunately I can't make up such number because I navigate not so good enough in IEEE754 corner cases =(

By ilogbl({2, -0x1p-64}), I meant ilogbl(2.L + -0x1p-64). The answer I got at runtime is 0 (which is what I expected). It seems the ilogbl implementation at compile time does not return 0 though.

Here, I try the largest number in the format that is smaller than 2:

$ cat ilogb.cc
#include <math.h>
#include <stdio.h>

constexpr int comptime = __builtin_ilogbl(2.L - __DBL_DENORM_MIN__);
constinit volatile long double rtinput = 2.L - __DBL_DENORM_MIN__;
int runtime;
int main(void) {
  runtime = ilogbl(rtinput);
  printf("comptime(%d)\nruntime(%d)\n", comptime, runtime);
}

$ clang++ -target powerpc64le-unknown-linux-gnu -std=c++20 ilogb.cc && ./a.out
comptime(1)
runtime(0)
clang/test/Sema/constant-builtins-ilogb.cpp
1

There seems to be no C language test in the patch (although the builtin presumably is okay at least as part of arithmetic constant expressions).

@aaron.ballman, what are your thoughts re: integer constant expression contexts? For example:

struct C { int x : __builtin_ilogb(1. + 1.); };
60

This test also fails for PPC double-double (which does not increase the range of exponents).

With C++, it is presumably possible for you to verify using predefined floating point constants with platform-specific values, e.g., __FLT_DENORM_MIN__, and checking the answer by dividing or multiplying the number by 2 (or more generally, the base) in a constexpr function repeatedly.

This revision now requires changes to proceed.Oct 30 2022, 5:35 PM
aaron.ballman added inline comments.Oct 31 2022, 7:56 AM
clang/test/Sema/constant-builtins-ilogb.cpp
1

WG14 adopted https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2713.htm for C2x to clarify that implementations are not allowed to extend what they consider to be an integer constant expression. The operand in this case is a function call expression, which is not one of the permissible things in an ICE, so the standard doesn't want us to make it one.

I believe that Clang's response to that paper is to implement it to the letter rather than to the intent. e.g., issue a warning that a constant expression is being folded into an ICE but otherwise accept the code. We have too many situations already in which we fold a constant expression to an ICE and changing that behavior would be observable (and a performance regression). So I think it's fine for us to treat the builtin call as an ICE so long as we issue a (pedantic) warning about folding it.

(FWIW, I don't think it qualifies as an arithmetic constant expression because that also doesn't allow function call expressions. But we can extend the definition of an arithmetic constant expression.)

clang/test/Sema/constant-builtins-ilogb.cpp
1

Thanks Aaron. I think a C language test for this patch may be a good way to explore the extension space (and any pedantic conformance diagnostics).

Particular to the example above and ICEs is that the argument involves floating-point operations. Would the desired model be that the call is considered a literal for the purposes of the language requirements?

It seems that built-ins that are accepted in constant expressions in C are a more general issue though (and I am not sure if, for this patch, we defer the issue for some general approach that would take care of the class of problems as a whole).

aaron.ballman added inline comments.Oct 31 2022, 10:42 AM
clang/test/Sema/constant-builtins-ilogb.cpp
1

Thanks Aaron. I think a C language test for this patch may be a good way to explore the extension space (and any pedantic conformance diagnostics).

+1, especially because C2x has support for constexpr object definitions (which we've not implemented in Clang yet).

Particular to the example above and ICEs is that the argument involves floating-point operations. Would the desired model be that the call is considered a literal for the purposes of the language requirements?

I think that model makes the most sense to me. If we can compute the correct answer at compile time, it seems a bit silly to defer until runtime just because the builtin looks like a function call expression.

It seems that built-ins that are accepted in constant expressions in C are a more general issue though (and I am not sure if, for this patch, we defer the issue for some general approach that would take care of the class of problems as a whole).

Yeah, I think it is a more general issue with builtins in C (and how we handle constant expression evaluation in C). I don't think this patch will make the problem significantly *worse*, so I think I'd be fine not addressing the specific concern for this builtin in this patch. But it would probably make a lot of sense for us to fix up the builtin situation before converting a ton of math library functions to builtins since all of those are going to have the same class of concern.