Browse Source

Improve array access performance (#1625)

* always have JsValue in array and use sparse as helper to remove type checks
* faster paths for HasProperty against arrays
* remove JsArray constructor taking PropertyDescriptor[]
Marko Lahma 1 year ago
parent
commit
553fdb86ec

+ 0 - 28
Jint.Tests.PublicInterface/RavenApiUsageTests.cs

@@ -3,7 +3,6 @@ using Jint.Constraints;
 using Jint.Native;
 using Jint.Native.Function;
 using Jint.Runtime;
-using Jint.Runtime.Descriptors;
 using Jint.Runtime.Interop;
 
 namespace Jint.Tests.PublicInterface;
@@ -57,20 +56,6 @@ public class RavenApiUsageTests
         Assert.Equal(321, constraint.MaxStatements);
     }
 
-    [Fact]
-    public void CanConstructArrayInstanceFromDescriptorArray()
-    {
-        var descriptors = new[]
-        {
-            new PropertyDescriptor(42, writable: false, enumerable: false, configurable: false),
-        };
-
-        var engine = new Engine();
-        var array = new JsArray(engine, descriptors);
-        Assert.Equal(1L, array.Length);
-        Assert.Equal(42, array[0]);
-    }
-
     [Fact]
     public void CanGetPropertyDescriptor()
     {
@@ -97,15 +82,6 @@ public class RavenApiUsageTests
 
         TestArrayAccess(engine, array1, "array1");
 
-        var array3 = new JsArray(engine, new[]
-        {
-            new PropertyDescriptor(JsNumber.Create(1), true, true, true),
-            new PropertyDescriptor(JsNumber.Create(2), true, true, true),
-            new PropertyDescriptor(JsNumber.Create(3), true, true, true),
-        });
-        engine.SetValue("array3", array3);
-        TestArrayAccess(engine, array3, "array3");
-
         engine.SetValue("obj", obj);
         Assert.Equal("test", engine.Evaluate("obj.name"));
 
@@ -117,10 +93,6 @@ public class RavenApiUsageTests
         Assert.Equal(0, engine.Evaluate("emptyArray.length"));
         Assert.Equal(1, engine.Evaluate("emptyArray.push(1); return emptyArray.length"));
 
-        engine.SetValue("emptyArray", new JsArray(engine, Array.Empty<PropertyDescriptor>()));
-        Assert.Equal(0, engine.Evaluate("emptyArray.length"));
-        Assert.Equal(1, engine.Evaluate("emptyArray.push(1); return emptyArray.length"));
-
         engine.SetValue("date", new JsDate(engine, new DateTime(2022, 10, 20)));
         Assert.Equal(2022, engine.Evaluate("date.getFullYear()"));
     }

+ 271 - 291
Jint/Native/Array/ArrayInstance.cs

@@ -15,34 +15,30 @@ namespace Jint.Native.Array
         private const int MaxDenseArrayLength = 10_000_000;
 
         // we have dense and sparse, we usually can start with dense and fall back to sparse when necessary
-        // entries are lazy and can be either of type PropertyDescriptor or plain JsValue while there is no need for extra info
-        internal object?[]? _dense;
-        private Dictionary<uint, object?>? _sparse;
+        // when we have plain JsValues, _denseValues is used - if any operation occurs which requires setting more property flags
+        // we convert to _sparse and _denseValues is set to null - it will be a slow array
+        internal JsValue?[]? _dense;
+
+        private Dictionary<uint, PropertyDescriptor?>? _sparse;
 
         private ObjectChangeFlags _objectChangeFlags;
-        private bool _isObjectArray = true;
 
         private protected ArrayInstance(Engine engine, InternalTypes type) : base(engine, type: type)
         {
-            _dense = System.Array.Empty<object?>();
+            _dense = System.Array.Empty<JsValue?>();
         }
 
         private protected ArrayInstance(Engine engine, uint capacity = 0, uint length = 0) : base(engine, type: InternalTypes.Object | InternalTypes.Array)
         {
-            _prototype = engine.Realm.Intrinsics.Array.PrototypeObject;
-
-            if (capacity > engine.Options.Constraints.MaxArraySize)
-            {
-                ThrowMaximumArraySizeReachedException(engine, capacity);
-            }
+            InitializePrototypeAndValidateCapacity(engine, capacity);
 
             if (capacity < MaxDenseArrayLength)
             {
-                _dense = capacity > 0 ? new object?[capacity] : System.Array.Empty<object?>();
+                _dense = capacity > 0 ? new JsValue?[capacity] : System.Array.Empty<JsValue?>();
             }
             else
             {
-                _sparse = new Dictionary<uint, object?>(1024);
+                _sparse = new Dictionary<uint, PropertyDescriptor?>(1024);
             }
 
             _length = new PropertyDescriptor(length, PropertyFlag.OnlyWritable);
@@ -50,42 +46,20 @@ namespace Jint.Native.Array
 
         private protected ArrayInstance(Engine engine, JsValue[] items) : base(engine, type: InternalTypes.Object | InternalTypes.Array)
         {
-            Initialize(engine, items);
-        }
-
-        private protected ArrayInstance(Engine engine, PropertyDescriptor[] items) : base(engine, type: InternalTypes.Object | InternalTypes.Array)
-        {
-            Initialize(engine, items);
-        }
+            InitializePrototypeAndValidateCapacity(engine, capacity: 0);
 
-        private protected ArrayInstance(Engine engine, object[] items) : base(engine, type: InternalTypes.Object | InternalTypes.Array)
-        {
-            Initialize(engine, items);
+            _dense = items;
+            _length = new PropertyDescriptor(items.Length, PropertyFlag.OnlyWritable);
         }
 
-        private void Initialize<T>(Engine engine, T[] items) where T : class
+        private void InitializePrototypeAndValidateCapacity(Engine engine, uint capacity)
         {
-            if (items.Length > engine.Options.Constraints.MaxArraySize)
-            {
-                ThrowMaximumArraySizeReachedException(engine, (uint) items.Length);
-            }
-
             _prototype = engine.Realm.Intrinsics.Array.PrototypeObject;
-            _isObjectArray = typeof(T) == typeof(object);
 
-            int length;
-            if (items == null || items.Length == 0)
+            if (capacity > 0 && capacity > engine.Options.Constraints.MaxArraySize)
             {
-                _dense = System.Array.Empty<object>();
-                length = 0;
-            }
-            else
-            {
-                _dense = items;
-                length = items.Length;
+                ThrowMaximumArraySizeReachedException(engine, capacity);
             }
-
-            _length = new PropertyDescriptor(length, PropertyFlag.OnlyWritable);
         }
 
         public sealed override bool IsArrayLike => true;
@@ -133,149 +107,155 @@ namespace Jint.Native.Array
 
         public sealed override bool DefineOwnProperty(JsValue property, PropertyDescriptor desc)
         {
+            if (property == CommonProperties.Length)
+            {
+                return DefineLength(desc);
+            }
+
             var isArrayIndex = IsArrayIndex(property, out var index);
             TrackChanges(property, desc, isArrayIndex);
 
             if (isArrayIndex)
             {
+                ConvertToSparse();
                 return DefineOwnProperty(index, desc);
             }
 
-            if (property == CommonProperties.Length)
+            return base.DefineOwnProperty(property, desc);
+        }
+
+        private bool DefineLength(PropertyDescriptor desc)
+        {
+            var value = desc.Value;
+            if (ReferenceEquals(value, null))
             {
-                var value = desc.Value;
-                if (ReferenceEquals(value, null))
-                {
-                    return base.DefineOwnProperty(CommonProperties.Length, desc);
-                }
+                return base.DefineOwnProperty(CommonProperties.Length, desc);
+            }
 
-                var newLenDesc = new PropertyDescriptor(desc);
-                uint newLen = TypeConverter.ToUint32(value);
-                if (newLen != TypeConverter.ToNumber(value))
-                {
-                    ExceptionHelper.ThrowRangeError(_engine.Realm);
-                }
+            var newLenDesc = new PropertyDescriptor(desc);
+            uint newLen = TypeConverter.ToUint32(value);
+            if (newLen != TypeConverter.ToNumber(value))
+            {
+                ExceptionHelper.ThrowRangeError(_engine.Realm);
+            }
 
-                var oldLenDesc = _length;
-                var oldLen = (uint) TypeConverter.ToNumber(oldLenDesc!.Value);
+            var oldLenDesc = _length;
+            var oldLen = (uint) TypeConverter.ToNumber(oldLenDesc!.Value);
 
-                newLenDesc.Value = newLen;
-                if (newLen >= oldLen)
-                {
-                    return base.DefineOwnProperty(CommonProperties.Length, newLenDesc);
-                }
+            newLenDesc.Value = newLen;
+            if (newLen >= oldLen)
+            {
+                return base.DefineOwnProperty(CommonProperties.Length, newLenDesc);
+            }
 
-                if (!oldLenDesc.Writable)
-                {
-                    return false;
-                }
+            if (!oldLenDesc.Writable)
+            {
+                return false;
+            }
 
-                bool newWritable;
-                if (!newLenDesc.WritableSet || newLenDesc.Writable)
-                {
-                    newWritable = true;
-                }
-                else
-                {
-                    newWritable = false;
-                    newLenDesc.Writable = true;
-                }
+            bool newWritable;
+            if (!newLenDesc.WritableSet || newLenDesc.Writable)
+            {
+                newWritable = true;
+            }
+            else
+            {
+                newWritable = false;
+                newLenDesc.Writable = true;
+            }
 
-                var succeeded = base.DefineOwnProperty(CommonProperties.Length, newLenDesc);
-                if (!succeeded)
-                {
-                    return false;
-                }
+            var succeeded = base.DefineOwnProperty(CommonProperties.Length, newLenDesc);
+            if (!succeeded)
+            {
+                return false;
+            }
 
-                var count = _dense?.Length ?? _sparse!.Count;
-                if (count < oldLen - newLen)
+            var count = _dense?.Length ?? _sparse!.Count;
+            if (count < oldLen - newLen)
+            {
+                if (_dense != null)
                 {
-                    if (_dense != null)
+                    for (uint keyIndex = 0; keyIndex < _dense.Length; ++keyIndex)
                     {
-                        for (uint keyIndex = 0; keyIndex < _dense.Length; ++keyIndex)
+                        if (_dense[keyIndex] == null)
                         {
-                            if (_dense[keyIndex] == null)
-                            {
-                                continue;
-                            }
+                            continue;
+                        }
 
-                            // is it the index of the array
-                            if (keyIndex >= newLen && keyIndex < oldLen)
+                        // is it the index of the array
+                        if (keyIndex >= newLen && keyIndex < oldLen)
+                        {
+                            var deleteSucceeded = Delete(keyIndex);
+                            if (!deleteSucceeded)
                             {
-                                var deleteSucceeded = Delete(keyIndex);
-                                if (!deleteSucceeded)
+                                newLenDesc.Value = keyIndex + 1;
+                                if (!newWritable)
                                 {
-                                    newLenDesc.Value = keyIndex + 1;
-                                    if (!newWritable)
-                                    {
-                                        newLenDesc.Writable = false;
-                                    }
-
-                                    base.DefineOwnProperty(CommonProperties.Length, newLenDesc);
-                                    return false;
+                                    newLenDesc.Writable = false;
                                 }
+
+                                base.DefineOwnProperty(CommonProperties.Length, newLenDesc);
+                                return false;
                             }
                         }
                     }
-                    else
+                }
+                else
+                {
+                    // in the case of sparse arrays, treat each concrete element instead of
+                    // iterating over all indexes
+                    var keys = new List<uint>(_sparse!.Keys);
+                    var keysCount = keys.Count;
+                    for (var i = 0; i < keysCount; i++)
                     {
-                        // in the case of sparse arrays, treat each concrete element instead of
-                        // iterating over all indexes
-                        var keys = new List<uint>(_sparse!.Keys);
-                        var keysCount = keys.Count;
-                        for (var i = 0; i < keysCount; i++)
-                        {
-                            var keyIndex = keys[i];
+                        var keyIndex = keys[i];
 
-                            // is it the index of the array
-                            if (keyIndex >= newLen && keyIndex < oldLen)
+                        // is it the index of the array
+                        if (keyIndex >= newLen && keyIndex < oldLen)
+                        {
+                            var deleteSucceeded = Delete(TypeConverter.ToString(keyIndex));
+                            if (!deleteSucceeded)
                             {
-                                var deleteSucceeded = Delete(TypeConverter.ToString(keyIndex));
-                                if (!deleteSucceeded)
+                                newLenDesc.Value = JsNumber.Create(keyIndex + 1);
+                                if (!newWritable)
                                 {
-                                    newLenDesc.Value = JsNumber.Create(keyIndex + 1);
-                                    if (!newWritable)
-                                    {
-                                        newLenDesc.Writable = false;
-                                    }
-
-                                    base.DefineOwnProperty(CommonProperties.Length, newLenDesc);
-                                    return false;
+                                    newLenDesc.Writable = false;
                                 }
+
+                                base.DefineOwnProperty(CommonProperties.Length, newLenDesc);
+                                return false;
                             }
                         }
                     }
                 }
-                else
+            }
+            else
+            {
+                while (newLen < oldLen)
                 {
-                    while (newLen < oldLen)
+                    // algorithm as per the spec
+                    oldLen--;
+                    var deleteSucceeded = Delete(oldLen);
+                    if (!deleteSucceeded)
                     {
-                        // algorithm as per the spec
-                        oldLen--;
-                        var deleteSucceeded = Delete(oldLen);
-                        if (!deleteSucceeded)
+                        newLenDesc.Value = oldLen + 1;
+                        if (!newWritable)
                         {
-                            newLenDesc.Value = oldLen + 1;
-                            if (!newWritable)
-                            {
-                                newLenDesc.Writable = false;
-                            }
-
-                            base.DefineOwnProperty(CommonProperties.Length, newLenDesc);
-                            return false;
+                            newLenDesc.Writable = false;
                         }
-                    }
-                }
 
-                if (!newWritable)
-                {
-                    base.DefineOwnProperty(CommonProperties.Length, new PropertyDescriptor(value: null, PropertyFlag.WritableSet));
+                        base.DefineOwnProperty(CommonProperties.Length, newLenDesc);
+                        return false;
+                    }
                 }
+            }
 
-                return true;
+            if (!newWritable)
+            {
+                base.DefineOwnProperty(CommonProperties.Length, new PropertyDescriptor(value: null, PropertyFlag.WritableSet));
             }
 
-            return base.DefineOwnProperty(property, desc);
+            return true;
         }
 
         private bool DefineOwnProperty(uint index, PropertyDescriptor desc)
@@ -397,39 +377,28 @@ namespace Jint.Native.Array
             if (temp != null)
             {
                 var length = System.Math.Min(temp.Length, GetLength());
-                for (var i = 0; i < length; i++)
+                for (uint i = 0; i < length; i++)
                 {
                     var value = temp[i];
                     if (value != null)
                     {
-                        if (value is not PropertyDescriptor descriptor)
+                        if (_sparse is null || !_sparse.TryGetValue(i, out var descriptor) || descriptor is null)
                         {
-                            if (EnsureCompatibleDense(typeof(PropertyDescriptor)))
-                            {
-                                temp = _dense!;
-                            }
-                            temp[i] = descriptor = new PropertyDescriptor((JsValue) value, PropertyFlag.ConfigurableEnumerableWritable);
+                            _sparse ??= new Dictionary<uint, PropertyDescriptor?>();
+                            _sparse[i] = descriptor = new PropertyDescriptor(value, PropertyFlag.ConfigurableEnumerableWritable);
                         }
                         yield return new KeyValuePair<JsValue, PropertyDescriptor>(TypeConverter.ToString(i), descriptor);
                     }
                 }
             }
-            else
+            else if (_sparse != null)
             {
-                foreach (var entry in _sparse!)
+                foreach (var entry in _sparse)
                 {
                     var value = entry.Value;
                     if (value is not null)
                     {
-                        if (value is not PropertyDescriptor descriptor)
-                        {
-                            if (EnsureCompatibleDense(typeof(PropertyDescriptor)))
-                            {
-                                temp = _dense!;
-                            }
-                            _sparse[entry.Key] = descriptor = new PropertyDescriptor((JsValue) value, PropertyFlag.ConfigurableEnumerableWritable);
-                        }
-                        yield return new KeyValuePair<JsValue, PropertyDescriptor>(TypeConverter.ToString(entry.Key), descriptor);
+                        yield return new KeyValuePair<JsValue, PropertyDescriptor>(TypeConverter.ToString(entry.Key), value);
                     }
                 }
             }
@@ -454,7 +423,7 @@ namespace Jint.Native.Array
 
             if (IsArrayIndex(property, out var index))
             {
-                if (TryGetDescriptor(index, out var result))
+                if (TryGetDescriptor(index, createIfMissing: true, out var result))
                 {
                     return result;
                 }
@@ -497,33 +466,23 @@ namespace Jint.Native.Array
         public sealed override bool Set(JsValue property, JsValue value, JsValue receiver)
         {
             var isSafeSelfTarget = IsSafeSelfTarget(receiver);
-            if (isSafeSelfTarget && IsArrayIndex(property, out var index))
+            if (isSafeSelfTarget && CanUseFastAccess)
             {
-                var temp = _dense;
-                if (temp is not null && CanUseFastAccess)
+                if (IsArrayIndex(property, out var index))
                 {
-                    var current = index < temp.Length ? temp[index] : null;
-                    if (current is not PropertyDescriptor p || p.IsDefaultArrayValueDescriptor())
-                    {
-                        SetIndexValue(index, value, true);
-                        return true;
-                    }
+                    SetIndexValue(index, value, updateLength: true);
+                    return true;
                 }
 
-                // slower and more allocating
-                if (TryGetDescriptor(index, out var descriptor))
+                if (property == CommonProperties.Length
+                    && _length is { Writable: true }
+                    && value is JsNumber jsNumber
+                    && jsNumber.IsInteger()
+                    && jsNumber._value <= MaxDenseArrayLength
+                    && jsNumber._value >= GetLength())
                 {
-                    if (descriptor.IsDefaultArrayValueDescriptor())
-                    {
-                        // fast path with direct write without allocations
-                        descriptor.Value = value;
-                        return true;
-                    }
-                }
-                else if (CanUseFastAccess)
-                {
-                    // we know it's to be written to own array backing field as new value
-                    SetIndexValue(index, value, true);
+                    // we don't need explicit resize
+                    _length.Value = jsNumber;
                     return true;
                 }
             }
@@ -544,6 +503,27 @@ namespace Jint.Native.Array
             return base.HasProperty(property);
         }
 
+        internal bool HasProperty(ulong index)
+        {
+            if (index < uint.MaxValue)
+            {
+                var temp = _dense;
+                if (temp != null)
+                {
+                    if (index < (uint) temp.Length && temp[index] is not null)
+                    {
+                        return true;
+                    }
+                }
+                else if (_sparse!.ContainsKey((uint) index))
+                {
+                    return true;
+                }
+            }
+
+            return base.HasProperty(index);
+        }
+
         protected internal sealed override void SetOwnProperty(JsValue property, PropertyDescriptor desc)
         {
             var isArrayIndex = IsArrayIndex(property, out var index);
@@ -568,11 +548,15 @@ namespace Jint.Native.Array
 
             if (isArrayIndex)
             {
-                if (!desc.IsDefaultArrayValueDescriptor())
+                if (!desc.IsDefaultArrayValueDescriptor() && desc.Flags != PropertyFlag.None)
                 {
                     _objectChangeFlags |= ObjectChangeFlags.NonDefaultDataDescriptorUsage;
                 }
-                _objectChangeFlags |= ObjectChangeFlags.ArrayIndex;
+
+                if (GetType() != typeof(JsArray))
+                {
+                    _objectChangeFlags |= ObjectChangeFlags.ArrayIndex;
+                }
             }
             else
             {
@@ -712,8 +696,7 @@ namespace Jint.Native.Array
             {
                 if (index < (uint) temp.Length)
                 {
-                    var value = temp[index];
-                    if (value is JsValue || value is PropertyDescriptor { Configurable: true })
+                    if (!TryGetDescriptor(index, createIfMissing: false, out var descriptor) || descriptor.Configurable)
                     {
                         temp[index] = null;
                         return true;
@@ -721,7 +704,7 @@ namespace Jint.Native.Array
                 }
             }
 
-            if (!TryGetDescriptor(index, out var desc))
+            if (!TryGetDescriptor(index, createIfMissing: false, out var desc))
             {
                 return true;
             }
@@ -754,7 +737,7 @@ namespace Jint.Native.Array
             return false;
         }
 
-        private bool TryGetDescriptor(uint index, [NotNullWhen(true)] out PropertyDescriptor? descriptor)
+        private bool TryGetDescriptor(uint index, bool createIfMissing, [NotNullWhen(true)] out PropertyDescriptor? descriptor)
         {
             descriptor = null;
             var temp = _dense;
@@ -763,37 +746,30 @@ namespace Jint.Native.Array
                 if (index < (uint) temp.Length)
                 {
                     var value = temp[index];
-                    if (value is JsValue jsValue)
+                    if (value != null)
                     {
-                        if (EnsureCompatibleDense(typeof(PropertyDescriptor)))
+                        if (_sparse is null || !_sparse.TryGetValue(index, out descriptor) || descriptor is null)
                         {
-                            temp = _dense!;
+                            if (!createIfMissing)
+                            {
+                                return false;
+                            }
+                            _sparse ??= new Dictionary<uint, PropertyDescriptor?>();
+                            _sparse[index] = descriptor = new PropertyDescriptor(value, PropertyFlag.ConfigurableEnumerableWritable);
                         }
-                        temp[index] = descriptor = new PropertyDescriptor(jsValue, PropertyFlag.ConfigurableEnumerableWritable);
-                    }
-                    else if (value is PropertyDescriptor propertyDescriptor)
-                    {
-                        descriptor = propertyDescriptor;
-                    }
-                }
-                return descriptor != null;
-            }
 
-            if (_sparse!.TryGetValue(index, out var sparseValue))
-            {
-                if (sparseValue is JsValue jsValue)
-                {
-                    _sparse[index] = descriptor = new PropertyDescriptor(jsValue, PropertyFlag.ConfigurableEnumerableWritable);
-                }
-                else if (sparseValue is PropertyDescriptor propertyDescriptor)
-                {
-                    descriptor = propertyDescriptor;
+                        descriptor.Value = value;
+                        return true;
+                    }
                 }
+                return false;
             }
 
+            _sparse?.TryGetValue(index, out descriptor);
             return descriptor is not null;
         }
 
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
         internal bool TryGetValue(uint index, out JsValue value)
         {
             value = GetValue(index, unwrapFromNonDataDescriptor: true)!;
@@ -803,6 +779,12 @@ namespace Jint.Native.Array
                 return true;
             }
 
+            return TryGetValueUnlikely(index, out value);
+        }
+
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        private bool TryGetValueUnlikely(uint index, out JsValue value)
+        {
             if (!CanUseFastAccess)
             {
                 // slow path must be checked for prototype
@@ -825,54 +807,75 @@ namespace Jint.Native.Array
             return false;
         }
 
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
         private JsValue? GetValue(uint index, bool unwrapFromNonDataDescriptor)
         {
-            object? value = null;
             var temp = _dense;
             if (temp != null)
             {
                 if (index < (uint) temp.Length)
                 {
-                    value = temp[index];
+                    return temp[index];
                 }
-            }
-            else
-            {
-                _sparse!.TryGetValue(index, out value);
+                return null;
             }
 
-            if (value is JsValue jsValue)
-            {
-                return jsValue;
-            }
+            return GetValueUnlikely(index, unwrapFromNonDataDescriptor);
+        }
 
-            if (value is PropertyDescriptor propertyDescriptor)
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        private JsValue? GetValueUnlikely(uint index, bool unwrapFromNonDataDescriptor)
+        {
+            JsValue? value = null;
+            if (_sparse!.TryGetValue(index, out var descriptor) && descriptor != null)
             {
-                return propertyDescriptor.IsDataDescriptor() || unwrapFromNonDataDescriptor ? UnwrapJsValue(propertyDescriptor) : null;
+                value = descriptor.IsDataDescriptor() || unwrapFromNonDataDescriptor
+                    ? UnwrapJsValue(descriptor)
+                    : null;
             }
 
-            return null;
+            return value;
         }
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        private void WriteArrayValue(uint index, object? value)
+        private void WriteArrayValue(uint index, PropertyDescriptor descriptor)
         {
-            var dense = _dense;
-            if (dense != null && index < (uint) dense.Length)
+            var temp = _dense;
+            if (temp != null && descriptor.IsDefaultArrayValueDescriptor())
             {
-                if (value is not null && !_isObjectArray && EnsureCompatibleDense(value is JsValue ? typeof(JsValue) : typeof(PropertyDescriptor)))
+                if (index < (uint) temp.Length)
                 {
-                    dense = _dense;
+                    temp[index] = descriptor.Value;
+                }
+                else
+                {
+                    WriteArrayValueUnlikely(index, descriptor.Value);
                 }
-                dense![index] = value;
             }
             else
             {
-                WriteArrayValueUnlikely(index, value);
+                WriteArrayValueUnlikely(index, descriptor);
             }
         }
 
-        private void WriteArrayValueUnlikely(uint index, object? value)
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private void WriteArrayValue(uint index, JsValue? value)
+        {
+            var temp = _dense;
+            if (temp != null)
+            {
+                if (index < (uint) temp.Length)
+                {
+                    temp[index] = value;
+                    return;
+                }
+            }
+
+            WriteArrayValueUnlikely(index, value);
+        }
+
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        private void WriteArrayValueUnlikely(uint index, JsValue? value)
         {
             // calculate eagerly so we know if we outgrow
             var dense = _dense;
@@ -892,50 +895,45 @@ namespace Jint.Native.Array
             }
             else
             {
-                if (dense != null)
-                {
-                    ConvertToSparse();
-                }
-
-                _sparse![index] = value;
+                ConvertToSparse();
+                _sparse![index] = new PropertyDescriptor(value, PropertyFlag.ConfigurableEnumerableWritable);
             }
         }
 
-        /// <summary>
-        /// Converts to object array when needed. Returns true if conversion was made.
-        /// </summary>
-        private bool EnsureCompatibleDense(Type expectedElementType)
+        private void WriteArrayValueUnlikely(uint index, PropertyDescriptor? value)
         {
-            if (!_isObjectArray)
+            if (_sparse == null)
             {
-                return CheckConversionUnlikely(expectedElementType);
+                ConvertToSparse();
             }
 
-            return false;
+            _sparse![index] = value;
         }
 
-        private bool CheckConversionUnlikely(Type expectedElementType)
+        private void ConvertToSparse()
         {
-            var currentElementType = _dense!.GetType().GetElementType();
-            if (currentElementType != typeof(object) && !expectedElementType.IsAssignableFrom(currentElementType))
+            // need to move data
+            var temp = _dense;
+
+            if (temp is null)
             {
-                // triggers conversion for array
-                EnsureCapacity((uint) _dense.Length, force: true);
-                return true;
+                return;
             }
 
-            return false;
-        }
-
-        private void ConvertToSparse()
-        {
-            _sparse = new Dictionary<uint, object?>(_dense!.Length <= 1024 ? _dense.Length : 0);
-            // need to move data
-            for (uint i = 0; i < (uint) _dense.Length; ++i)
+            _sparse ??= new Dictionary<uint, PropertyDescriptor?>();
+            for (uint i = 0; i < (uint) temp.Length; ++i)
             {
-                if (_dense[i] != null)
+                var value = temp[i];
+                if (value != null)
                 {
-                    _sparse[i] = _dense[i];
+                    _sparse.TryGetValue(i, out var descriptor);
+                    descriptor ??= new PropertyDescriptor(value, PropertyFlag.ConfigurableEnumerableWritable);
+                    descriptor.Value = value;
+                    _sparse[i] = descriptor;
+                }
+                else
+                {
+                    _sparse.Remove(i);
                 }
             }
 
@@ -962,10 +960,9 @@ namespace Jint.Native.Array
             }
 
             // need to grow
-            var newArray = new object[capacity];
+            var newArray = new JsValue[capacity];
             System.Array.Copy(dense, newArray, dense.Length);
             _dense = newArray;
-            _isObjectArray = true;
         }
 
         public JsValue[] ToArray()
@@ -1020,14 +1017,7 @@ namespace Jint.Native.Array
                     var value = temp[i];
                     if (value != null)
                     {
-                        if (value is not PropertyDescriptor descriptor)
-                        {
-                            yield return new IndexedEntry(i, (JsValue) value);
-                        }
-                        else
-                        {
-                            yield return new IndexedEntry(i, descriptor.Value);
-                        }
+                        yield return new IndexedEntry(i, value);
                     }
                 }
             }
@@ -1035,17 +1025,10 @@ namespace Jint.Native.Array
             {
                 foreach (var entry in _sparse!)
                 {
-                    var value = entry.Value;
-                    if (value is not null)
+                    var descriptor = entry.Value;
+                    if (descriptor is not null)
                     {
-                        if (value is not PropertyDescriptor descriptor)
-                        {
-                            yield return new IndexedEntry((int) entry.Key, (JsValue) value);
-                        }
-                        else
-                        {
-                            yield return new IndexedEntry((int) entry.Key, descriptor.Value);
-                        }
+                        yield return new IndexedEntry((int) entry.Key, descriptor.Value);
                     }
                 }
             }
@@ -1069,7 +1052,7 @@ namespace Jint.Native.Array
             }
             else
             {
-                WriteValueSlow(n, new PropertyDescriptor(value, PropertyFlag.ConfigurableEnumerableWritable));
+                WriteValueSlow(n, value);
             }
 
             // check if we can set length fast without breaking ECMA specification
@@ -1146,15 +1129,15 @@ namespace Jint.Native.Array
         }
 
         [MethodImpl(MethodImplOptions.NoInlining)]
-        private void WriteValueSlow(double n, PropertyDescriptor desc)
+        private void WriteValueSlow(double n, JsValue value)
         {
-            if (n < uint.MaxValue)
+            if (n < ArrayOperations.MaxArrayLength)
             {
-                WriteArrayValue((uint) n, desc);
+                WriteArrayValue((uint) n, value);
             }
             else
             {
-                DefinePropertyOrThrow((uint) n, desc);
+                DefinePropertyOrThrow((uint) n, new PropertyDescriptor(value, PropertyFlag.ConfigurableEnumerableWritable));
             }
         }
 
@@ -1325,21 +1308,18 @@ namespace Jint.Native.Array
             }
 
             var dense = _dense;
-            if (dense != null && sourceDense != null
-                               && (uint) dense.Length >= targetStartIndex + length
-                               && dense[targetStartIndex] is null)
+            if (dense != null
+                && sourceDense != null
+                && (uint) dense.Length >= targetStartIndex + length
+                && dense[targetStartIndex] is null)
             {
                 uint j = 0;
                 for (var i = sourceStartIndex; i < sourceStartIndex + length; ++i, j++)
                 {
-                    object? sourceValue;
+                    JsValue? sourceValue;
                     if (i < (uint) sourceDense.Length && sourceDense[i] != null)
                     {
                         sourceValue = sourceDense[i];
-                        if (sourceValue is PropertyDescriptor propertyDescriptor)
-                        {
-                            sourceValue = UnwrapJsValue(propertyDescriptor);
-                        }
                     }
                     else
                     {

+ 12 - 11
Jint/Native/Array/ArrayOperations.cs

@@ -3,7 +3,6 @@ using Jint.Native.Number;
 using Jint.Native.Object;
 using Jint.Native.TypedArray;
 using Jint.Runtime;
-using Jint.Runtime.Descriptors;
 
 namespace Jint.Native.Array
 {
@@ -70,7 +69,7 @@ namespace Jint.Native.Array
 
         public abstract bool TryGetValue(ulong index, out JsValue value);
 
-        public bool HasProperty(ulong index) => Target.HasProperty(index);
+        public abstract bool HasProperty(ulong index);
 
         public abstract void CreateDataPropertyOrThrow(ulong index, JsValue value);
 
@@ -201,6 +200,8 @@ namespace Jint.Native.Array
 
             public override void DeletePropertyOrThrow(ulong index)
                 => _target.DeletePropertyOrThrow(JsString.Create(index));
+
+            public override bool HasProperty(ulong index) => Target.HasProperty(index);
         }
 
         private sealed class ArrayInstanceOperations : ArrayOperations<JsArray>
@@ -244,22 +245,18 @@ namespace Jint.Native.Array
                 var jsValues = new JsValue[n];
                 for (uint i = 0; i < (uint) jsValues.Length; i++)
                 {
-                    var prop = _target._dense[i];
-                    if (prop is null)
-                    {
-                        prop = _target.Prototype?.Get(i) ?? JsValue.Undefined;
-                    }
-                    else if (prop is not JsValue)
+                    var value = _target._dense[i];
+                    if (value is null)
                     {
-                        prop = _target.UnwrapJsValue((PropertyDescriptor) prop);
+                        value = _target.Prototype?.Get(i) ?? JsValue.Undefined;
                     }
 
-                    if (prop is JsValue jsValue && (jsValue.Type & elementTypes) == 0)
+                    if ((value.Type & elementTypes) == 0)
                     {
                         ExceptionHelper.ThrowTypeErrorNoEngine("invalid type");
                     }
 
-                    jsValues[writeIndex++] = (JsValue?) prop ?? JsValue.Undefined;
+                    jsValues[writeIndex++] = (JsValue?) value ?? JsValue.Undefined;
                 }
 
                 return jsValues;
@@ -273,6 +270,8 @@ namespace Jint.Native.Array
 
             public override void Set(ulong index, JsValue value, bool updateLength = false, bool throwOnError = true)
                 => _target.SetIndexValue((uint) index, value, updateLength);
+
+            public override bool HasProperty(ulong index) => _target.HasProperty(index);
         }
 
         private sealed class TypedArrayInstanceOperations : ArrayOperations
@@ -336,6 +335,8 @@ namespace Jint.Native.Array
 
             public override void DeletePropertyOrThrow(ulong index)
                 => _target.DeletePropertyOrThrow(index);
+
+            public override bool HasProperty(ulong index) => _target.HasProperty(index);
         }
 
     }

+ 2 - 2
Jint/Native/Array/ArrayPrototype.cs

@@ -1648,10 +1648,10 @@ namespace Jint.Native.Array
             return element;
         }
 
-        private object[] CreateBackingArray(ulong length)
+        private JsValue[] CreateBackingArray(ulong length)
         {
             ValidateArrayLength(length);
-            return new object[length];
+            return new JsValue[length];
         }
 
         private void ValidateArrayLength(ulong length)

+ 0 - 13
Jint/Native/JsArray.cs

@@ -1,5 +1,4 @@
 using Jint.Native.Array;
-using Jint.Runtime.Descriptors;
 
 namespace Jint.Native;
 
@@ -22,16 +21,4 @@ public sealed class JsArray : ArrayInstance
     public JsArray(Engine engine, JsValue[] items) : base(engine, items)
     {
     }
-
-    /// <summary>
-    /// Possibility to construct valid array fast, requires that supplied array does not have holes.
-    /// The array will be owned and modified by Jint afterwards.
-    /// </summary>
-    public JsArray(Engine engine, PropertyDescriptor[] items) : base(engine, items)
-    {
-    }
-
-    internal JsArray(Engine engine, object[] items) : base(engine, items)
-    {
-    }
 }

+ 3 - 2
Jint/Native/Object/ObjectInstance.cs

@@ -555,6 +555,7 @@ namespace Jint.Native.Object
             return SetUnlikely(property, value, receiver);
         }
 
+        [MethodImpl(MethodImplOptions.NoInlining)]
         private bool SetUnlikely(JsValue property, JsValue value, JsValue receiver)
         {
             var ownDesc = GetOwnProperty(property);
@@ -1510,7 +1511,7 @@ namespace Jint.Native.Object
             }
 
             var min = length;
-            foreach (var entry in Properties)
+            foreach (var entry in GetOwnProperties())
             {
                 if (ulong.TryParse(entry.Key.ToString(), out var index))
                 {
@@ -1520,7 +1521,7 @@ namespace Jint.Native.Object
 
             if (Prototype?.Properties != null)
             {
-                foreach (var entry in Prototype.Properties)
+                foreach (var entry in Prototype.GetOwnProperties())
                 {
                     if (ulong.TryParse(entry.Key.ToString(), out var index))
                     {

+ 15 - 10
Jint/Runtime/TypeConverter.cs

@@ -845,40 +845,45 @@ namespace Jint.Runtime
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         internal static string ToString(long i)
         {
-            return i >= 0 && i < intToString.Length
-                ? intToString[i]
+            var temp = intToString;
+            return (ulong) i < (ulong) temp.Length
+                ? temp[i]
                 : i.ToString();
         }
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         internal static string ToString(int i)
         {
-            return i >= 0 && i < intToString.Length
-                ? intToString[i]
+            var temp = intToString;
+            return (uint) i < (uint) temp.Length
+                ? temp[i]
                 : i.ToString();
         }
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         internal static string ToString(uint i)
         {
-            return i < (uint) intToString.Length
-                ? intToString[i]
+            var temp = intToString;
+            return i < (uint) temp.Length
+                ? temp[i]
                 : i.ToString();
         }
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         internal static string ToString(char c)
         {
-            return c >= 0 && c < charToString.Length
-                ? charToString[c]
+            var temp = charToString;
+            return (uint) c < (uint) temp.Length
+                ? temp[c]
                 : c.ToString();
         }
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         internal static string ToString(ulong i)
         {
-            return i >= 0 && i < (ulong) intToString.Length
-                ? intToString[i]
+            var temp = intToString;
+            return i < (ulong) temp.Length
+                ? temp[i]
                 : i.ToString();
         }