This is an archive of the discontinued LLVM Phabricator instance.

InstCombine: Recognize fneg when performed as bitcasted integer
ClosedPublic

Authored by arsenm on Jun 1 2023, 2:00 PM.

Details

Summary

This is a resurrection of D18874. This was previously wrong with
fneg conflated with fsub, but we now have a proper fneg instruction.
Additionally, I think it is now clearer that IR float=IEEE float,
and a different bit layout would require adding a different IR type.

Diff Detail

Event Timeline

arsenm created this revision.Jun 1 2023, 2:00 PM
Herald added a project: Restricted Project. · View Herald TranscriptJun 1 2023, 2:00 PM
arsenm requested review of this revision.Jun 1 2023, 2:00 PM
Herald added a project: Restricted Project. · View Herald TranscriptJun 1 2023, 2:00 PM
Herald added a subscriber: wdng. · View Herald Transcript

In general, we need to be careful introducing floating-point values where the don't otherwise exist, for a couple reasons. One, loading a value into an x87 floating-point register can quiet SNaNs. And two, certain compiler options obligate us to avoid floating-point code. That said, if the source value is already floating-point, it should be fine to introduce floating-point operations on it, I think.

The other possible issue is the interaction with "UnsafeFPMath" and related CodeGen options. Fast-math is supposed to be a floating-point thing, but if we transform integer ops into floating-point ops, they effectively apply to integer math. Which is probably (?) not what the user expects. (I think fast-math flags on individual instructions should interact correctly, since the "fneg" created by this transform won't have any flags; my concern is with the global flags.)

arsenm added a comment.Jun 1 2023, 3:00 PM

In general, we need to be careful introducing floating-point values where the don't otherwise exist, for a couple reasons. One, loading a value into an x87 floating-point register can quiet SNaNs. And two, certain compiler options obligate us to avoid floating-point code. That said, if the source value is already floating-point, it should be fine to introduce floating-point operations on it, I think.

fneg is defined as a bitwise operation. It's not a real FP operation, it's a bitwise operation on FP types. It's only permitted to change the sign bit. If x87 decided to quiet an snan in the fneg implementation, the lowering is broken. All concerns about floating-pointness and fast math should be gone since fsub is no longer involved.

If you compile the following for x86 with -mno-sse:

void f(float *x) { *x = -*x; }

You get the following:

movl    4(%esp), %eax
flds    (%eax)
fchs
fstps   (%eax)
retl

This transforms snan into qnan. That's arguably incorrect, but it's what every x86 compiler has emitted since forever, so nobody has tried to fix it.

The whole thing isn't really relevant for this patch, though, because you trigger the exact same thing if you bitcast the float to an i32. (The exception is triggered on the flds which loads the register, not the fchs which actually flips the sign bit.)


In any case, I'm more concerned about the UnsafeFPMath thing.

arsenm added a comment.Jun 8 2023, 3:34 PM

In any case, I'm more concerned about the UnsafeFPMath thing.

We already do this transform in the DAG, guarded by isFNegFree. I don't think this is a real issue. You can't really do anything unsafe with an fneg on its own, it has to be connected to something where the unsafe flag already applied. e.g. fadd (x, fneg(x)) -> 0 already had to have an nnan

You can't really do anything unsafe with an fneg on its own, it has to be connected to something where the unsafe flag already applied.

If the fneg is the only use of an unsafe floating-point operation, there isn't really any way to observe whether the fneg itself is safe or unsafe. But we could produce contradictory results if the operand has multiple uses. For example, it's normally safe to assume x ^ 0x80000000 != x. But if fneg is treated as an unsafe math operation, there are some situations where fneg(x) == x. This could lead to a miscompile, at least in theory. Not sure how likely it is for anyone to trip over that in practice.

arsenm updated this revision to Diff 554776.Aug 30 2023, 10:37 AM

Check noimplicitfloat (though I don't think it should be necessary, but it isn't really well defined what it means)

You can't really do anything unsafe with an fneg on its own, it has to be connected to something where the unsafe flag already applied.

If the fneg is the only use of an unsafe floating-point operation, there isn't really any way to observe whether the fneg itself is safe or unsafe. But we could produce contradictory results if the operand has multiple uses. For example, it's normally safe to assume x ^ 0x80000000 != x. But if fneg is treated as an unsafe math operation, there are some situations where fneg(x) == x. This could lead to a miscompile, at least in theory. Not sure how likely it is for anyone to trip over that in practice.

I think you have no right to complain once your code touches fast math code. It's not a miscompile if you permitted it. The sign bit changing would have to originate from the fast math imbued instruction

imo should require a cast back to float. If we are keeping it as an integer
I think xor is more recognizable/canonical.

efriedma accepted this revision.Aug 31 2023, 3:15 PM

LGTM

I think you have no right to complain once your code touches fast math code. It's not a miscompile if you permitted it. The sign bit changing would have to originate from the fast math imbued instruction

The point here is that there should be a boundary between fast and non-fast instructions: if you have a "fast" algorithm, you should be able to use its output in non-"fast" code without infecting it.

But I think it only ends up being an issue with the function-wide flag, not the per-instruction flags, so anyone who cares about that can just avoid setting that bit.

imo should require a cast back to float. If we are keeping it as an integer, I think xor is more recognizable/canonical.

fneg seems more likely to actually correspond to a natively supported instruction, and more likely to be combined with adjacent operations.

This revision is now accepted and ready to land.Aug 31 2023, 3:15 PM
goldstein.w.n added a comment.EditedAug 31 2023, 3:48 PM

LGTM

I think you have no right to complain once your code touches fast math code. It's not a miscompile if you permitted it. The sign bit changing would have to originate from the fast math imbued instruction

The point here is that there should be a boundary between fast and non-fast instructions: if you have a "fast" algorithm, you should be able to use its output in non-"fast" code without infecting it.

But I think it only ends up being an issue with the function-wide flag, not the per-instruction flags, so anyone who cares about that can just avoid setting that bit.

imo should require a cast back to float. If we are keeping it as an integer, I think xor is more recognizable/canonical.

fneg seems more likely to actually correspond to a natively supported instruction, and more likely to be combined with adjacent operations.

Depends which context you are talking about. If its with other float instructions sure. If its with integer instructions I'm not so sure.
Thats why I think it should be (bitcast (xor (bitcast)) not just (xor (bitcast)).

Edit: Although it could go either way, so no real strong opposition.

Depends which context you are talking about. If its with other float instructions sure. If its with integer instructions I'm not so sure.
Thats why I think it should be (bitcast (xor (bitcast)) not just (xor (bitcast)).

Edit: Although it could go either way, so no real strong opposition.

Canonical form tends to push things into sources, not the other direction. If the integer operation was useful downstream it should have folded before reaching here

uabelho added a subscriber: uabelho.Sep 1 2023, 3:28 AM