Bug 1176451 - Optimize Array.concat when used on different types of boxed vs. unboxed arrays, r=jandem.
authorBrian Hackett <bhackett1024@gmail.com>
Mon, 03 Aug 2015 14:32:40 -0700
changeset 287645 fdf5862a8c00f1711f4cae23cfd12ba9bcdfb237
parent 287644 d69d9a16becb8e4aca2dc0d20c66bc180137a45a
child 287646 c2b62aef6a4421753e42b9856a55579c62b98921
push id5067
push userraliiev@mozilla.com
push dateMon, 21 Sep 2015 14:04:52 +0000
treeherdermozilla-beta@14221ffe5b2f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjandem
bugs1176451
milestone42.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1176451 - Optimize Array.concat when used on different types of boxed vs. unboxed arrays, r=jandem.
js/src/jit/BaselineIC.cpp
js/src/jit/CodeGenerator.cpp
js/src/jit/MCallOptimize.cpp
js/src/jit/MIR.h
js/src/jsarray.cpp
js/src/vm/UnboxedObject-inl.h
js/src/vm/UnboxedObject.cpp
--- a/js/src/jit/BaselineIC.cpp
+++ b/js/src/jit/BaselineIC.cpp
@@ -9536,54 +9536,74 @@ TryAttachFunCallStub(JSContext* cx, ICCa
         return true;
     }
 
     return true;
 }
 
 static bool
 GetTemplateObjectForNative(JSContext* cx, Native native, const CallArgs& args,
-                           MutableHandleObject res)
+                           MutableHandleObject res, bool* skipAttach)
 {
     // Check for natives to which template objects can be attached. This is
     // done to provide templates to Ion for inlining these natives later on.
 
     if (native == ArrayConstructor) {
         // Note: the template array won't be used if its length is inaccurately
         // computed here.  (We allocate here because compilation may occur on a
         // separate thread where allocation is impossible.)
         size_t count = 0;
         if (args.length() != 1)
             count = args.length();
         else if (args.length() == 1 && args[0].isInt32() && args[0].toInt32() >= 0)
             count = args[0].toInt32();
 
         if (count <= ArrayObject::EagerAllocationMaxLength) {
+            ObjectGroup* group = ObjectGroup::callingAllocationSiteGroup(cx, JSProto_Array);
+            if (!group)
+                return false;
+            if (group->maybePreliminaryObjects()) {
+                *skipAttach = true;
+                return true;
+            }
+
             // With this and other array templates, set forceAnalyze so that we
             // don't end up with a template whose structure might change later.
-            res.set(NewFullyAllocatedArrayForCallingAllocationSite(cx, count, TenuredObject,
-                                                                   /* forceAnalyze = */ true));
+            res.set(NewFullyAllocatedArrayForCallingAllocationSite(cx, count, TenuredObject));
             if (!res)
                 return false;
             return true;
         }
     }
 
     if (native == js::array_concat || native == js::array_slice) {
-        if (args.thisv().isObject() && !args.thisv().toObject().isSingleton()) {
-            res.set(NewFullyAllocatedArrayTryReuseGroup(cx, &args.thisv().toObject(), 0,
-                                                        TenuredObject, /* forceAnalyze = */ true));
-            if (!res)
-                return false;
+        if (args.thisv().isObject()) {
+            JSObject* obj = &args.thisv().toObject();
+            if (!obj->isSingleton()) {
+                if (obj->group()->maybePreliminaryObjects()) {
+                    *skipAttach = true;
+                    return true;
+                }
+                res.set(NewFullyAllocatedArrayTryReuseGroup(cx, &args.thisv().toObject(), 0,
+                                                            TenuredObject));
+                return !!res;
+            }
         }
     }
 
     if (native == js::str_split && args.length() == 1 && args[0].isString()) {
-        res.set(NewFullyAllocatedArrayForCallingAllocationSite(cx, 0, TenuredObject,
-                                                               /* forceAnalyze = */ true));
+        ObjectGroup* group = ObjectGroup::callingAllocationSiteGroup(cx, JSProto_Array);
+        if (!group)
+            return false;
+        if (group->maybePreliminaryObjects()) {
+            *skipAttach = true;
+            return true;
+        }
+
+        res.set(NewFullyAllocatedArrayForCallingAllocationSite(cx, 0, TenuredObject));
         if (!res)
             return false;
         return true;
     }
 
     if (native == StringConstructor) {
         RootedString emptyString(cx, cx->runtime()->emptyString);
         res.set(StringObject::create(cx, emptyString, TenuredObject));
@@ -9872,19 +9892,25 @@ TryAttachCallStub(JSContext* cx, ICCall_
 
             stub->addNewStub(newStub);
             *handled = true;
             return true;
         }
 
         RootedObject templateObject(cx);
         if (MOZ_LIKELY(!isSpread)) {
+            bool skipAttach = false;
             CallArgs args = CallArgsFromVp(argc, vp);
-            if (!GetTemplateObjectForNative(cx, fun->native(), args, &templateObject))
+            if (!GetTemplateObjectForNative(cx, fun->native(), args, &templateObject, &skipAttach))
                 return false;
+            if (skipAttach) {
+                *handled = true;
+                return true;
+            }
+            MOZ_ASSERT_IF(templateObject, !templateObject->group()->maybePreliminaryObjects());
         }
 
         JitSpew(JitSpew_BaselineIC, "  Generating Call_Native stub (fun=%p, cons=%s, spread=%s)",
                 fun.get(), constructing ? "yes" : "no", isSpread ? "yes" : "no");
         ICCall_Native::Compiler compiler(cx, stub->fallbackMonitorStub()->firstMonitorStub(),
                                          fun, templateObject, constructing, isSpread,
                                          script->pcToOffset(pc));
         ICStub* newStub = compiler.getStub(compiler.getStubSpace(script));
--- a/js/src/jit/CodeGenerator.cpp
+++ b/js/src/jit/CodeGenerator.cpp
@@ -7217,32 +7217,33 @@ CodeGenerator::visitArrayConcat(LArrayCo
     Register rhs = ToRegister(lir->rhs());
     Register temp1 = ToRegister(lir->temp1());
     Register temp2 = ToRegister(lir->temp2());
 
     // If 'length == initializedLength' for both arrays we try to allocate an object
     // inline and pass it to the stub. Else, we just pass nullptr and the stub falls
     // back to a slow path.
     Label fail, call;
-    if (lir->mir()->unboxedType() == JSVAL_TYPE_MAGIC) {
+    if (lir->mir()->unboxedThis()) {
+        masm.load32(Address(lhs, UnboxedArrayObject::offsetOfCapacityIndexAndInitializedLength()), temp1);
+        masm.and32(Imm32(UnboxedArrayObject::InitializedLengthMask), temp1);
+        masm.branch32(Assembler::NotEqual, Address(lhs, UnboxedArrayObject::offsetOfLength()), temp1, &fail);
+    } else {
         masm.loadPtr(Address(lhs, NativeObject::offsetOfElements()), temp1);
         masm.load32(Address(temp1, ObjectElements::offsetOfInitializedLength()), temp2);
         masm.branch32(Assembler::NotEqual, Address(temp1, ObjectElements::offsetOfLength()), temp2, &fail);
-
+    }
+    if (lir->mir()->unboxedArg()) {
+        masm.load32(Address(rhs, UnboxedArrayObject::offsetOfCapacityIndexAndInitializedLength()), temp1);
+        masm.and32(Imm32(UnboxedArrayObject::InitializedLengthMask), temp1);
+        masm.branch32(Assembler::NotEqual, Address(rhs, UnboxedArrayObject::offsetOfLength()), temp1, &fail);
+    } else {
         masm.loadPtr(Address(rhs, NativeObject::offsetOfElements()), temp1);
         masm.load32(Address(temp1, ObjectElements::offsetOfInitializedLength()), temp2);
         masm.branch32(Assembler::NotEqual, Address(temp1, ObjectElements::offsetOfLength()), temp2, &fail);
-    } else {
-        masm.load32(Address(lhs, UnboxedArrayObject::offsetOfCapacityIndexAndInitializedLength()), temp1);
-        masm.and32(Imm32(UnboxedArrayObject::InitializedLengthMask), temp1);
-        masm.branch32(Assembler::NotEqual, Address(lhs, UnboxedArrayObject::offsetOfLength()), temp1, &fail);
-
-        masm.load32(Address(rhs, UnboxedArrayObject::offsetOfCapacityIndexAndInitializedLength()), temp1);
-        masm.and32(Imm32(UnboxedArrayObject::InitializedLengthMask), temp1);
-        masm.branch32(Assembler::NotEqual, Address(rhs, UnboxedArrayObject::offsetOfLength()), temp1, &fail);
     }
 
     // Try to allocate an object.
     masm.createGCObject(temp1, temp2, lir->mir()->templateObj(), lir->mir()->initialHeap(), &fail);
     masm.jump(&call);
     {
         masm.bind(&fail);
         masm.movePtr(ImmPtr(nullptr), temp1);
--- a/js/src/jit/MCallOptimize.cpp
+++ b/js/src/jit/MCallOptimize.cpp
@@ -859,44 +859,38 @@ IonBuilder::inlineArrayConcat(CallInfo& 
         return InliningStatus_NotInlined;
 
     // |this| and the argument must be dense arrays.
     TemporaryTypeSet* thisTypes = thisArg->resultTypeSet();
     TemporaryTypeSet* argTypes = objArg->resultTypeSet();
     if (!thisTypes || !argTypes)
         return InliningStatus_NotInlined;
 
-    const Class* clasp = thisTypes->getKnownClass(constraints());
-    if (clasp != &ArrayObject::class_ && clasp != &UnboxedArrayObject::class_)
+    const Class* thisClasp = thisTypes->getKnownClass(constraints());
+    if (thisClasp != &ArrayObject::class_ && thisClasp != &UnboxedArrayObject::class_)
         return InliningStatus_NotInlined;
+    bool unboxedThis = (thisClasp == &UnboxedArrayObject::class_);
     if (thisTypes->hasObjectFlags(constraints(), OBJECT_FLAG_SPARSE_INDEXES |
                                   OBJECT_FLAG_LENGTH_OVERFLOW))
     {
         trackOptimizationOutcome(TrackedOutcome::ArrayBadFlags);
         return InliningStatus_NotInlined;
     }
 
-    if (argTypes->getKnownClass(constraints()) != clasp)
+    const Class* argClasp = argTypes->getKnownClass(constraints());
+    if (argClasp != &ArrayObject::class_ && argClasp != &UnboxedArrayObject::class_)
         return InliningStatus_NotInlined;
+    bool unboxedArg = (argClasp == &UnboxedArrayObject::class_);
     if (argTypes->hasObjectFlags(constraints(), OBJECT_FLAG_SPARSE_INDEXES |
                                  OBJECT_FLAG_LENGTH_OVERFLOW))
     {
         trackOptimizationOutcome(TrackedOutcome::ArrayBadFlags);
         return InliningStatus_NotInlined;
     }
 
-    JSValueType unboxedType = JSVAL_TYPE_MAGIC;
-    if (clasp == &UnboxedArrayObject::class_) {
-        unboxedType = UnboxedArrayElementType(constraints(), thisArg, nullptr);
-        if (unboxedType == JSVAL_TYPE_MAGIC)
-            return InliningStatus_NotInlined;
-        if (unboxedType != UnboxedArrayElementType(constraints(), objArg, nullptr))
-            return InliningStatus_NotInlined;
-    }
-
     // Watch out for indexed properties on the prototype.
     if (ArrayPrototypeHasIndexedProperty(this, script())) {
         trackOptimizationOutcome(TrackedOutcome::ProtoIndexedProps);
         return InliningStatus_NotInlined;
     }
 
     // Require the 'this' types to have a specific type matching the current
     // global, so we can create the result object inline.
@@ -946,17 +940,17 @@ IonBuilder::inlineArrayConcat(CallInfo& 
     if (!templateObj || templateObj->group() != thisGroup)
         return InliningStatus_NotInlined;
 
     callInfo.setImplicitlyUsedUnchecked();
 
     MArrayConcat* ins = MArrayConcat::New(alloc(), constraints(), thisArg, objArg,
                                           templateObj,
                                           templateObj->group()->initialHeap(constraints()),
-                                          unboxedType);
+                                          unboxedThis, unboxedArg);
     current->add(ins);
     current->push(ins);
 
     if (!resumeAfter(ins))
         return InliningStatus_Error;
     return InliningStatus_Inlined;
 }
 
--- a/js/src/jit/MIR.h
+++ b/js/src/jit/MIR.h
@@ -9181,55 +9181,62 @@ class MArrayPush
 
 // Array.prototype.concat on two dense arrays.
 class MArrayConcat
   : public MBinaryInstruction,
     public MixPolicy<ObjectPolicy<0>, ObjectPolicy<1> >::Data
 {
     CompilerObject templateObj_;
     gc::InitialHeap initialHeap_;
-    JSValueType unboxedType_;
+    bool unboxedThis_, unboxedArg_;
 
     MArrayConcat(CompilerConstraintList* constraints, MDefinition* lhs, MDefinition* rhs,
-                 JSObject* templateObj, gc::InitialHeap initialHeap, JSValueType unboxedType)
+                 JSObject* templateObj, gc::InitialHeap initialHeap,
+                 bool unboxedThis, bool unboxedArg)
       : MBinaryInstruction(lhs, rhs),
         templateObj_(templateObj),
         initialHeap_(initialHeap),
-        unboxedType_(unboxedType)
+        unboxedThis_(unboxedThis),
+        unboxedArg_(unboxedArg)
     {
         setResultType(MIRType_Object);
         setResultTypeSet(MakeSingletonTypeSet(constraints, templateObj));
     }
 
   public:
     INSTRUCTION_HEADER(ArrayConcat)
 
     static MArrayConcat* New(TempAllocator& alloc, CompilerConstraintList* constraints,
                              MDefinition* lhs, MDefinition* rhs,
                              JSObject* templateObj, gc::InitialHeap initialHeap,
-                             JSValueType unboxedType)
+                             bool unboxedThis, bool unboxedArg)
     {
         return new(alloc) MArrayConcat(constraints, lhs, rhs, templateObj,
-                                       initialHeap, unboxedType);
+                                       initialHeap, unboxedThis, unboxedArg);
     }
 
     JSObject* templateObj() const {
         return templateObj_;
     }
 
     gc::InitialHeap initialHeap() const {
         return initialHeap_;
     }
 
-    JSValueType unboxedType() const {
-        return unboxedType_;
-    }
-
-    AliasSet getAliasSet() const override {
-        return AliasSet::Store(AliasSet::BoxedOrUnboxedElements(unboxedType()) |
+    bool unboxedThis() const {
+        return unboxedThis_;
+    }
+
+    bool unboxedArg() const {
+        return unboxedArg_;
+    }
+
+    AliasSet getAliasSet() const override {
+        return AliasSet::Store(AliasSet::BoxedOrUnboxedElements(unboxedThis() ? JSVAL_TYPE_INT32 : JSVAL_TYPE_MAGIC) |
+                               AliasSet::BoxedOrUnboxedElements(unboxedArg() ? JSVAL_TYPE_INT32 : JSVAL_TYPE_MAGIC) |
                                AliasSet::ObjectFields);
     }
     bool possiblyCalls() const override {
         return true;
     }
 };
 
 // Array.prototype.slice on a dense array.
--- a/js/src/jsarray.cpp
+++ b/js/src/jsarray.cpp
@@ -2558,50 +2558,51 @@ js::array_splice_impl(JSContext* cx, uns
 
     /* Step 17. */
     if (returnValueIsUsed)
         args.rval().setObject(*arr);
 
     return true;
 }
 
-template <JSValueType Type>
+template <JSValueType TypeOne, JSValueType TypeTwo>
 DenseElementResult
 ArrayConcatDenseKernel(JSContext* cx, JSObject* obj1, JSObject* obj2, JSObject* result)
 {
-    uint32_t initlen1 = GetBoxedOrUnboxedInitializedLength<Type>(obj1);
+    uint32_t initlen1 = GetBoxedOrUnboxedInitializedLength<TypeOne>(obj1);
     MOZ_ASSERT(initlen1 == GetAnyBoxedOrUnboxedArrayLength(obj1));
 
-    uint32_t initlen2 = GetBoxedOrUnboxedInitializedLength<Type>(obj2);
+    uint32_t initlen2 = GetBoxedOrUnboxedInitializedLength<TypeTwo>(obj2);
     MOZ_ASSERT(initlen2 == GetAnyBoxedOrUnboxedArrayLength(obj2));
 
     /* No overflow here due to nelements limit. */
     uint32_t len = initlen1 + initlen2;
 
-    MOZ_ASSERT(GetBoxedOrUnboxedInitializedLength<Type>(result) == 0);
-
-    if (!EnsureBoxedOrUnboxedDenseElements<Type>(cx, result, len))
-        return DenseElementResult::Failure;
-
-    CopyBoxedOrUnboxedDenseElements<Type>(cx, result, obj1, 0, 0, initlen1);
-    CopyBoxedOrUnboxedDenseElements<Type>(cx, result, obj2, initlen1, 0, initlen2);
+    MOZ_ASSERT(GetBoxedOrUnboxedInitializedLength<TypeOne>(result) == 0);
+
+    DenseElementResult rv = EnsureBoxedOrUnboxedDenseElements<TypeOne>(cx, result, len);
+    if (rv != DenseElementResult::Success)
+        return rv;
+
+    CopyBoxedOrUnboxedDenseElements<TypeOne, TypeOne>(cx, result, obj1, 0, 0, initlen1);
+    CopyBoxedOrUnboxedDenseElements<TypeOne, TypeTwo>(cx, result, obj2, initlen1, 0, initlen2);
 
     SetAnyBoxedOrUnboxedArrayLength(cx, result, len);
     return DenseElementResult::Success;
 }
 
-DefineBoxedOrUnboxedFunctor4(ArrayConcatDenseKernel,
-                             JSContext*, JSObject*, JSObject*, JSObject*);
+DefineBoxedOrUnboxedFunctorPair4(ArrayConcatDenseKernel,
+                                 JSContext*, JSObject*, JSObject*, JSObject*);
 
 bool
 js::array_concat_dense(JSContext* cx, HandleObject obj1, HandleObject obj2,
                        HandleObject result)
 {
     ArrayConcatDenseKernelFunctor functor(cx, obj1, obj2, result);
-    DenseElementResult rv = CallBoxedOrUnboxedSpecialization(functor, result);
+    DenseElementResult rv = CallBoxedOrUnboxedSpecialization(functor, obj1, obj2);
     MOZ_ASSERT(rv != DenseElementResult::Incomplete);
     return rv == DenseElementResult::Success;
 }
 
 /*
  * Python-esque sequence operations.
  */
 bool
@@ -2622,27 +2623,73 @@ js::array_concat(JSContext* cx, unsigned
     if (IsArray(aobj, cx) && !ObjectMayHaveExtraIndexedProperties(aobj)) {
         if (!GetLengthProperty(cx, aobj, &length))
             return false;
 
         size_t initlen = GetAnyBoxedOrUnboxedInitializedLength(aobj);
         narr = NewFullyAllocatedArrayTryReuseGroup(cx, aobj, initlen);
         if (!narr)
             return false;
+        CopyAnyBoxedOrUnboxedDenseElements(cx, narr, aobj, 0, 0, initlen);
         SetAnyBoxedOrUnboxedArrayLength(cx, narr, length);
 
-        DebugOnly<DenseElementResult> result =
-            CopyAnyBoxedOrUnboxedDenseElements(cx, narr, aobj, 0, 0, initlen);
-        MOZ_ASSERT(result.value == DenseElementResult::Success);
-
         args.rval().setObject(*narr);
         if (argc == 0)
             return true;
         argc--;
         p++;
+
+        if (length == initlen) {
+            while (argc) {
+                HandleValue v = HandleValue::fromMarkedLocation(p);
+                if (!v.isObject())
+                    break;
+                RootedObject obj(cx, &v.toObject());
+
+                // This should be IsConcatSpreadable
+                if (!IsArray(obj, cx) || ObjectMayHaveExtraIndexedProperties(obj))
+                    break;
+
+                uint32_t argLength;
+                if (!GetLengthProperty(cx, obj, &argLength))
+                    return false;
+
+                initlen = GetAnyBoxedOrUnboxedInitializedLength(obj);
+                if (argLength != initlen)
+                    break;
+
+                DenseElementResult result =
+                    EnsureAnyBoxedOrUnboxedDenseElements(cx, narr, length + argLength);
+                if (result == DenseElementResult::Failure)
+                    return false;
+                if (result == DenseElementResult::Incomplete)
+                    break;
+
+                SetAnyBoxedOrUnboxedInitializedLength(cx, narr, length + argLength);
+
+                bool success = true;
+                for (size_t i = 0; i < initlen; i++) {
+                    Value v = GetAnyBoxedOrUnboxedDenseElement(obj, i);
+                    if (!InitAnyBoxedOrUnboxedDenseElement(cx, narr, length + i, v)) {
+                        success = false;
+                        break;
+                    }
+                }
+                if (!success) {
+                    SetAnyBoxedOrUnboxedInitializedLength(cx, narr, length);
+                    break;
+                }
+
+                length += argLength;
+                SetAnyBoxedOrUnboxedArrayLength(cx, narr, length);
+
+                argc--;
+                p++;
+            }
+        }
     } else {
         narr = NewDenseEmptyArray(cx);
         if (!narr)
             return false;
         args.rval().setObject(*narr);
         length = 0;
     }
 
@@ -2929,19 +2976,20 @@ ArraySliceDenseKernel(JSContext* cx, JSO
 
     if (begin > end)
         begin = end;
 
     size_t initlen = GetBoxedOrUnboxedInitializedLength<Type>(obj);
     if (initlen > begin) {
         size_t count = Min<size_t>(initlen - begin, end - begin);
         if (count) {
-            if (!EnsureBoxedOrUnboxedDenseElements<Type>(cx, result, count))
-                return DenseElementResult::Failure;
-            CopyBoxedOrUnboxedDenseElements<Type>(cx, result, obj, 0, begin, count);
+            DenseElementResult rv = EnsureBoxedOrUnboxedDenseElements<Type>(cx, result, count);
+            if (rv != DenseElementResult::Success)
+                return rv;
+            CopyBoxedOrUnboxedDenseElements<Type, Type>(cx, result, obj, 0, begin, count);
         }
     }
 
     SetAnyBoxedOrUnboxedArrayLength(cx, result, end - begin);
     return DenseElementResult::Success;
 }
 
 DefineBoxedOrUnboxedFunctor5(ArraySliceDenseKernel,
--- a/js/src/vm/UnboxedObject-inl.h
+++ b/js/src/vm/UnboxedObject-inl.h
@@ -267,16 +267,22 @@ UnboxedArrayObject::triggerPreBarrier(si
         MOZ_CRASH("Bad type");
     }
 }
 
 /////////////////////////////////////////////////////////////////////
 // Combined methods for NativeObject and UnboxedArrayObject accesses.
 /////////////////////////////////////////////////////////////////////
 
+static inline bool
+HasAnyBoxedOrUnboxedDenseElements(JSObject* obj)
+{
+    return obj->isNative() || obj->is<UnboxedArrayObject>();
+}
+
 static inline size_t
 GetAnyBoxedOrUnboxedInitializedLength(JSObject* obj)
 {
     if (obj->isNative())
         return obj->as<NativeObject>().getDenseInitializedLength();
     if (obj->is<UnboxedArrayObject>())
         return obj->as<UnboxedArrayObject>().initializedLength();
     return 0;
@@ -325,16 +331,26 @@ SetAnyBoxedOrUnboxedDenseElement(JSConte
 {
     if (obj->isNative()) {
         obj->as<NativeObject>().setDenseElementWithType(cx, index, value);
         return true;
     }
     return obj->as<UnboxedArrayObject>().setElement(cx, index, value);
 }
 
+static inline bool
+InitAnyBoxedOrUnboxedDenseElement(JSContext* cx, JSObject* obj, size_t index, const Value& value)
+{
+    if (obj->isNative()) {
+        obj->as<NativeObject>().initDenseElementWithType(cx, index, value);
+        return true;
+    }
+    return obj->as<UnboxedArrayObject>().initElement(cx, index, value);
+}
+
 /////////////////////////////////////////////////////////////////////
 // Template methods for NativeObject and UnboxedArrayObject accesses.
 /////////////////////////////////////////////////////////////////////
 
 static inline JSValueType
 GetBoxedOrUnboxedType(JSObject* obj)
 {
     if (obj->isNative())
@@ -412,29 +428,29 @@ SetBoxedOrUnboxedDenseElement(JSContext*
     if (Type == JSVAL_TYPE_MAGIC) {
         obj->as<NativeObject>().setDenseElementWithType(cx, index, value);
         return true;
     }
     return obj->as<UnboxedArrayObject>().setElementSpecific<Type>(cx, index, value);
 }
 
 template <JSValueType Type>
-static inline bool
+static inline DenseElementResult
 EnsureBoxedOrUnboxedDenseElements(JSContext* cx, JSObject* obj, size_t count)
 {
     if (Type == JSVAL_TYPE_MAGIC) {
         if (!obj->as<ArrayObject>().ensureElements(cx, count))
-            return false;
+            return DenseElementResult::Failure;
     } else {
         if (obj->as<UnboxedArrayObject>().capacity() < count) {
             if (!obj->as<UnboxedArrayObject>().growElements(cx, count))
-                return false;
+                return DenseElementResult::Failure;
         }
     }
-    return true;
+    return DenseElementResult::Success;
 }
 
 template <JSValueType Type>
 static inline DenseElementResult
 SetOrExtendBoxedOrUnboxedDenseElements(ExclusiveContext* cx, JSObject* obj,
                                        uint32_t start, const Value* vp, uint32_t count,
                                        ShouldUpdateTypes updateTypes = ShouldUpdateTypes::Update)
 {
@@ -542,43 +558,64 @@ MoveBoxedOrUnboxedDenseElements(JSContex
         memmove(data + dstStart * elementSize,
                 data + srcStart * elementSize,
                 length * elementSize);
     }
 
     return DenseElementResult::Success;
 }
 
-template <JSValueType Type>
+template <JSValueType DstType, JSValueType SrcType>
 static inline DenseElementResult
 CopyBoxedOrUnboxedDenseElements(JSContext* cx, JSObject* dst, JSObject* src,
                                 uint32_t dstStart, uint32_t srcStart, uint32_t length)
 {
-    MOZ_ASSERT(HasBoxedOrUnboxedDenseElements<Type>(src));
-    MOZ_ASSERT(HasBoxedOrUnboxedDenseElements<Type>(dst));
-    MOZ_ASSERT(GetBoxedOrUnboxedInitializedLength<Type>(dst) == dstStart);
-    MOZ_ASSERT(GetBoxedOrUnboxedCapacity<Type>(dst) >= length);
+    MOZ_ASSERT(HasBoxedOrUnboxedDenseElements<SrcType>(src));
+    MOZ_ASSERT(HasBoxedOrUnboxedDenseElements<DstType>(dst));
+    MOZ_ASSERT(GetBoxedOrUnboxedInitializedLength<DstType>(dst) == dstStart);
+    MOZ_ASSERT(GetBoxedOrUnboxedInitializedLength<DstType>(src) >= srcStart + length);
+    MOZ_ASSERT(GetBoxedOrUnboxedCapacity<DstType>(dst) >= dstStart + length);
+
+    SetBoxedOrUnboxedInitializedLength<DstType>(cx, dst, dstStart + length);
 
-    SetBoxedOrUnboxedInitializedLength<Type>(cx, dst, dstStart + length);
-
-    if (Type == JSVAL_TYPE_MAGIC) {
-        const Value* vp = src->as<NativeObject>().getDenseElements() + srcStart;
-        dst->as<NativeObject>().initDenseElements(dstStart, vp, length);
-    } else {
+    if (DstType == JSVAL_TYPE_MAGIC) {
+        if (SrcType == JSVAL_TYPE_MAGIC) {
+            const Value* vp = src->as<NativeObject>().getDenseElements() + srcStart;
+            dst->as<NativeObject>().initDenseElements(dstStart, vp, length);
+        } else {
+            for (size_t i = 0; i < length; i++) {
+                Value v = GetBoxedOrUnboxedDenseElement<SrcType>(src, srcStart + i);
+                dst->as<NativeObject>().initDenseElement(dstStart + i, v);
+            }
+        }
+    } else if (DstType == SrcType) {
         uint8_t* dstData = dst->as<UnboxedArrayObject>().elements();
         uint8_t* srcData = src->as<UnboxedArrayObject>().elements();
-        size_t elementSize = UnboxedTypeSize(Type);
+        size_t elementSize = UnboxedTypeSize(DstType);
 
         memcpy(dstData + dstStart * elementSize,
                srcData + srcStart * elementSize,
                length * elementSize);
 
         // Add a store buffer entry if we might have copied a nursery pointer to dst.
-        if (UnboxedTypeNeedsPostBarrier(Type) && !IsInsideNursery(dst))
+        if (UnboxedTypeNeedsPostBarrier(DstType) && !IsInsideNursery(dst))
             dst->runtimeFromMainThread()->gc.storeBuffer.putWholeCellFromMainThread(dst);
+    } else if (DstType == JSVAL_TYPE_DOUBLE && SrcType == JSVAL_TYPE_INT32) {
+        uint8_t* dstData = dst->as<UnboxedArrayObject>().elements();
+        uint8_t* srcData = src->as<UnboxedArrayObject>().elements();
+
+        for (size_t i = 0; i < length; i++) {
+            int32_t v = *reinterpret_cast<int32_t*>(srcData + (srcStart + i) * sizeof(int32_t));
+            *reinterpret_cast<double*>(dstData + (dstStart + i) * sizeof(double)) = v;
+        }
+    } else {
+        for (size_t i = 0; i < length; i++) {
+            Value v = GetBoxedOrUnboxedDenseElement<SrcType>(src, srcStart + i);
+            dst->as<UnboxedArrayObject>().initElementNoTypeChangeSpecific<DstType>(dstStart + i, v);
+        }
     }
 
     return DenseElementResult::Success;
 }
 
 /////////////////////////////////////////////////////////////////////
 // Dispatch to specialized methods based on the type of an object.
 /////////////////////////////////////////////////////////////////////
@@ -591,29 +628,80 @@ CopyBoxedOrUnboxedDenseElements(JSContex
 #endif
 
 // Function to dispatch a method specialized to whatever boxed or unboxed dense
 // elements which an input object has.
 template <typename F>
 DenseElementResult
 CallBoxedOrUnboxedSpecialization(F f, JSObject* obj)
 {
-    if (HasBoxedOrUnboxedDenseElements<JSVAL_TYPE_MAGIC>(obj))
+    if (!HasAnyBoxedOrUnboxedDenseElements(obj))
+        return DenseElementResult::Incomplete;
+    switch (GetBoxedOrUnboxedType(obj)) {
+      case JSVAL_TYPE_MAGIC:
         return f. DEPENDENT_TEMPLATE_HINT operator()<JSVAL_TYPE_MAGIC>();
-    if (HasBoxedOrUnboxedDenseElements<JSVAL_TYPE_BOOLEAN>(obj))
+      case JSVAL_TYPE_BOOLEAN:
         return f. DEPENDENT_TEMPLATE_HINT operator()<JSVAL_TYPE_BOOLEAN>();
-    if (HasBoxedOrUnboxedDenseElements<JSVAL_TYPE_INT32>(obj))
+      case JSVAL_TYPE_INT32:
         return f. DEPENDENT_TEMPLATE_HINT operator()<JSVAL_TYPE_INT32>();
-    if (HasBoxedOrUnboxedDenseElements<JSVAL_TYPE_DOUBLE>(obj))
+      case JSVAL_TYPE_DOUBLE:
         return f. DEPENDENT_TEMPLATE_HINT operator()<JSVAL_TYPE_DOUBLE>();
-    if (HasBoxedOrUnboxedDenseElements<JSVAL_TYPE_STRING>(obj))
+      case JSVAL_TYPE_STRING:
         return f. DEPENDENT_TEMPLATE_HINT operator()<JSVAL_TYPE_STRING>();
-    if (HasBoxedOrUnboxedDenseElements<JSVAL_TYPE_OBJECT>(obj))
+      case JSVAL_TYPE_OBJECT:
         return f. DEPENDENT_TEMPLATE_HINT operator()<JSVAL_TYPE_OBJECT>();
-    return DenseElementResult::Incomplete;
+      default:
+        MOZ_CRASH();
+    }
+}
+
+// As above, except the specialization can reflect the unboxed type of two objects.
+template <typename F>
+DenseElementResult
+CallBoxedOrUnboxedSpecialization(F f, JSObject* obj1, JSObject* obj2)
+{
+    if (!HasAnyBoxedOrUnboxedDenseElements(obj1) || !HasAnyBoxedOrUnboxedDenseElements(obj2))
+        return DenseElementResult::Incomplete;
+
+#define SPECIALIZE_OBJ2(TYPE)                                                     \
+    switch (GetBoxedOrUnboxedType(obj2)) {                                        \
+      case JSVAL_TYPE_MAGIC:                                                      \
+        return f. DEPENDENT_TEMPLATE_HINT operator()<TYPE, JSVAL_TYPE_MAGIC>();   \
+      case JSVAL_TYPE_BOOLEAN:                                                    \
+        return f. DEPENDENT_TEMPLATE_HINT operator()<TYPE, JSVAL_TYPE_BOOLEAN>(); \
+      case JSVAL_TYPE_INT32:                                                      \
+        return f. DEPENDENT_TEMPLATE_HINT operator()<TYPE, JSVAL_TYPE_INT32>();   \
+      case JSVAL_TYPE_DOUBLE:                                                     \
+        return f. DEPENDENT_TEMPLATE_HINT operator()<TYPE, JSVAL_TYPE_DOUBLE>();  \
+      case JSVAL_TYPE_STRING:                                                     \
+        return f. DEPENDENT_TEMPLATE_HINT operator()<TYPE, JSVAL_TYPE_STRING>();  \
+      case JSVAL_TYPE_OBJECT:                                                     \
+        return f. DEPENDENT_TEMPLATE_HINT operator()<TYPE, JSVAL_TYPE_OBJECT>();  \
+      default:                                                                    \
+        MOZ_CRASH();                                                              \
+    }
+
+    switch (GetBoxedOrUnboxedType(obj1)) {
+      case JSVAL_TYPE_MAGIC:
+        SPECIALIZE_OBJ2(JSVAL_TYPE_MAGIC)
+      case JSVAL_TYPE_BOOLEAN:
+        SPECIALIZE_OBJ2(JSVAL_TYPE_BOOLEAN)
+      case JSVAL_TYPE_INT32:
+        SPECIALIZE_OBJ2(JSVAL_TYPE_INT32)
+      case JSVAL_TYPE_DOUBLE:
+        SPECIALIZE_OBJ2(JSVAL_TYPE_DOUBLE)
+      case JSVAL_TYPE_STRING:
+        SPECIALIZE_OBJ2(JSVAL_TYPE_STRING)
+      case JSVAL_TYPE_OBJECT:
+        SPECIALIZE_OBJ2(JSVAL_TYPE_OBJECT)
+      default:
+        MOZ_CRASH();
+    }
+
+#undef SPECIALIZE_OBJ2
 }
 
 #undef DEPENDENT_TEMPLATE_HINT
 
 #define DefineBoxedOrUnboxedFunctor1(Signature, A)                      \
 struct Signature ## Functor {                                           \
     A a;                                                                \
     explicit Signature ## Functor(A a)                                  \
@@ -644,16 +732,28 @@ struct Signature ## Functor {           
       : a(a), b(b), c(c), d(d)                                          \
     {}                                                                  \
     template <JSValueType Type>                                         \
     DenseElementResult operator()() {                                   \
         return Signature<Type>(a, b, c, d);                             \
     }                                                                   \
 }
 
+#define DefineBoxedOrUnboxedFunctorPair4(Signature, A, B, C, D)         \
+struct Signature ## Functor {                                           \
+    A a; B b; C c; D d;                                                 \
+    Signature ## Functor(A a, B b, C c, D d)                            \
+      : a(a), b(b), c(c), d(d)                                          \
+    {}                                                                  \
+    template <JSValueType TypeOne, JSValueType TypeTwo>                 \
+    DenseElementResult operator()() {                                   \
+        return Signature<TypeOne, TypeTwo>(a, b, c, d);                 \
+    }                                                                   \
+}
+
 #define DefineBoxedOrUnboxedFunctor5(Signature, A, B, C, D, E)          \
 struct Signature ## Functor {                                           \
     A a; B b; C c; D d; E e;                                            \
     Signature ## Functor(A a, B b, C c, D d, E e)                       \
       : a(a), b(b), c(c), d(d), e(e)                                    \
     {}                                                                  \
     template <JSValueType Type>                                         \
     DenseElementResult operator()() {                                   \
@@ -668,30 +768,42 @@ struct Signature ## Functor {           
       : a(a), b(b), c(c), d(d), e(e), f(f)                              \
     {}                                                                  \
     template <JSValueType Type>                                         \
     DenseElementResult operator()() {                                   \
         return Signature<Type>(a, b, c, d, e, f);                       \
     }                                                                   \
 }
 
+#define DefineBoxedOrUnboxedFunctorPair6(Signature, A, B, C, D, E, F)   \
+struct Signature ## Functor {                                           \
+    A a; B b; C c; D d; E e; F f;                                       \
+    Signature ## Functor(A a, B b, C c, D d, E e, F f)                  \
+      : a(a), b(b), c(c), d(d), e(e), f(f)                              \
+    {}                                                                  \
+    template <JSValueType TypeOne, JSValueType TypeTwo>                 \
+    DenseElementResult operator()() {                                   \
+        return Signature<TypeOne, TypeTwo>(a, b, c, d, e, f);           \
+    }                                                                   \
+}
+
 DenseElementResult
 SetOrExtendAnyBoxedOrUnboxedDenseElements(ExclusiveContext* cx, JSObject* obj,
                                           uint32_t start, const Value* vp, uint32_t count,
                                           ShouldUpdateTypes updateTypes = ShouldUpdateTypes::Update);
 
 DenseElementResult
 MoveAnyBoxedOrUnboxedDenseElements(JSContext* cx, JSObject* obj,
                                    uint32_t dstStart, uint32_t srcStart, uint32_t length);
 
 DenseElementResult
 CopyAnyBoxedOrUnboxedDenseElements(JSContext* cx, JSObject* dst, JSObject* src,
                                    uint32_t dstStart, uint32_t srcStart, uint32_t length);
 
 void
 SetAnyBoxedOrUnboxedInitializedLength(JSContext* cx, JSObject* obj, size_t initlen);
 
-bool
+DenseElementResult
 EnsureAnyBoxedOrUnboxedDenseElements(JSContext* cx, JSObject* obj, size_t count);
 
 } // namespace js
 
 #endif // vm_UnboxedObject_inl_h
--- a/js/src/vm/UnboxedObject.cpp
+++ b/js/src/vm/UnboxedObject.cpp
@@ -1666,21 +1666,19 @@ CombinePlainObjectProperties(PlainObject
     return true;
 }
 
 static bool
 CombineArrayObjectElements(ExclusiveContext* cx, ArrayObject* obj, JSValueType* elementType)
 {
     if (obj->inDictionaryMode() ||
         obj->lastProperty()->propid() != AtomToId(cx->names().length) ||
-        !obj->lastProperty()->previous()->isEmptyShape() ||
-        !obj->getDenseInitializedLength())
+        !obj->lastProperty()->previous()->isEmptyShape())
     {
-        // Only use an unboxed representation if the object has at
-        // least one element, and no properties.
+        // Only use an unboxed representation if the object has no properties.
         return false;
     }
 
     for (size_t i = 0; i < obj->getDenseInitializedLength(); i++) {
         Value val = obj->getDenseElement(i);
 
         // For now, unboxed arrays cannot have holes.
         if (val.isMagic(JS_ELEMENTS_HOLE))
@@ -1825,16 +1823,18 @@ UnboxedArrayObject::fillAfterConvert(Exc
     MOZ_ASSERT(CapacityArray[1] == 0);
     setCapacityIndex(1);
     setInitializedLength(0);
     setInlineElements();
 
     setLength(cx, NextValue(values, valueCursor).toInt32());
 
     int32_t initlen = NextValue(values, valueCursor).toInt32();
+    if (!initlen)
+        return;
 
     if (!growElements(cx, initlen))
         CrashAtUnhandlableOOM("UnboxedArrayObject::fillAfterConvert");
     setInitializedLength(initlen);
 
     for (size_t i = 0; i < size_t(initlen); i++)
         JS_ALWAYS_TRUE(initElement(cx, i, NextValue(values, valueCursor)));
 }
@@ -2050,28 +2050,38 @@ DefineBoxedOrUnboxedFunctor5(MoveBoxedOr
 DenseElementResult
 js::MoveAnyBoxedOrUnboxedDenseElements(JSContext* cx, JSObject* obj,
                                        uint32_t dstStart, uint32_t srcStart, uint32_t length)
 {
     MoveBoxedOrUnboxedDenseElementsFunctor functor(cx, obj, dstStart, srcStart, length);
     return CallBoxedOrUnboxedSpecialization(functor, obj);
 }
 
-DefineBoxedOrUnboxedFunctor6(CopyBoxedOrUnboxedDenseElements,
-                             JSContext*, JSObject*, JSObject*, uint32_t, uint32_t, uint32_t);
+DefineBoxedOrUnboxedFunctorPair6(CopyBoxedOrUnboxedDenseElements,
+                                 JSContext*, JSObject*, JSObject*, uint32_t, uint32_t, uint32_t);
 
 DenseElementResult
 js::CopyAnyBoxedOrUnboxedDenseElements(JSContext* cx, JSObject* dst, JSObject* src,
                                        uint32_t dstStart, uint32_t srcStart, uint32_t length)
 {
     CopyBoxedOrUnboxedDenseElementsFunctor functor(cx, dst, src, dstStart, srcStart, length);
-    return CallBoxedOrUnboxedSpecialization(functor, dst);
+    return CallBoxedOrUnboxedSpecialization(functor, dst, src);
 }
 
 DefineBoxedOrUnboxedFunctor3(SetBoxedOrUnboxedInitializedLength,
                              JSContext*, JSObject*, size_t);
 
 void
 js::SetAnyBoxedOrUnboxedInitializedLength(JSContext* cx, JSObject* obj, size_t initlen)
 {
     SetBoxedOrUnboxedInitializedLengthFunctor functor(cx, obj, initlen);
     JS_ALWAYS_TRUE(CallBoxedOrUnboxedSpecialization(functor, obj) == DenseElementResult::Success);
 }
+
+DefineBoxedOrUnboxedFunctor3(EnsureBoxedOrUnboxedDenseElements,
+                             JSContext*, JSObject*, size_t);
+
+DenseElementResult
+js::EnsureAnyBoxedOrUnboxedDenseElements(JSContext* cx, JSObject* obj, size_t initlen)
+{
+    EnsureBoxedOrUnboxedDenseElementsFunctor functor(cx, obj, initlen);
+    return CallBoxedOrUnboxedSpecialization(functor, obj);
+}