Commit fafb4769 authored by Richard Stotz's avatar Richard Stotz Committed by Commit Bot

[turbofan][wasm][arm64] Improved saturated conversions float32 to int32.

The design of this change was discussed here:
https://docs.google.com/document/d/12otOj6SyXMXj0Dnnx9B6MGLMRwHPhg6RIZRazVw3tFA/

Bug: v8:10720
Change-Id: I8292dcf7272bdf4526a2d630b49fc374cdb01bdc
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2304570
Commit-Queue: Richard Stotz <rstz@chromium.org>
Reviewed-by: 's avatarGeorg Neis <neis@chromium.org>
Reviewed-by: 's avatarAndreas Haas <ahaas@chromium.org>
Cr-Commit-Position: refs/heads/master@{#68994}
parent f91231f1
......@@ -1518,24 +1518,32 @@ CodeGenerator::CodeGenResult CodeGenerator::AssembleArchInstruction(
case kArm64Float64ToFloat32:
__ Fcvt(i.OutputDoubleRegister().S(), i.InputDoubleRegister(0));
break;
case kArm64Float32ToInt32:
case kArm64Float32ToInt32: {
__ Fcvtzs(i.OutputRegister32(), i.InputFloat32Register(0));
// Avoid INT32_MAX as an overflow indicator and use INT32_MIN instead,
// because INT32_MIN allows easier out-of-bounds detection.
__ Cmn(i.OutputRegister32(), 1);
__ Csinc(i.OutputRegister32(), i.OutputRegister32(), i.OutputRegister32(),
vc);
bool set_overflow_to_min_i32 = MiscField::decode(instr->opcode());
if (set_overflow_to_min_i32) {
// Avoid INT32_MAX as an overflow indicator and use INT32_MIN instead,
// because INT32_MIN allows easier out-of-bounds detection.
__ Cmn(i.OutputRegister32(), 1);
__ Csinc(i.OutputRegister32(), i.OutputRegister32(),
i.OutputRegister32(), vc);
}
break;
}
case kArm64Float64ToInt32:
__ Fcvtzs(i.OutputRegister32(), i.InputDoubleRegister(0));
break;
case kArm64Float32ToUint32:
case kArm64Float32ToUint32: {
__ Fcvtzu(i.OutputRegister32(), i.InputFloat32Register(0));
// Avoid UINT32_MAX as an overflow indicator and use 0 instead,
// because 0 allows easier out-of-bounds detection.
__ Cmn(i.OutputRegister32(), 1);
__ Adc(i.OutputRegister32(), i.OutputRegister32(), Operand(0));
bool set_overflow_to_min_u32 = MiscField::decode(instr->opcode());
if (set_overflow_to_min_u32) {
// Avoid UINT32_MAX as an overflow indicator and use 0 instead,
// because 0 allows easier out-of-bounds detection.
__ Cmn(i.OutputRegister32(), 1);
__ Adc(i.OutputRegister32(), i.OutputRegister32(), Operand(0));
}
break;
}
case kArm64Float64ToUint32:
__ Fcvtzu(i.OutputRegister32(), i.InputDoubleRegister(0));
break;
......
......@@ -1336,10 +1336,8 @@ void InstructionSelector::VisitWord64Ror(Node* node) {
V(ChangeInt32ToFloat64, kArm64Int32ToFloat64) \
V(ChangeInt64ToFloat64, kArm64Int64ToFloat64) \
V(ChangeUint32ToFloat64, kArm64Uint32ToFloat64) \
V(TruncateFloat32ToInt32, kArm64Float32ToInt32) \
V(ChangeFloat64ToInt32, kArm64Float64ToInt32) \
V(ChangeFloat64ToInt64, kArm64Float64ToInt64) \
V(TruncateFloat32ToUint32, kArm64Float32ToUint32) \
V(ChangeFloat64ToUint32, kArm64Float64ToUint32) \
V(ChangeFloat64ToUint64, kArm64Float64ToUint64) \
V(TruncateFloat64ToInt64, kArm64Float64ToInt64) \
......@@ -1640,6 +1638,28 @@ void InstructionSelector::VisitUint32MulHigh(Node* node) {
Emit(kArm64Lsr, g.DefineAsRegister(node), smull_operand, g.TempImmediate(32));
}
void InstructionSelector::VisitTruncateFloat32ToInt32(Node* node) {
Arm64OperandGenerator g(this);
InstructionCode opcode = kArm64Float32ToInt32;
TruncateKind kind = OpParameter<TruncateKind>(node->op());
opcode |= MiscField::encode(kind == TruncateKind::kSetOverflowToMin);
Emit(opcode, g.DefineAsRegister(node), g.UseRegister(node->InputAt(0)));
}
void InstructionSelector::VisitTruncateFloat32ToUint32(Node* node) {
Arm64OperandGenerator g(this);
InstructionCode opcode = kArm64Float32ToUint32;
TruncateKind kind = OpParameter<TruncateKind>(node->op());
if (kind == TruncateKind::kSetOverflowToMin) {
opcode |= MiscField::encode(true);
}
Emit(opcode, g.DefineAsRegister(node), g.UseRegister(node->InputAt(0)));
}
void InstructionSelector::VisitTryTruncateFloat32ToInt64(Node* node) {
Arm64OperandGenerator g(this);
......@@ -3694,7 +3714,8 @@ InstructionSelector::SupportedMachineOperatorFlags() {
MachineOperatorBuilder::kInt32DivIsSafe |
MachineOperatorBuilder::kUint32DivIsSafe |
MachineOperatorBuilder::kWord32ReverseBits |
MachineOperatorBuilder::kWord64ReverseBits;
MachineOperatorBuilder::kWord64ReverseBits |
MachineOperatorBuilder::kSatConversionIsSafe;
}
// static
......
......@@ -619,6 +619,11 @@ TNode<Float64T> CodeAssembler::RoundIntPtrToFloat64(Node* value) {
return UncheckedCast<Float64T>(raw_assembler()->ChangeInt32ToFloat64(value));
}
TNode<Int32T> CodeAssembler::TruncateFloat32ToInt32(
SloppyTNode<Float32T> value) {
return UncheckedCast<Int32T>(raw_assembler()->TruncateFloat32ToInt32(
value, TruncateKind::kSetOverflowToMin));
}
#define DEFINE_CODE_ASSEMBLER_UNARY_OP(name, ResType, ArgType) \
TNode<ResType> CodeAssembler::name(SloppyTNode<ArgType> a) { \
return UncheckedCast<ResType>(raw_assembler()->name(a)); \
......
......@@ -321,7 +321,6 @@ TNode<Float64T> Float64Add(TNode<Float64T> a, TNode<Float64T> b);
V(BitcastMaybeObjectToWord, IntPtrT, MaybeObject) \
V(BitcastWordToTagged, Object, WordT) \
V(BitcastWordToTaggedSigned, Smi, WordT) \
V(TruncateFloat32ToInt32, Int32T, Float32T) \
V(TruncateFloat64ToFloat32, Float32T, Float64T) \
V(TruncateFloat64ToWord32, Uint32T, Float64T) \
V(TruncateInt64ToInt32, Int32T, Int64T) \
......@@ -936,6 +935,12 @@ class V8_EXPORT_PRIVATE CodeAssembler {
// No-op on 32-bit, otherwise sign extend.
TNode<IntPtrT> ChangeInt32ToIntPtr(TNode<Word32T> value);
// Truncates a float to a 32-bit integer. If the float is outside of 32-bit
// range, make sure that overflow detection is easy. In particular, return
// int_min instead of int_max on arm platforms by using parameter
// kSetOverflowToMin.
TNode<Int32T> TruncateFloat32ToInt32(SloppyTNode<Float32T> value);
// No-op that guarantees that the value is kept alive till this point even
// if GC happens.
Node* Retain(Node* value);
......
......@@ -235,8 +235,6 @@ ShiftKind ShiftKindOf(Operator const* op) {
V(ChangeFloat64ToUint64, Operator::kNoProperties, 1, 0, 1) \
V(TruncateFloat64ToInt64, Operator::kNoProperties, 1, 0, 1) \
V(TruncateFloat64ToUint32, Operator::kNoProperties, 1, 0, 1) \
V(TruncateFloat32ToInt32, Operator::kNoProperties, 1, 0, 1) \
V(TruncateFloat32ToUint32, Operator::kNoProperties, 1, 0, 1) \
V(TryTruncateFloat32ToInt64, Operator::kNoProperties, 1, 0, 2) \
V(TryTruncateFloat64ToInt64, Operator::kNoProperties, 1, 0, 2) \
V(TryTruncateFloat32ToUint64, Operator::kNoProperties, 1, 0, 2) \
......@@ -1014,6 +1012,55 @@ const Operator* MachineOperatorBuilder::UnalignedStore(
UNREACHABLE();
}
template <TruncateKind kind>
struct TruncateFloat32ToUint32Operator : Operator1<TruncateKind> {
TruncateFloat32ToUint32Operator()
: Operator1(IrOpcode::kTruncateFloat32ToUint32, Operator::kPure,
"TruncateFloat32ToUint32", 1, 0, 0, 1, 0, 0, kind) {}
};
const Operator* MachineOperatorBuilder::TruncateFloat32ToUint32(
TruncateKind kind) {
switch (kind) {
case TruncateKind::kArchitectureDefault:
return GetCachedOperator<TruncateFloat32ToUint32Operator<
TruncateKind::kArchitectureDefault>>();
case TruncateKind::kSetOverflowToMin:
return GetCachedOperator<
TruncateFloat32ToUint32Operator<TruncateKind::kSetOverflowToMin>>();
}
}
template <TruncateKind kind>
struct TruncateFloat32ToInt32Operator : Operator1<TruncateKind> {
TruncateFloat32ToInt32Operator()
: Operator1(IrOpcode::kTruncateFloat32ToInt32, Operator::kPure,
"TruncateFloat32ToInt32", 1, 0, 0, 1, 0, 0, kind) {}
};
const Operator* MachineOperatorBuilder::TruncateFloat32ToInt32(
TruncateKind kind) {
switch (kind) {
case TruncateKind::kArchitectureDefault:
return GetCachedOperator<
TruncateFloat32ToInt32Operator<TruncateKind::kArchitectureDefault>>();
case TruncateKind::kSetOverflowToMin:
return GetCachedOperator<
TruncateFloat32ToInt32Operator<TruncateKind::kSetOverflowToMin>>();
}
}
size_t hash_value(TruncateKind kind) { return static_cast<size_t>(kind); }
std::ostream& operator<<(std::ostream& os, TruncateKind kind) {
switch (kind) {
case TruncateKind::kArchitectureDefault:
return os << "kArchitectureDefault";
case TruncateKind::kSetOverflowToMin:
return os << "kSetOverflowToMin";
}
}
#define PURE(Name, properties, value_input_count, control_input_count, \
output_count) \
const Operator* MachineOperatorBuilder::Name() { \
......
......@@ -192,6 +192,12 @@ size_t hash_value(ShiftKind);
V8_EXPORT_PRIVATE std::ostream& operator<<(std::ostream&, ShiftKind);
ShiftKind ShiftKindOf(Operator const*) V8_WARN_UNUSED_RESULT;
// TruncateKind::kSetOverflowToMin sets the result of a saturating float-to-int
// conversion to INT_MIN if the conversion returns INT_MAX due to overflow. This
// makes it easier to detect an overflow. This parameter is ignored on platforms
// like x64 and ia32 where a range overflow does not result in INT_MAX.
enum class TruncateKind { kArchitectureDefault, kSetOverflowToMin };
// Interface for building machine-level operators. These operators are
// machine-level but machine-independent and thus define a language suitable
// for generating code to run on architectures such as ia32, x64, arm, etc.
......@@ -224,13 +230,14 @@ class V8_EXPORT_PRIVATE MachineOperatorBuilder final
kInt64AbsWithOverflow = 1u << 21,
kWord32Rol = 1u << 22,
kWord64Rol = 1u << 23,
kSatConversionIsSafe = 1u << 24,
kAllOptionalOps =
kFloat32RoundDown | kFloat64RoundDown | kFloat32RoundUp |
kFloat64RoundUp | kFloat32RoundTruncate | kFloat64RoundTruncate |
kFloat64RoundTiesAway | kFloat32RoundTiesEven | kFloat64RoundTiesEven |
kWord32Ctz | kWord64Ctz | kWord32Popcnt | kWord64Popcnt |
kWord32ReverseBits | kWord64ReverseBits | kInt32AbsWithOverflow |
kInt64AbsWithOverflow | kWord32Rol | kWord64Rol
kInt64AbsWithOverflow | kWord32Rol | kWord64Rol | kSatConversionIsSafe
};
using Flags = base::Flags<Flag, unsigned>;
......@@ -332,6 +339,11 @@ class V8_EXPORT_PRIVATE MachineOperatorBuilder final
// generate a mask with 0x1f on the amount ahead of generating the shift.
bool Word32ShiftIsSafe() const { return flags_ & kWord32ShiftIsSafe; }
// Return true if the target's implementation of float-to-int-conversions is a
// saturating conversion rounding towards 0. Otherwise, we have to manually
// generate the correct value if a saturating conversion is requested.
bool SatConversionIsSafe() const { return flags_ & kSatConversionIsSafe; }
const Operator* Word64And();
const Operator* Word64Or();
const Operator* Word64Xor();
......@@ -417,6 +429,10 @@ class V8_EXPORT_PRIVATE MachineOperatorBuilder final
// in the target type and are *not* defined for other inputs.
// Use narrowing change operators only when there is a static guarantee that
// the input value is representable in the target value.
//
// Some operators can have the behaviour on overflow change through specifying
// TruncateKind. The exact semantics are documented in the tests in
// test/cctest/compiler/test-run-machops.cc .
const Operator* ChangeFloat32ToFloat64();
const Operator* ChangeFloat64ToInt32(); // narrowing
const Operator* ChangeFloat64ToInt64();
......@@ -424,8 +440,8 @@ class V8_EXPORT_PRIVATE MachineOperatorBuilder final
const Operator* ChangeFloat64ToUint64();
const Operator* TruncateFloat64ToInt64();
const Operator* TruncateFloat64ToUint32();
const Operator* TruncateFloat32ToInt32();
const Operator* TruncateFloat32ToUint32();
const Operator* TruncateFloat32ToInt32(TruncateKind kind);
const Operator* TruncateFloat32ToUint32(TruncateKind kind);
const Operator* TryTruncateFloat32ToInt64();
const Operator* TryTruncateFloat64ToInt64();
const Operator* TryTruncateFloat32ToUint64();
......
......@@ -721,11 +721,11 @@ class V8_EXPORT_PRIVATE RawMachineAssembler {
Node* TruncateFloat64ToUint32(Node* a) {
return AddNode(machine()->TruncateFloat64ToUint32(), a);
}
Node* TruncateFloat32ToInt32(Node* a) {
return AddNode(machine()->TruncateFloat32ToInt32(), a);
Node* TruncateFloat32ToInt32(Node* a, TruncateKind kind) {
return AddNode(machine()->TruncateFloat32ToInt32(kind), a);
}
Node* TruncateFloat32ToUint32(Node* a) {
return AddNode(machine()->TruncateFloat32ToUint32(), a);
Node* TruncateFloat32ToUint32(Node* a, TruncateKind kind) {
return AddNode(machine()->TruncateFloat32ToUint32(kind), a);
}
Node* TryTruncateFloat32ToInt64(Node* a) {
return AddNode(machine()->TryTruncateFloat32ToInt64(), a);
......
......@@ -1550,11 +1550,17 @@ MachineType FloatConvertType(wasm::WasmOpcode opcode) {
const Operator* ConvertOp(WasmGraphBuilder* builder, wasm::WasmOpcode opcode) {
switch (opcode) {
case wasm::kExprI32SConvertF32:
return builder->mcgraph()->machine()->TruncateFloat32ToInt32(
TruncateKind::kSetOverflowToMin);
case wasm::kExprI32SConvertSatF32:
return builder->mcgraph()->machine()->TruncateFloat32ToInt32();
return builder->mcgraph()->machine()->TruncateFloat32ToInt32(
TruncateKind::kArchitectureDefault);
case wasm::kExprI32UConvertF32:
return builder->mcgraph()->machine()->TruncateFloat32ToUint32(
TruncateKind::kSetOverflowToMin);
case wasm::kExprI32UConvertSatF32:
return builder->mcgraph()->machine()->TruncateFloat32ToUint32();
return builder->mcgraph()->machine()->TruncateFloat32ToUint32(
TruncateKind::kArchitectureDefault);
case wasm::kExprI32SConvertF64:
case wasm::kExprI32SConvertSatF64:
return builder->mcgraph()->machine()->ChangeFloat64ToInt32();
......@@ -1753,6 +1759,9 @@ Node* WasmGraphBuilder::BuildIntConvertFloat(Node* input,
}
return converted_value;
}
if (mcgraph()->machine()->SatConversionIsSafe()) {
return converted_value;
}
Node* test = ConvertSaturateTest(this, opcode, int_ty, float_ty, trunc,
converted_value);
Diamond tl_d(graph(), mcgraph()->common(), test, BranchHint::kFalse);
......
......@@ -110,7 +110,7 @@ Node* ToInt32(RawMachineAssembler* m, MachineType type, Node* a) {
case MachineRepresentation::kWord64:
return m->TruncateInt64ToInt32(a);
case MachineRepresentation::kFloat32:
return m->TruncateFloat32ToInt32(a);
return m->TruncateFloat32ToInt32(a, TruncateKind::kArchitectureDefault);
case MachineRepresentation::kFloat64:
return m->RoundFloat64ToInt32(a);
default:
......
......@@ -4173,8 +4173,6 @@ TEST(RunChangeUint32ToFloat64) {
TEST(RunTruncateFloat32ToInt32) {
BufferedRawMachineAssemblerTester<int32_t> m(MachineType::Float32());
m.Return(m.TruncateFloat32ToInt32(m.Parameter(0)));
// The upper bound is (INT32_MAX + 1), which is the lowest float-representable
// number above INT32_MAX which cannot be represented as int32.
float upper_bound = 2147483648.0f;
......@@ -4182,31 +4180,98 @@ TEST(RunTruncateFloat32ToInt32) {
// representable as float, and no number between (INT32_MIN - 1) and INT32_MIN
// is.
float lower_bound = static_cast<float>(INT32_MIN);
FOR_FLOAT32_INPUTS(i) {
if (i < upper_bound && i >= lower_bound) {
CHECK_FLOAT_EQ(static_cast<int32_t>(i), m.Call(i));
{
BufferedRawMachineAssemblerTester<int32_t> m(MachineType::Float32());
m.Return(m.TruncateFloat32ToInt32(m.Parameter(0),
TruncateKind::kArchitectureDefault));
FOR_FLOAT32_INPUTS(i) {
if (i < upper_bound && i >= lower_bound) {
CHECK_FLOAT_EQ(static_cast<int32_t>(i), m.Call(i));
} else if (i < lower_bound) {
CHECK_FLOAT_EQ(std::numeric_limits<int32_t>::min(), m.Call(i));
} else if (i >= upper_bound) {
#if V8_TARGET_ARCH_IA32 || V8_TARGET_ARCH_X64
CHECK_FLOAT_EQ(std::numeric_limits<int32_t>::min(), m.Call(i));
#elif V8_TARGET_ARCH_ARM64
CHECK_FLOAT_EQ(std::numeric_limits<int32_t>::max(), m.Call(i));
#endif
} else {
DCHECK(std::isnan(i));
#if V8_TARGET_ARCH_IA32 || V8_TARGET_ARCH_X64
CHECK_FLOAT_EQ(std::numeric_limits<int32_t>::min(), m.Call(i));
#elif V8_TARGET_ARCH_ARM64
CHECK_FLOAT_EQ(0, m.Call(i));
#endif
}
}
}
{
BufferedRawMachineAssemblerTester<int32_t> m(MachineType::Float32());
m.Return(m.TruncateFloat32ToInt32(m.Parameter(0),
TruncateKind::kSetOverflowToMin));
FOR_FLOAT32_INPUTS(i) {
if (i < upper_bound && i >= lower_bound) {
CHECK_FLOAT_EQ(static_cast<int32_t>(i), m.Call(i));
} else if (!std::isnan(i)) {
CHECK_FLOAT_EQ(std::numeric_limits<int32_t>::min(), m.Call(i));
} else {
DCHECK(std::isnan(i));
#if V8_TARGET_ARCH_IA32 || V8_TARGET_ARCH_X64
CHECK_FLOAT_EQ(std::numeric_limits<int32_t>::min(), m.Call(i));
#elif V8_TARGET_ARCH_ARM64
CHECK_FLOAT_EQ(0, m.Call(i));
#endif
}
}
}
}
TEST(RunTruncateFloat32ToUint32) {
BufferedRawMachineAssemblerTester<uint32_t> m(MachineType::Float32());
m.Return(m.TruncateFloat32ToUint32(m.Parameter(0)));
// The upper bound is (UINT32_MAX + 1), which is the lowest
// float-representable number above UINT32_MAX which cannot be represented as
// uint32.
double upper_bound = 4294967296.0f;
double lower_bound = -1.0f;
FOR_UINT32_INPUTS(i) {
volatile float input = static_cast<float>(i);
if (input < upper_bound) {
CHECK_EQ(static_cast<uint32_t>(input), m.Call(input));
// No tests outside the range of UINT32 are performed, as the semantics are
// tricky on x64. On this architecture, the assembler transforms float32 into
// a signed int64 instead of an unsigned int32. Overflow can then be detected
// by converting back to float and testing for equality as done in
// wasm-compiler.cc .
//
// On arm architectures, TruncateKind::kArchitectureDefault rounds towards 0
// upon overflow and returns 0 if the input is NaN.
// TruncateKind::kSetOverflowToMin returns 0 on overflow and NaN.
{
BufferedRawMachineAssemblerTester<uint32_t> m(MachineType::Float32());
m.Return(m.TruncateFloat32ToUint32(m.Parameter(0),
TruncateKind::kArchitectureDefault));
FOR_UINT32_INPUTS(i) {
volatile float input = static_cast<float>(i);
if (input < upper_bound) {
CHECK_EQ(static_cast<uint32_t>(input), m.Call(input));
}
}
FOR_FLOAT32_INPUTS(j) {
if ((j < upper_bound) && (j > lower_bound)) {
CHECK_FLOAT_EQ(static_cast<uint32_t>(j), m.Call(j));
}
}
}
FOR_FLOAT32_INPUTS(j) {
if ((j < upper_bound) && (j > lower_bound)) {
CHECK_FLOAT_EQ(static_cast<uint32_t>(j), m.Call(j));
{
BufferedRawMachineAssemblerTester<uint32_t> m(MachineType::Float32());
m.Return(m.TruncateFloat32ToUint32(m.Parameter(0),
TruncateKind::kSetOverflowToMin));
FOR_UINT32_INPUTS(i) {
volatile float input = static_cast<float>(i);
if (input < upper_bound) {
CHECK_EQ(static_cast<uint32_t>(input), m.Call(input));
}
}
FOR_FLOAT32_INPUTS(j) {
if ((j < upper_bound) && (j > lower_bound)) {
CHECK_FLOAT_EQ(static_cast<uint32_t>(j), m.Call(j));
}
}
}
}
......
......@@ -107,7 +107,7 @@ Node* ToInt32(RawMachineAssembler* m, MachineType type, Node* a) {
case MachineRepresentation::kWord64:
return m->TruncateInt64ToInt32(a);
case MachineRepresentation::kFloat32:
return m->TruncateFloat32ToInt32(a);
return m->TruncateFloat32ToInt32(a, TruncateKind::kArchitectureDefault);
case MachineRepresentation::kFloat64:
return m->RoundFloat64ToInt32(a);
default:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment