Skip to content

Commit 8332aaa

Browse files
alexmalyshevmeta-codesync[bot]
authored andcommitted
Implement float comparisons with primitive operations
Summary: Lower float comparisons to unboxed `PrimitiveCompare` on `CDouble` instead of falling back to `PyFloat_Type.tp_richcompare`. This avoids boxing, a type-generic dispatch, and a call per comparison on the hot path. The subtlety is NaN. Python requires every ordering comparison involving a NaN to be false, `nan == nan` to be false, and `nan != nan` to be true, but the hardware float-compare instructions expose different flags for unordered (NaN) operands on x86-64 (`comisd`) and ARM64 (`fcmp`). The HIR stays architecture-independent, it emits the natural comparison (`PrimitiveCompare<LessThan> x, y` and friends, and `PrimitiveCompare<Equal>`/`<NotEqual>` for `==`/`!=`), and the NaN-correct condition is chosen when the compare is lowered to LIR. The choice must be made at the LIR-opcode level (not by, say, swapping operands inside the compare's asm emission) because a compare that feeds a branch is fused into a conditional jump whose condition is derived from the LIR opcode via `compareToBranchCC`, reusing the compare's flags. If the compare's asm silently swapped operands, the fused jump, which is unaware of that swap, would test the wrong condition. So the LIR generator picks the opcode and operand order so that the opcode's standard condition is NaN-correct for both the standalone `setcc`/`cset` and the fused `jcc`/`b.cc`: - x86-64: `comisd` sets the flags in an unsigned sense and marks unordered operands with CF=ZF=PF=1, so only the above / above-equal conditions are false for NaN. Every ordering is expressed as `>` / `>=` (`kGreaterThanUnsigned`/`kGreaterThanEqualUnsigned`), swapping the operands for `<` / `<=`. `comisd` folds unordered into ZF (the equality flag), so `==`/`!=` have no NaN-correct single-instruction form; they are decomposed into the ordering comparisons: `a == b` becomes `(a <= b) && (a >= b)` (both false when a NaN is involved), and `a != b` negates that. `(a < b) || (a > b)` would be wrong, it yields false for NaN. - ARM64: `fcmp` sets NZCV and marks unordered operands with C=1, V=1 while leaving Z=0. `>` / `>=` use the signed `GT`/`GE` conditions and `<` / `<=` the unsigned `LO`/`LS` conditions (all false for unordered); `==`/`!=` use `EQ`/`NE` (which key off Z alone and are already NaN-correct). `TranslateCompare` is unchanged in spirit: it emits `comisd`/`fcmp` for double operands and falls through to the shared per-opcode condition switch, so the standalone and fused forms stay consistent. Floats are neither signed nor unsigned, so both the signed and unsigned `PrimitiveCompareOp` variants are handled together in the generator (Static Python `double` emits the unsigned variants; the Python-float path emits the natural ones). A side benefit is that Static Python `double` `<`/`<=` are now NaN-correct too (they previously used `setb`/`setbe`, which are true for unordered operands). Unboxing the nms float comparisons speeds it up ~7%; the other workloads are within noise. Reviewed By: mpage Differential Revision: D110238069 fbshipit-source-id: 24952346336c44ea6d7225add39596c255a53f8b
1 parent c0124b8 commit 8332aaa

12 files changed

Lines changed: 242 additions & 75 deletions

File tree

cinderx/Jit/codegen/autogen.cpp

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,13 @@ void TranslateCompare(Environ* env, const Instruction* instr) {
531531
} else if (!inp1->isVecD()) {
532532
as->cmp(AutoTranslator::getGp(inp0), AutoTranslator::getGp(inp1));
533533
} else {
534+
// Floating-point comparison; both operands are in XMM registers. `comisd`
535+
// sets the flags in the unsigned sense (CF/ZF) and reports unordered (NaN)
536+
// operands as CF=ZF=PF=1; the shared condition switch below then reads
537+
// those flags. NaN-correctness and the comparison direction are chosen
538+
// when the compare is lowered to LIR, so a compare fused into a branch,
539+
// which reuses these flags via compareToBranchCC on the LIR opcode, stays
540+
// consistent with the standalone setcc emitted here.
534541
as->comisd(AutoTranslator::getVecD(inp0), AutoTranslator::getVecD(inp1));
535542
}
536543
auto output = AutoTranslator::getGp(instr->output());
@@ -566,7 +573,7 @@ void TranslateCompare(Environ* env, const Instruction* instr) {
566573
as->setbe(output);
567574
break;
568575
default:
569-
JIT_ABORT("bad instruction for TranslateCompare");
576+
JIT_ABORT("Bad instruction for TranslateCompare {}", instr->opname());
570577
}
571578
if (instr->output()->dataType() != lir::Operand::k8bit) {
572579
as->movzx(
@@ -593,6 +600,11 @@ void TranslateCompare(Environ* env, const Instruction* instr) {
593600
} else if (!inp1->isVecD()) {
594601
as->cmp(AutoTranslator::getGpWiden(inp0), AutoTranslator::getGpWiden(inp1));
595602
} else {
603+
// Floating-point comparison, see the note in the x86-64 path. `fcmp` sets
604+
// NZCV (unordered/NaN operands set C=1, V=1 while leaving Z=0), the shared
605+
// condition switch below picks the cset. NaN-correctness and the comparison
606+
// direction are chosen when the compare is lowered to LIR, keeping the
607+
// standalone cset and any fused b.cc consistent.
596608
as->fcmp(AutoTranslator::getVecD(inp0), AutoTranslator::getVecD(inp1));
597609
}
598610

@@ -629,7 +641,7 @@ void TranslateCompare(Environ* env, const Instruction* instr) {
629641
as->cset(output, arm::CondCode::kLS);
630642
break;
631643
default:
632-
JIT_ABORT("bad instruction for TranslateCompare");
644+
JIT_ABORT("Bad instruction for TranslateCompare {}", instr->opname());
633645
}
634646
#else
635647
CINDER_UNSUPPORTED

cinderx/Jit/hir/hir.cpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,6 @@ bool Instr::isReplayable() const {
394394
case Opcode::kCIntToCBool:
395395
case Opcode::kCompactLongUnbox:
396396
case Opcode::kDoubleBinaryOp:
397-
case Opcode::kFloatCompare:
398397
case Opcode::kFormatValue:
399398
case Opcode::kFormatWithSpec:
400399
case Opcode::kGetSecondOutput:
@@ -760,7 +759,6 @@ bool isPassthrough(const Instr& instr) {
760759
case Opcode::kFillTypeAttrCache:
761760
case Opcode::kFillTypeMethodCache:
762761
case Opcode::kFloatBinaryOp:
763-
case Opcode::kFloatCompare:
764762
case Opcode::kFormatValue:
765763
case Opcode::kFormatWithSpec:
766764
case Opcode::kGetAIter:

cinderx/Jit/hir/hir.h

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1785,32 +1785,6 @@ class INSTR_CLASS(
17851785
CompareOp op_;
17861786
};
17871787

1788-
// Perform the comparison indicated by op between two floats
1789-
class INSTR_CLASS(
1790-
FloatCompare,
1791-
(TFloatExact, TFloatExact),
1792-
HasOutput,
1793-
Operands<2>) {
1794-
public:
1795-
FloatCompare(Register* dst, CompareOp op, Register* left, Register* right)
1796-
: InstrT(dst, left, right), op_(op) {}
1797-
1798-
CompareOp op() const {
1799-
return op_;
1800-
}
1801-
1802-
Register* left() const {
1803-
return getOperand(0);
1804-
}
1805-
1806-
Register* right() const {
1807-
return getOperand(1);
1808-
}
1809-
1810-
private:
1811-
CompareOp op_;
1812-
};
1813-
18141788
// Perform the comparison indicated by op between two longs
18151789
class INSTR_CLASS(
18161790
LongCompare,

cinderx/Jit/hir/instr_effects.cpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ MemoryEffects memoryEffects(const Instr& inst) {
4141
case Opcode::kDeopt:
4242
case Opcode::kDeoptPatchpoint:
4343
case Opcode::kDoubleBinaryOp:
44-
case Opcode::kFloatCompare:
4544
case Opcode::kGetSecondOutput:
4645
case Opcode::kHintType:
4746
case Opcode::kIndexUnbox:
@@ -400,7 +399,6 @@ bool hasArbitraryExecution(const Instr& inst) {
400399
case Opcode::kDeoptPatchpoint:
401400
case Opcode::kDoubleBinaryOp:
402401
case Opcode::kEndInlinedFunction:
403-
case Opcode::kFloatCompare:
404402
case Opcode::kGetSecondOutput:
405403
case Opcode::kGuardIs:
406404
case Opcode::kHintType:

cinderx/Jit/hir/ops.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ namespace cinderx::jit::hir {
5757
V(FillTypeAttrCache) \
5858
V(FillTypeMethodCache) \
5959
V(FloatBinaryOp) \
60-
V(FloatCompare) \
6160
V(FormatValue) \
6261
V(FormatWithSpec) \
6362
V(GetAIter) \

cinderx/Jit/hir/parser.cpp

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -588,15 +588,6 @@ HIRParser::parseInstr(std::string_view opcode, Register* dst, int bb_index) {
588588
instruction = newInstr<Compare>(dst, op, left, right);
589589
break;
590590
}
591-
case Opcode::kFloatCompare: {
592-
expect("<");
593-
CompareOp op = ParseCompareOpName(getNextToken());
594-
expect(">");
595-
auto left = parseRegister();
596-
auto right = parseRegister();
597-
NEW_INSTR(FloatCompare, dst, op, left, right);
598-
break;
599-
}
600591
case Opcode::kLongCompare: {
601592
expect("<");
602593
CompareOp op = ParseCompareOpName(getNextToken());

cinderx/Jit/hir/pass.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,6 @@ Type outputType(
354354
}
355355
case Opcode::kFloatBinaryOp:
356356
return TFloatExact;
357-
case Opcode::kFloatCompare:
358357
case Opcode::kLongCompare:
359358
case Opcode::kUnicodeCompare:
360359
return TImmortalBool;

cinderx/Jit/hir/printer.cpp

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -474,10 +474,6 @@ static std::string format_immediates(const Function* func, const Instr& instr) {
474474
const auto& cmp = static_cast<const Compare&>(instr);
475475
return std::string{GetCompareOpName(cmp.op())};
476476
}
477-
case Opcode::kFloatCompare: {
478-
const auto& cmp = static_cast<const FloatCompare&>(instr);
479-
return std::string{GetCompareOpName(cmp.op())};
480-
}
481477
case Opcode::kLongCompare: {
482478
const auto& cmp = static_cast<const LongCompare&>(instr);
483479
return std::string{GetCompareOpName(cmp.op())};

cinderx/Jit/hir/simplify.cpp

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -430,12 +430,22 @@ Register* simplifyCompare(Env& env, const Compare* instr) {
430430
}
431431
}
432432

433-
// Emit FloatCompare if both args are FloatExact and the op is supported
434-
// between two longs.
433+
// Emit primitive comparisons for floats: unbox and compare as CDouble. The
434+
// op is emitted naturally (PrimitiveCompare<LessThan> and friends). NaNs are
435+
// handled using Python's rules (`NaN == NaN` is false, `NaN != NaN` is true,
436+
// all other comparison types with NaN are false).
435437
if (left->isA(TFloatExact) && right->isA(TFloatExact) &&
436-
!(op == CompareOp::kIn || op == CompareOp::kNotIn ||
437-
op == CompareOp::kExcMatch)) {
438-
return env.emit<FloatCompare>(instr->op(), left, right);
438+
(op == CompareOp::kLessThan || op == CompareOp::kLessThanEqual ||
439+
op == CompareOp::kGreaterThan || op == CompareOp::kGreaterThanEqual ||
440+
op == CompareOp::kEqual || op == CompareOp::kNotEqual)) {
441+
std::optional<PrimitiveCompareOp> prim_op = toPrimitiveCompareOp(op);
442+
env.emit<UseType>(left, TFloatExact);
443+
env.emit<UseType>(right, TFloatExact);
444+
Register* unboxed_left = env.emit<PrimitiveUnbox>(left, TCDouble);
445+
Register* unboxed_right = env.emit<PrimitiveUnbox>(right, TCDouble);
446+
Register* result =
447+
env.emit<PrimitiveCompare>(*prim_op, unboxed_left, unboxed_right);
448+
return env.emit<PrimitiveBoxBool>(result);
439449
}
440450

441451
// Emit LongCompare if both args are LongExact and the op is supported between

cinderx/Jit/lir/generator.cpp

Lines changed: 120 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2592,6 +2592,126 @@ LIRGenerator::TranslatedBlock LIRGenerator::translateOneBasicBlock(
25922592
}
25932593
case Opcode::kPrimitiveCompare: {
25942594
auto instr = static_cast<const PrimitiveCompare*>(&i);
2595+
// Float comparisons need NaN-correct condition codes, and the choice is
2596+
// architecture-specific because comisd (x86-64) and fcmp (ARM64) expose
2597+
// different flags for unordered (NaN) operands. Pick the LIR opcode
2598+
// (and operand order) so the opcode's standard condition, used both by
2599+
// the standalone setcc/cset and by a compare fused into a branch
2600+
// through compareToBranchCC, yields Python's result: every ordering
2601+
// comparison involving a NaN is false, `NaN == NaN` is False, `Nan !=
2602+
// NaN` is True.
2603+
if (instr->left()->type() <= TCDouble) {
2604+
Register* lhs = instr->left();
2605+
Register* rhs = instr->right();
2606+
#if defined(CINDER_X86_64)
2607+
// comisd sets unsigned-sense flags and reports unordered as CF=1, so
2608+
// only the above / above-equal conditions are false for NaN. Express
2609+
// every ordering as > / >=, swapping operands for < / <=. == / !=
2610+
// have no NaN-correct single-instruction form (comisd folds unordered
2611+
// into ZF), so build them from ordering comparisons:
2612+
// a == b == (a <= b) && (a >= b); a != b == !(a == b)
2613+
// Floats are neither signed nor unsigned, so the signed and unsigned
2614+
// PrimitiveCompareOp variants denote the same ordering (Static Python
2615+
// `double` emits the unsigned variants; the Python-float path emits
2616+
// the natural ones).
2617+
switch (instr->op()) {
2618+
case PrimitiveCompareOp::kGreaterThan:
2619+
case PrimitiveCompareOp::kGreaterThanUnsigned:
2620+
bbb.appendInstr(
2621+
instr->output(), Instruction::kGreaterThanUnsigned, lhs, rhs);
2622+
break;
2623+
case PrimitiveCompareOp::kGreaterThanEqual:
2624+
case PrimitiveCompareOp::kGreaterThanEqualUnsigned:
2625+
bbb.appendInstr(
2626+
instr->output(),
2627+
Instruction::kGreaterThanEqualUnsigned,
2628+
lhs,
2629+
rhs);
2630+
break;
2631+
case PrimitiveCompareOp::kLessThan: // a < b == b > a
2632+
case PrimitiveCompareOp::kLessThanUnsigned:
2633+
bbb.appendInstr(
2634+
instr->output(), Instruction::kGreaterThanUnsigned, rhs, lhs);
2635+
break;
2636+
case PrimitiveCompareOp::kLessThanEqual: // a <= b == b >= a
2637+
case PrimitiveCompareOp::kLessThanEqualUnsigned:
2638+
bbb.appendInstr(
2639+
instr->output(),
2640+
Instruction::kGreaterThanEqualUnsigned,
2641+
rhs,
2642+
lhs);
2643+
break;
2644+
case PrimitiveCompareOp::kEqual:
2645+
case PrimitiveCompareOp::kNotEqual: {
2646+
Instruction* le = bbb.appendInstr(
2647+
OutVReg{Operand::k8bit},
2648+
Instruction::kGreaterThanEqualUnsigned,
2649+
rhs,
2650+
lhs); // a <= b
2651+
Instruction* ge = bbb.appendInstr(
2652+
OutVReg{Operand::k8bit},
2653+
Instruction::kGreaterThanEqualUnsigned,
2654+
lhs,
2655+
rhs); // a >= b
2656+
if (instr->op() == PrimitiveCompareOp::kEqual) {
2657+
bbb.appendInstr(instr->output(), Instruction::kAnd, le, ge);
2658+
} else {
2659+
Instruction* eq = bbb.appendInstr(
2660+
OutVReg{Operand::k8bit}, Instruction::kAnd, le, ge);
2661+
bbb.appendInstr(
2662+
instr->output(),
2663+
Instruction::kXor,
2664+
eq,
2665+
Imm{1, DataType::k8bit});
2666+
}
2667+
break;
2668+
}
2669+
default:
2670+
JIT_ABORT(
2671+
"Not a float comparison {}", static_cast<int>(instr->op()));
2672+
}
2673+
#elif defined(CINDER_AARCH64)
2674+
// fcmp leaves Z=0 for unordered operands and sets C=1, V=1. Pick the
2675+
// condition that is false for NaN on each ordering: GT/GE for > / >=
2676+
// (signed opcodes), LO/LS for < / <= (unsigned opcodes). EQ/NE key
2677+
// off Z alone and are already NaN-correct. Floats are neither signed
2678+
// nor unsigned, so the signed and unsigned PrimitiveCompareOp
2679+
// variants denote the same ordering (Static Python `double` emits the
2680+
// unsigned variants; the Python-float path emits the natural ones).
2681+
Instruction::Opcode op;
2682+
switch (instr->op()) {
2683+
case PrimitiveCompareOp::kEqual:
2684+
op = Instruction::kEqual;
2685+
break;
2686+
case PrimitiveCompareOp::kNotEqual:
2687+
op = Instruction::kNotEqual;
2688+
break;
2689+
case PrimitiveCompareOp::kGreaterThan:
2690+
case PrimitiveCompareOp::kGreaterThanUnsigned:
2691+
op = Instruction::kGreaterThanSigned;
2692+
break;
2693+
case PrimitiveCompareOp::kGreaterThanEqual:
2694+
case PrimitiveCompareOp::kGreaterThanEqualUnsigned:
2695+
op = Instruction::kGreaterThanEqualSigned;
2696+
break;
2697+
case PrimitiveCompareOp::kLessThan:
2698+
case PrimitiveCompareOp::kLessThanUnsigned:
2699+
op = Instruction::kLessThanUnsigned;
2700+
break;
2701+
case PrimitiveCompareOp::kLessThanEqual:
2702+
case PrimitiveCompareOp::kLessThanEqualUnsigned:
2703+
op = Instruction::kLessThanEqualUnsigned;
2704+
break;
2705+
default:
2706+
JIT_ABORT(
2707+
"Not a float comparison {}", static_cast<int>(instr->op()));
2708+
}
2709+
bbb.appendInstr(instr->output(), op, lhs, rhs);
2710+
#else
2711+
CINDER_UNSUPPORTED
2712+
#endif
2713+
break;
2714+
}
25952715
Instruction::Opcode op;
25962716
switch (instr->op()) {
25972717
case PrimitiveCompareOp::kEqual:
@@ -3341,17 +3461,6 @@ LIRGenerator::TranslatedBlock LIRGenerator::translateOneBasicBlock(
33413461
op);
33423462
break;
33433463
}
3344-
case Opcode::kFloatCompare: {
3345-
auto instr = static_cast<const FloatCompare*>(&i);
3346-
3347-
bbb.appendCallInstruction(
3348-
instr->output(),
3349-
PyFloat_Type.tp_richcompare,
3350-
instr->left(),
3351-
instr->right(),
3352-
static_cast<int>(instr->op()));
3353-
break;
3354-
}
33553464
case Opcode::kLongCompare: {
33563465
auto instr = static_cast<const LongCompare*>(&i);
33573466

0 commit comments

Comments
 (0)