commit 33caf3462cb418accd6013428bb83af20ac791ad
parent fbb75c78ef3221c211308b858dd2931f4f41295f
Author: Jan de Mooij <jdemooij@mozilla.com>
Date: Tue, 18 Nov 2025 12:03:05 +0000
Bug 1992990 part 5 - Add MGuardShapeList and use for GuardMultipleShapes with a small number of shapes. r=iain
Snapshot the list of shapes on the main thread and then use this list for `MGuardShapeList`.
Because this MIR op has the list of shapes available at compile-time, we can perform more
MIR optimizations such as GVN. This improves some JetStream 3 subtests such as acorn-wtb a lot.
Differential Revision: https://phabricator.services.mozilla.com/D272481
Diffstat:
10 files changed, 235 insertions(+), 8 deletions(-)
diff --git a/js/src/jit/CodeGenerator.cpp b/js/src/jit/CodeGenerator.cpp
@@ -19,6 +19,7 @@
#include "mozilla/ScopeExit.h"
#include "mozilla/SIMD.h"
+#include <algorithm>
#include <cmath>
#include <limits>
#include <type_traits>
@@ -4500,6 +4501,44 @@ void CodeGenerator::visitGuardMultipleShapes(LGuardMultipleShapes* guard) {
bailoutFrom(&bail, guard->snapshot());
}
+void CodeGenerator::visitGuardShapeList(LGuardShapeList* guard) {
+ Register obj = ToRegister(guard->object());
+ Register temp = ToRegister(guard->temp0());
+ Register spectre = ToTempRegisterOrInvalid(guard->temp1());
+
+ Label done, bail;
+ masm.loadObjShapeUnsafe(obj, temp);
+
+ // Count the number of branches to emit.
+ const auto& shapes = guard->mir()->shapeList()->shapes();
+ size_t branchesLeft = std::count_if(shapes.begin(), shapes.end(),
+ [](Shape* s) { return s != nullptr; });
+ MOZ_RELEASE_ASSERT(branchesLeft > 0);
+
+ for (Shape* shape : shapes) {
+ if (!shape) {
+ continue;
+ }
+ if (branchesLeft > 1) {
+ masm.branchPtr(Assembler::Equal, temp, ImmGCPtr(shape), &done);
+ if (spectre != InvalidReg) {
+ masm.spectreMovePtr(Assembler::Equal, spectre, obj);
+ }
+ } else {
+ // This is the last branch so invert the condition and jump to |bail|.
+ masm.branchPtr(Assembler::NotEqual, temp, ImmGCPtr(shape), &bail);
+ if (spectre != InvalidReg) {
+ masm.spectreMovePtr(Assembler::NotEqual, spectre, obj);
+ }
+ }
+ branchesLeft--;
+ }
+ MOZ_ASSERT(branchesLeft == 0);
+
+ masm.bind(&done);
+ bailoutFrom(&bail, guard->snapshot());
+}
+
void CodeGenerator::visitGuardProto(LGuardProto* guard) {
Register obj = ToRegister(guard->object());
Register expected = ToRegister(guard->expected());
diff --git a/js/src/jit/Lowering.cpp b/js/src/jit/Lowering.cpp
@@ -5789,6 +5789,23 @@ void LIRGenerator::visitGuardMultipleShapes(MGuardMultipleShapes* ins) {
}
}
+void LIRGenerator::visitGuardShapeList(MGuardShapeList* ins) {
+ MOZ_ASSERT(ins->object()->type() == MIRType::Object);
+
+ if (JitOptions.spectreObjectMitigations) {
+ auto* lir = new (alloc())
+ LGuardShapeList(useRegisterAtStart(ins->object()), temp(), temp());
+ assignSnapshot(lir, ins->bailoutKind());
+ defineReuseInput(lir, ins, 0);
+ } else {
+ auto* lir = new (alloc()) LGuardShapeList(useRegister(ins->object()),
+ temp(), LDefinition::BogusTemp());
+ assignSnapshot(lir, ins->bailoutKind());
+ add(lir, ins);
+ redefine(ins, ins->object());
+ }
+}
+
void LIRGenerator::visitGuardProto(MGuardProto* ins) {
MOZ_ASSERT(ins->object()->type() == MIRType::Object);
MOZ_ASSERT(ins->expected()->type() == MIRType::Object);
diff --git a/js/src/jit/MIR.cpp b/js/src/jit/MIR.cpp
@@ -11,6 +11,7 @@
#include "mozilla/MathAlgorithms.h"
#include "mozilla/Maybe.h"
+#include <algorithm>
#include <array>
#include <utility>
@@ -27,6 +28,7 @@
#include "jit/RangeAnalysis.h"
#include "jit/VMFunctions.h"
#include "jit/WarpBuilderShared.h"
+#include "jit/WarpSnapshot.h"
#include "js/Conversions.h"
#include "js/experimental/JitInfo.h" // JSJitInfo, JSTypedMethodJitInfo
#include "js/ScalarType.h" // js::Scalar::Type
@@ -560,6 +562,14 @@ const MDefinition* MDefinition::skipObjectGuards() const {
result = result->toGuardShape()->object();
continue;
}
+ if (result->isGuardShapeList()) {
+ result = result->toGuardShapeList()->object();
+ continue;
+ }
+ if (result->isGuardMultipleShapes()) {
+ result = result->toGuardMultipleShapes()->object();
+ continue;
+ }
if (result->isGuardNullProto()) {
result = result->toGuardNullProto()->object();
continue;
@@ -7184,6 +7194,36 @@ AliasSet MGuardShape::getAliasSet() const {
return AliasSet::Load(AliasSet::ObjectFields);
}
+bool MGuardShapeList::congruentTo(const MDefinition* ins) const {
+ if (!congruentIfOperandsEqual(ins)) {
+ return false;
+ }
+
+ // Returns true iff all non-nullptr shapes in |a| are also in |b|.
+ auto hasAllShapes = [](const auto& a, const auto& b) {
+ for (Shape* shape : a) {
+ if (!shape) {
+ continue;
+ }
+ auto isSameShape = [shape](Shape* other) { return shape == other; };
+ if (!std::any_of(b.begin(), b.end(), isSameShape)) {
+ return false;
+ }
+ }
+ return true;
+ };
+
+ // Return true if all shapes in |shapesA| are also in |shapesB| and vice
+ // versa.
+ const auto& shapesA = this->shapeList()->shapes();
+ const auto& shapesB = ins->toGuardShapeList()->shapeList()->shapes();
+ return hasAllShapes(shapesA, shapesB) && hasAllShapes(shapesB, shapesA);
+}
+
+AliasSet MGuardShapeList::getAliasSet() const {
+ return AliasSet::Load(AliasSet::ObjectFields);
+}
+
bool MHasShape::congruentTo(const MDefinition* ins) const {
if (!ins->isHasShape()) {
return false;
diff --git a/js/src/jit/MIR.h b/js/src/jit/MIR.h
@@ -74,6 +74,7 @@ bool CurrentThreadIsIonCompiling();
namespace jit {
class CallInfo;
+class ShapeListSnapshot;
#ifdef JS_JITSPEW
// Helper for debug printing. Avoids creating a MIR.h <--> MIRGraph.h cycle.
diff --git a/js/src/jit/MIROps.yaml b/js/src/jit/MIROps.yaml
@@ -2203,6 +2203,9 @@
alias_set: custom
generate_lir: true
+# This is the equivalent of the GuardMultipleShapes CacheIR op. Because it
+# stores a pointer to the ShapeListObject, we don't need to recompile Ion code
+# when a new shape is added to the list.
- name: GuardMultipleShapes
operands:
object: Object
@@ -2215,6 +2218,23 @@
generate_lir: true
lir_temps: 4
+# Similar to GuardMultipleShapes but used when the list had a small number of
+# shapes. These shapes have been copied to the ShapeListSnapshot and are
+# available at compile-time. This lets us perform more MIR optimizations, but
+# we need to recompile when a new shape is added to the list.
+- name: GuardShapeList
+ operands:
+ object: Object
+ arguments:
+ shapeList: const ShapeListSnapshot*
+ result_type: Object
+ guard: true
+ movable: true
+ congruent_to: custom
+ alias_set: custom
+ generate_lir: true
+ lir_temps: 2
+
- name: GuardProto
gen_boilerplate: false
diff --git a/js/src/jit/WarpBuilder.cpp b/js/src/jit/WarpBuilder.cpp
@@ -3380,7 +3380,11 @@ bool WarpBuilder::buildIC(BytecodeLocation loc, CacheKind kind,
mozilla::DebugOnly<size_t> numInputs = inputs.size();
MOZ_ASSERT(numInputs == NumInputsForCacheKind(kind));
- if (auto* cacheIRSnapshot = getOpSnapshot<WarpCacheIR>(loc)) {
+ const WarpCacheIRBase* cacheIRSnapshot = getOpSnapshot<WarpCacheIR>(loc);
+ if (!cacheIRSnapshot) {
+ cacheIRSnapshot = getOpSnapshot<WarpCacheIRWithShapeList>(loc);
+ }
+ if (cacheIRSnapshot) {
return TranspileCacheIRToMIR(this, loc, cacheIRSnapshot, inputs);
}
diff --git a/js/src/jit/WarpCacheIRTranspiler.cpp b/js/src/jit/WarpCacheIRTranspiler.cpp
@@ -512,11 +512,19 @@ bool WarpCacheIRTranspiler::emitGuardObjectFuseProperty(
bool WarpCacheIRTranspiler::emitGuardMultipleShapes(ObjOperandId objId,
uint32_t shapesOffset) {
MDefinition* def = getOperand(objId);
- MInstruction* shapeList = objectStubField(shapesOffset);
- auto* ins = MGuardMultipleShapes::New(alloc(), def, shapeList);
- if (builder_->info().inlineScriptTree()->hasSharedICScript()) {
- ins->setBailoutKind(BailoutKind::MonomorphicInlinedStubFolding);
+ // Use MGuardShapeList if we snapshotted the list of shapes on the main
+ // thread.
+ MInstruction* ins;
+ if (cacheIRSnapshot_->is<WarpCacheIRWithShapeList>()) {
+ auto* shapes = cacheIRSnapshot_->as<WarpCacheIRWithShapeList>()->shapes();
+ ins = MGuardShapeList::New(alloc(), def, shapes);
+ } else {
+ MInstruction* shapeList = objectStubField(shapesOffset);
+ ins = MGuardMultipleShapes::New(alloc(), def, shapeList);
+ if (builder_->info().inlineScriptTree()->hasSharedICScript()) {
+ ins->setBailoutKind(BailoutKind::MonomorphicInlinedStubFolding);
+ }
}
add(ins);
diff --git a/js/src/jit/WarpOracle.cpp b/js/src/jit/WarpOracle.cpp
@@ -22,6 +22,7 @@
#include "jit/JitSpewer.h"
#include "jit/JitZone.h"
#include "jit/MIRGenerator.h"
+#include "jit/ShapeList.h"
#include "jit/TrialInlining.h"
#include "jit/TypeData.h"
#include "jit/WarpBuilder.h"
@@ -1103,6 +1104,9 @@ AbortReasonOr<Ok> WarpScriptOracle::maybeInlineIC(WarpOpSnapshotList& snapshots,
const CacheIRStubInfo* stubInfo = stub->stubInfo();
const uint8_t* stubData = stub->stubDataStart();
+ // List of shapes for a GuardMultipleShapes op with a small number of shapes.
+ mozilla::Maybe<ShapeListSnapshot> shapeList;
+
// Only create a snapshot if all opcodes are supported by the transpiler.
CacheIRReader reader(stubInfo);
bool hasInvalidFuseGuard = false;
@@ -1204,6 +1208,21 @@ AbortReasonOr<Ok> WarpScriptOracle::maybeInlineIC(WarpOpSnapshotList& snapshots,
}
break;
}
+ case CacheOp::GuardMultipleShapes: {
+ auto args = reader.argsForGuardMultipleShapes();
+ JSObject* shapes = stubInfo->getStubField<StubField::Type::JSObject>(
+ stub, args.shapesOffset);
+ auto* shapesObject = &shapes->as<ShapeListObject>();
+ MOZ_ASSERT(shapeList.isNothing());
+ size_t numShapes = shapesObject->length();
+ if (ShapeListSnapshot::shouldSnapshot(numShapes)) {
+ shapeList.emplace();
+ for (size_t i = 0; i < numShapes; i++) {
+ shapeList->init(i, shapesObject->get(i));
+ }
+ }
+ break;
+ }
default:
reader.skip(opInfo.argLength);
break;
@@ -1256,9 +1275,17 @@ AbortReasonOr<Ok> WarpScriptOracle::maybeInlineIC(WarpOpSnapshotList& snapshots,
}
}
- if (!AddOpSnapshot<WarpCacheIR>(alloc_, snapshots, offset, jitCode, stubInfo,
- stubDataCopy)) {
- return abort(AbortReason::Alloc);
+ if (shapeList.isSome()) {
+ if (!AddOpSnapshot<WarpCacheIRWithShapeList>(alloc_, snapshots, offset,
+ jitCode, stubInfo,
+ stubDataCopy, *shapeList)) {
+ return abort(AbortReason::Alloc);
+ }
+ } else {
+ if (!AddOpSnapshot<WarpCacheIR>(alloc_, snapshots, offset, jitCode,
+ stubInfo, stubDataCopy)) {
+ return abort(AbortReason::Alloc);
+ }
}
fallbackStub->setUsedByTranspiler();
diff --git a/js/src/jit/WarpSnapshot.cpp b/js/src/jit/WarpSnapshot.cpp
@@ -207,6 +207,15 @@ void WarpCacheIR::dumpData(GenericPrinter& out) const {
WarpCacheIRBase::dumpData(out);
}
+void WarpCacheIRWithShapeList::dumpData(GenericPrinter& out) const {
+ WarpCacheIRBase::dumpData(out);
+ uint32_t index = 0;
+ for (Shape* shape : shapes_.shapes()) {
+ out.printf(" shape %u: 0x%p\n", index, shape);
+ index++;
+ }
+}
+
void WarpInlinedCall::dumpData(GenericPrinter& out) const {
out.printf(" scriptSnapshot: 0x%p\n", scriptSnapshot_);
out.printf(" info: 0x%p\n", info_);
@@ -431,6 +440,19 @@ void WarpCacheIRBase::traceData(JSTracer* trc) {
void WarpCacheIR::traceData(JSTracer* trc) { WarpCacheIRBase::traceData(trc); }
+void ShapeListSnapshot::trace(JSTracer* trc) const {
+ for (auto& shape : shapes_) {
+ if (shape) {
+ TraceOffthreadGCPtr(trc, shape, "warp-shape-list-shape");
+ }
+ }
+}
+
+void WarpCacheIRWithShapeList::traceData(JSTracer* trc) {
+ WarpCacheIRBase::traceData(trc);
+ shapes_.trace(trc);
+}
+
void WarpInlinedCall::traceData(JSTracer* trc) {
// Note: scriptSnapshot_ is traced through WarpSnapshot.
cacheIRSnapshot_->trace(trc);
diff --git a/js/src/jit/WarpSnapshot.h b/js/src/jit/WarpSnapshot.h
@@ -47,6 +47,7 @@ class WarpScriptSnapshot;
_(WarpClassBodyEnvironment) \
_(WarpBailout) \
_(WarpCacheIR) \
+ _(WarpCacheIRWithShapeList) \
_(WarpInlinedCall) \
_(WarpPolymorphicTypes)
@@ -262,6 +263,54 @@ class WarpCacheIR : public WarpCacheIRBase {
#endif
};
+class ShapeListSnapshot {
+ public:
+ ShapeListSnapshot() = default;
+
+ void init(size_t index, Shape* shape) {
+ MOZ_ASSERT(shape);
+ shapes_[index].init(shape);
+ }
+ const auto& shapes() const { return shapes_; }
+
+ static bool shouldSnapshot(size_t length) {
+ // ShapeListObject has weak pointers so a GC can remove shapes from the
+ // list. Don't snapshot empty shape lists to avoid bailouts and because
+ // handling this edge case would complicate the compiler code.
+ return length > 0 && length <= NumShapes;
+ }
+
+ void trace(JSTracer* trc) const;
+
+ private:
+ static constexpr size_t NumShapes = 4;
+ mozilla::Array<OffthreadGCPtr<Shape*>, NumShapes> shapes_{};
+};
+
+// Like WarpCacheIR, but also includes a ShapeListSnapshot for the
+// GuardMultipleShapes CacheIR op.
+class WarpCacheIRWithShapeList : public WarpCacheIRBase {
+ const ShapeListSnapshot shapes_;
+
+ public:
+ static constexpr Kind ThisKind = Kind::WarpCacheIRWithShapeList;
+
+ WarpCacheIRWithShapeList(uint32_t offset, JitCode* stubCode,
+ const CacheIRStubInfo* stubInfo,
+ const uint8_t* stubData,
+ const ShapeListSnapshot& shapes)
+ : WarpCacheIRBase(ThisKind, offset, stubCode, stubInfo, stubData),
+ shapes_(shapes) {}
+
+ void traceData(JSTracer* trc);
+
+#ifdef JS_JITSPEW
+ void dumpData(GenericPrinter& out) const;
+#endif
+
+ const ShapeListSnapshot* shapes() const { return &shapes_; }
+};
+
// [SMDOC] Warp Nursery Object/Value support
//
// CacheIR stub data can contain nursery allocated objects or values. This can