commit ac7d7c131dd65058ad6b7ecfa65dc8f31a0f203e
parent d4456d810664a2a13a871a68e5323522a78c1131
Author: Sandor Molnar <smolnar@mozilla.com>
Date: Mon, 8 Dec 2025 11:42:55 +0200
Revert "Bug 998485 - Removed analyzeEdgeCasesForward from MIR.h/.cpp - r=nbp" for causing perf regression (Bug 2003121)
This reverts commit b0c596352f76e501680fc2fbe4419e701e488ece.
Diffstat:
10 files changed, 387 insertions(+), 0 deletions(-)
diff --git a/js/src/jit/EdgeCaseAnalysis.cpp b/js/src/jit/EdgeCaseAnalysis.cpp
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set ts=8 sts=2 et sw=2 tw=80:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "jit/EdgeCaseAnalysis.h"
+
+#include "jit/MIR-wasm.h"
+#include "jit/MIR.h"
+#include "jit/MIRGenerator.h"
+#include "jit/MIRGraph.h"
+
+using namespace js;
+using namespace js::jit;
+
+EdgeCaseAnalysis::EdgeCaseAnalysis(const MIRGenerator* mir, MIRGraph& graph)
+ : mir(mir), graph(graph) {}
+
+bool EdgeCaseAnalysis::analyzeLate() {
+ // Renumber definitions for NeedNegativeZeroCheck under
+ // analyzeEdgeCasesBackward.
+ uint32_t nextId = 0;
+
+ for (ReversePostorderIterator block(graph.rpoBegin());
+ block != graph.rpoEnd(); block++) {
+ for (MDefinitionIterator iter(*block); iter; iter++) {
+ if (mir->shouldCancel("Analyze Late (first loop)")) {
+ return false;
+ }
+
+ iter->setId(nextId++);
+ iter->analyzeEdgeCasesForward();
+ }
+ block->lastIns()->setId(nextId++);
+ }
+
+ for (PostorderIterator block(graph.poBegin()); block != graph.poEnd();
+ block++) {
+ for (MInstructionReverseIterator riter(block->rbegin());
+ riter != block->rend(); riter++) {
+ if (mir->shouldCancel("Analyze Late (second loop)")) {
+ return false;
+ }
+
+ riter->analyzeEdgeCasesBackward();
+ }
+ }
+
+ return true;
+}
diff --git a/js/src/jit/EdgeCaseAnalysis.h b/js/src/jit/EdgeCaseAnalysis.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set ts=8 sts=2 et sw=2 tw=80:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef jit_EdgeCaseAnalysis_h
+#define jit_EdgeCaseAnalysis_h
+
+namespace js {
+namespace jit {
+
+class MIRGenerator;
+class MIRGraph;
+
+class EdgeCaseAnalysis {
+ const MIRGenerator* mir;
+ MIRGraph& graph;
+
+ public:
+ EdgeCaseAnalysis(const MIRGenerator* mir, MIRGraph& graph);
+ [[nodiscard]] bool analyzeLate();
+};
+
+} // namespace jit
+} // namespace js
+
+#endif /* jit_EdgeCaseAnalysis_h */
diff --git a/js/src/jit/Ion.cpp b/js/src/jit/Ion.cpp
@@ -23,6 +23,7 @@
#include "jit/CodeGenerator.h"
#include "jit/CompileInfo.h"
#include "jit/DominatorTree.h"
+#include "jit/EdgeCaseAnalysis.h"
#include "jit/EffectiveAddressAnalysis.h"
#include "jit/ExecutableAllocator.h"
#include "jit/FoldLinearArithConstants.h"
@@ -1491,6 +1492,22 @@ bool OptimizeMIR(MIRGenerator* mir) {
}
}
+ // Passes after this point must not move instructions; these analyses
+ // depend on knowing the final order in which instructions will execute.
+
+ if (mir->optimizationInfo().edgeCaseAnalysisEnabled()) {
+ EdgeCaseAnalysis edgeCaseAnalysis(mir, graph);
+ if (!edgeCaseAnalysis.analyzeLate()) {
+ return false;
+ }
+ mir->spewPass("Edge Case Analysis (Late)");
+ AssertGraphCoherency(graph);
+
+ if (mir->shouldCancel("Edge Case Analysis (Late)")) {
+ return false;
+ }
+ }
+
if (mir->optimizationInfo().eliminateRedundantChecksEnabled()) {
// Note: check elimination has to run after all other passes that move
// instructions. Since check uses are replaced with the actual index,
diff --git a/js/src/jit/IonOptimizationLevels.h b/js/src/jit/IonOptimizationLevels.h
@@ -44,6 +44,9 @@ class OptimizationInfo {
// Toggles whether Alignment Mask Analysis is performed.
bool ama_;
+ // Toggles whether Edge Case Analysis is used.
+ bool edgeCaseAnalysis_;
+
// Toggles whether redundant checks get removed.
bool eliminateRedundantChecks_;
@@ -88,6 +91,7 @@ class OptimizationInfo {
: level_(OptimizationLevel::Normal),
eaa_(false),
ama_(false),
+ edgeCaseAnalysis_(false),
eliminateRedundantChecks_(false),
eliminateRedundantShapeGuards_(false),
eliminateRedundantGCBarriers_(false),
@@ -107,6 +111,7 @@ class OptimizationInfo {
autoTruncate_ = true;
eaa_ = true;
+ edgeCaseAnalysis_ = true;
eliminateRedundantChecks_ = true;
eliminateRedundantShapeGuards_ = true;
eliminateRedundantGCBarriers_ = true;
@@ -132,6 +137,7 @@ class OptimizationInfo {
ama_ = true;
autoTruncate_ = false;
+ edgeCaseAnalysis_ = false;
eliminateRedundantChecks_ = false;
eliminateRedundantShapeGuards_ = false;
eliminateRedundantGCBarriers_ = false;
@@ -175,6 +181,10 @@ class OptimizationInfo {
bool amaEnabled() const { return ama_ && !JitOptions.disableAma; }
+ bool edgeCaseAnalysisEnabled() const {
+ return edgeCaseAnalysis_ && !JitOptions.disableEdgeCaseAnalysis;
+ }
+
bool eliminateRedundantChecksEnabled() const {
return eliminateRedundantChecks_;
}
diff --git a/js/src/jit/JitOptions.cpp b/js/src/jit/JitOptions.cpp
@@ -81,6 +81,9 @@ DefaultJitOptions::DefaultJitOptions() {
// Toggles whether Effective Address Analysis is globally disabled.
SET_DEFAULT(disableEaa, false);
+ // Toggles whether Edge Case Analysis is gobally disabled.
+ SET_DEFAULT(disableEdgeCaseAnalysis, false);
+
// Toggle whether global value numbering is globally disabled.
SET_DEFAULT(disableGvn, false);
diff --git a/js/src/jit/JitOptions.h b/js/src/jit/JitOptions.h
@@ -54,6 +54,7 @@ struct DefaultJitOptions {
bool disableJitHints;
bool disableAma;
bool disableEaa;
+ bool disableEdgeCaseAnalysis;
bool disableGvn;
bool disableInlining;
bool disableLicm;
diff --git a/js/src/jit/MIR.cpp b/js/src/jit/MIR.cpp
@@ -662,6 +662,10 @@ MDefinition* MInstruction::foldsToStore(TempAllocator& alloc) {
return value;
}
+void MDefinition::analyzeEdgeCasesForward() {}
+
+void MDefinition::analyzeEdgeCasesBackward() {}
+
void MInstruction::setResumePoint(MResumePoint* resumePoint) {
MOZ_ASSERT(!resumePoint_);
resumePoint_ = resumePoint;
@@ -2850,6 +2854,159 @@ MDefinition* MBinaryBitwiseInstruction::foldUnnecessaryBitop() {
return this;
}
+static inline bool CanProduceNegativeZero(MDefinition* def) {
+ // Test if this instruction can produce negative zero even when bailing out
+ // and changing types.
+ switch (def->op()) {
+ case MDefinition::Opcode::Constant:
+ if (def->type() == MIRType::Double &&
+ def->toConstant()->toDouble() == -0.0) {
+ return true;
+ }
+ [[fallthrough]];
+ case MDefinition::Opcode::BitAnd:
+ case MDefinition::Opcode::BitOr:
+ case MDefinition::Opcode::BitXor:
+ case MDefinition::Opcode::BitNot:
+ case MDefinition::Opcode::Lsh:
+ case MDefinition::Opcode::Rsh:
+ return false;
+ default:
+ return true;
+ }
+}
+
+static inline bool NeedNegativeZeroCheck(MDefinition* def) {
+ if (def->isGuard() || def->isGuardRangeBailouts()) {
+ return true;
+ }
+
+ // Test if all uses have the same semantics for -0 and 0
+ for (MUseIterator use = def->usesBegin(); use != def->usesEnd(); use++) {
+ if (use->consumer()->isResumePoint()) {
+ return true;
+ }
+
+ MDefinition* use_def = use->consumer()->toDefinition();
+ switch (use_def->op()) {
+ case MDefinition::Opcode::Add: {
+ // If add is truncating -0 and 0 are observed as the same.
+ if (use_def->toAdd()->isTruncated()) {
+ break;
+ }
+
+ // x + y gives -0, when both x and y are -0
+
+ // Figure out the order in which the addition's operands will
+ // execute. EdgeCaseAnalysis::analyzeLate has renumbered the MIR
+ // definitions for us so that this just requires comparing ids.
+ MDefinition* first = use_def->toAdd()->lhs();
+ MDefinition* second = use_def->toAdd()->rhs();
+ if (first->id() > second->id()) {
+ std::swap(first, second);
+ }
+ // Negative zero checks can be removed on the first executed
+ // operand only if it is guaranteed the second executed operand
+ // will produce a value other than -0. While the second is
+ // typed as an int32, a bailout taken between execution of the
+ // operands may change that type and cause a -0 to flow to the
+ // second.
+ //
+ // There is no way to test whether there are any bailouts
+ // between execution of the operands, so remove negative
+ // zero checks from the first only if the second's type is
+ // independent from type changes that may occur after bailing.
+ if (def == first && CanProduceNegativeZero(second)) {
+ return true;
+ }
+
+ // The negative zero check can always be removed on the second
+ // executed operand; by the time this executes the first will have
+ // been evaluated as int32 and the addition's result cannot be -0.
+ break;
+ }
+ case MDefinition::Opcode::Sub: {
+ // If sub is truncating -0 and 0 are observed as the same
+ if (use_def->toSub()->isTruncated()) {
+ break;
+ }
+
+ // x + y gives -0, when x is -0 and y is 0
+
+ // We can remove the negative zero check on the rhs, only if we
+ // are sure the lhs isn't negative zero.
+
+ // The lhs is typed as integer (i.e. not -0.0), but it can bailout
+ // and change type. This should be fine if the lhs is executed
+ // first. However if the rhs is executed first, the lhs can bail,
+ // change type and become -0.0 while the rhs has already been
+ // optimized to not make a difference between zero and negative zero.
+ MDefinition* lhs = use_def->toSub()->lhs();
+ MDefinition* rhs = use_def->toSub()->rhs();
+ if (rhs->id() < lhs->id() && CanProduceNegativeZero(lhs)) {
+ return true;
+ }
+
+ [[fallthrough]];
+ }
+ case MDefinition::Opcode::StoreElement:
+ case MDefinition::Opcode::StoreHoleValueElement:
+ case MDefinition::Opcode::LoadElement:
+ case MDefinition::Opcode::LoadElementHole:
+ case MDefinition::Opcode::LoadUnboxedScalar:
+ case MDefinition::Opcode::LoadDataViewElement:
+ case MDefinition::Opcode::LoadTypedArrayElementHole:
+ case MDefinition::Opcode::CharCodeAt:
+ case MDefinition::Opcode::Mod:
+ case MDefinition::Opcode::InArray:
+ // Only allowed to remove check when definition is the second operand
+ if (use_def->getOperand(0) == def) {
+ return true;
+ }
+ for (size_t i = 2, e = use_def->numOperands(); i < e; i++) {
+ if (use_def->getOperand(i) == def) {
+ return true;
+ }
+ }
+ break;
+ case MDefinition::Opcode::BoundsCheck:
+ // Only allowed to remove check when definition is the first operand
+ if (use_def->toBoundsCheck()->getOperand(1) == def) {
+ return true;
+ }
+ break;
+ case MDefinition::Opcode::ToString:
+ case MDefinition::Opcode::FromCharCode:
+ case MDefinition::Opcode::FromCodePoint:
+ case MDefinition::Opcode::TableSwitch:
+ case MDefinition::Opcode::Compare:
+ case MDefinition::Opcode::BitAnd:
+ case MDefinition::Opcode::BitOr:
+ case MDefinition::Opcode::BitXor:
+ case MDefinition::Opcode::Abs:
+ case MDefinition::Opcode::TruncateToInt32:
+ // Always allowed to remove check. No matter which operand.
+ break;
+ case MDefinition::Opcode::StoreElementHole:
+ case MDefinition::Opcode::StoreTypedArrayElementHole:
+ case MDefinition::Opcode::PostWriteElementBarrier:
+ // Only allowed to remove check when definition is the third operand.
+ for (size_t i = 0, e = use_def->numOperands(); i < e; i++) {
+ if (i == 2) {
+ continue;
+ }
+ if (use_def->getOperand(i) == def) {
+ return true;
+ }
+ }
+ break;
+ default:
+ return true;
+ }
+ }
+ return false;
+}
+
#ifdef JS_JITSPEW
void MBinaryArithInstruction::printOpcode(GenericPrinter& out) const {
MDefinition::printOpcode(out);
@@ -3780,6 +3937,54 @@ MDefinition* MDiv::foldsTo(TempAllocator& alloc) {
return this;
}
+void MDiv::analyzeEdgeCasesForward() {
+ // This is only meaningful when doing integer division.
+ if (type() != MIRType::Int32) {
+ return;
+ }
+
+ MOZ_ASSERT(lhs()->type() == MIRType::Int32);
+ MOZ_ASSERT(rhs()->type() == MIRType::Int32);
+
+ // Try removing divide by zero check
+ if (rhs()->isConstant() && !rhs()->toConstant()->isInt32(0)) {
+ canBeDivideByZero_ = false;
+ }
+
+ // If lhs is a constant int != INT32_MIN, then
+ // negative overflow check can be skipped.
+ if (lhs()->isConstant() && !lhs()->toConstant()->isInt32(INT32_MIN)) {
+ canBeNegativeOverflow_ = false;
+ }
+
+ // If rhs is a constant int != -1, likewise.
+ if (rhs()->isConstant() && !rhs()->toConstant()->isInt32(-1)) {
+ canBeNegativeOverflow_ = false;
+ }
+
+ // If lhs is != 0, then negative zero check can be skipped.
+ if (lhs()->isConstant() && !lhs()->toConstant()->isInt32(0)) {
+ setCanBeNegativeZero(false);
+ }
+
+ // If rhs is >= 0, likewise.
+ if (rhs()->isConstant() && rhs()->type() == MIRType::Int32) {
+ if (rhs()->toConstant()->toInt32() >= 0) {
+ setCanBeNegativeZero(false);
+ }
+ }
+}
+
+void MDiv::analyzeEdgeCasesBackward() {
+ // In general, canBeNegativeZero_ is only valid for integer divides.
+ // It's fine to access here because we're only using it to avoid
+ // wasting effort to decide whether we can clear an already cleared
+ // flag.
+ if (canBeNegativeZero_ && !NeedNegativeZeroCheck(this)) {
+ setCanBeNegativeZero(false);
+ }
+}
+
bool MDiv::fallible() const { return !isTruncated(); }
MDefinition* MMod::foldsTo(TempAllocator& alloc) {
@@ -3798,6 +4003,24 @@ MDefinition* MMod::foldsTo(TempAllocator& alloc) {
return this;
}
+void MMod::analyzeEdgeCasesForward() {
+ // These optimizations make sense only for integer division
+ if (type() != MIRType::Int32) {
+ return;
+ }
+
+ if (rhs()->isConstant() && !rhs()->toConstant()->isInt32(0)) {
+ canBeDivideByZero_ = false;
+ }
+
+ if (rhs()->isConstant()) {
+ int32_t n = rhs()->toConstant()->toInt32();
+ if (n > 0 && !IsPowerOfTwo(uint32_t(n))) {
+ canBePowerOfTwoDivisor_ = false;
+ }
+ }
+}
+
bool MMod::fallible() const {
return !isTruncated() &&
(isUnsigned() || canBeDivideByZero() || canBeNegativeDividend());
@@ -3903,6 +4126,34 @@ MDefinition* MMul::foldsTo(TempAllocator& alloc) {
return this;
}
+void MMul::analyzeEdgeCasesForward() {
+ // Try to remove the check for negative zero
+ // This only makes sense when using the integer multiplication
+ if (type() != MIRType::Int32) {
+ return;
+ }
+
+ // If lhs is > 0, no need for negative zero check.
+ if (lhs()->isConstant() && lhs()->type() == MIRType::Int32) {
+ if (lhs()->toConstant()->toInt32() > 0) {
+ setCanBeNegativeZero(false);
+ }
+ }
+
+ // If rhs is > 0, likewise.
+ if (rhs()->isConstant() && rhs()->type() == MIRType::Int32) {
+ if (rhs()->toConstant()->toInt32() > 0) {
+ setCanBeNegativeZero(false);
+ }
+ }
+}
+
+void MMul::analyzeEdgeCasesBackward() {
+ if (canBeNegativeZero() && !NeedNegativeZeroCheck(this)) {
+ setCanBeNegativeZero(false);
+ }
+}
+
bool MMul::canOverflow() const {
if (isTruncated()) {
return false;
@@ -4448,6 +4699,12 @@ MDefinition* MBooleanToInt32::foldsTo(TempAllocator& alloc) {
return this;
}
+void MToNumberInt32::analyzeEdgeCasesBackward() {
+ if (!NeedNegativeZeroCheck(this)) {
+ setNeedsNegativeZeroCheck(false);
+ }
+}
+
MDefinition* MTruncateToInt32::foldsTo(TempAllocator& alloc) {
MDefinition* input = getOperand(0);
if (input->isBox()) {
diff --git a/js/src/jit/MIR.h b/js/src/jit/MIR.h
@@ -678,6 +678,8 @@ class MDefinition : public MNode {
bool congruentIfOperandsEqual(const MDefinition* ins) const;
virtual MDefinition* foldsTo(TempAllocator& alloc);
+ virtual void analyzeEdgeCasesForward();
+ virtual void analyzeEdgeCasesBackward();
// |canTruncate| reports if this instruction supports truncation. If
// |canTruncate| function returns true, then the |truncate| function is
@@ -3824,6 +3826,7 @@ class MToNumberInt32 : public MUnaryInstruction, public ToInt32Policy::Data {
MDefinition* foldsTo(TempAllocator& alloc) override;
// this only has backwards information flow.
+ void analyzeEdgeCasesBackward() override;
bool needsNegativeZeroCheck() const { return needsNegativeZeroCheck_; }
void setNeedsNegativeZeroCheck(bool needsCheck) {
@@ -5212,6 +5215,8 @@ class MMul : public MBinaryArithInstruction {
}
MDefinition* foldsTo(TempAllocator& alloc) override;
+ void analyzeEdgeCasesForward() override;
+ void analyzeEdgeCasesBackward() override;
void collectRangeInfoPreTrunc() override;
double getIdentity() const override { return 1; }
@@ -5309,6 +5314,8 @@ class MDiv : public MBinaryArithInstruction {
}
MDefinition* foldsTo(TempAllocator& alloc) override;
+ void analyzeEdgeCasesForward() override;
+ void analyzeEdgeCasesBackward() override;
double getIdentity() const override { MOZ_CRASH("not used"); }
@@ -5445,6 +5452,8 @@ class MMod : public MBinaryArithInstruction {
return canBePowerOfTwoDivisor_;
}
+ void analyzeEdgeCasesForward() override;
+
bool isUnsigned() const { return unsigned_; }
bool trapOnError() const { return trapOnError_; }
diff --git a/js/src/jit/moz.build b/js/src/jit/moz.build
@@ -38,6 +38,7 @@ UNIFIED_SOURCES += [
"CompileWrappers.cpp",
"Disassemble.cpp",
"DominatorTree.cpp",
+ "EdgeCaseAnalysis.cpp",
"EffectiveAddressAnalysis.cpp",
"ExecutableAllocator.cpp",
"FlushICache.cpp",
diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp
@@ -13895,6 +13895,16 @@ bool SetContextJITOptions(JSContext* cx, const OptionParser& op) {
}
}
+ if (const char* str = op.getStringOption("ion-edgecase-analysis")) {
+ if (strcmp(str, "on") == 0) {
+ jit::JitOptions.disableEdgeCaseAnalysis = false;
+ } else if (strcmp(str, "off") == 0) {
+ jit::JitOptions.disableEdgeCaseAnalysis = true;
+ } else {
+ return OptionFailure("ion-edgecase-analysis", str);
+ }
+ }
+
if (const char* str = op.getStringOption("ion-pruning")) {
if (strcmp(str, "on") == 0) {
jit::JitOptions.disablePruning = false;