NativeHashMap - TryReplaceValue

May be useful for someone, extended NativeHashMap container for replacing value by key.
Cause native Unity implementation not support that yet :slight_smile:

May be someone posted this before on forum (really don’t remember, cause so many posts watched by me on forum from the beginning of ECS, and I can’t remember all :slight_smile: if so, please link thread with that )

Implemented 2 variations - by indexer and by method call:
Indexer

public TValue this [TKey key]
        {
            get
            {
                TValue res;
#if ENABLE_UNITY_COLLECTIONS_CHECKS
                if (TryGetValue(key, out res))
                    return res;
                else
                    throw new ArgumentException($"Key: {key} is not present in the NativeHashMap.");
#else
                TryGetValue(key, out res);
                return res;
#endif
            }
            set
            {
#if ENABLE_UNITY_COLLECTIONS_CHECKS
                if (!TryReplaceValue(key, value))
                    throw new ArgumentException($"Key: {key} is not present in the NativeHashMap.");
#else
                TryReplaceValue(key, value)
#endif
            }
        }

TryReplaceValue

public bool TryReplaceValue(TKey key, TValue item)
        {
#if ENABLE_UNITY_COLLECTIONS_CHECKS
            AtomicSafetyHandle.CheckWriteAndThrow(m_Safety);
#endif
            return NativeHashMapBase<TKey, TValue>.TryReplaceValue(m_Buffer, key, item, false, m_AllocatorLabel);
        }

public static unsafe bool TryReplaceValue(NativeHashMapData* data, TKey key, TValue item, bool isMultiHashMap,
            Allocator allocation)
        {
            TValue tempItem;
            NativeMultiHashMapIterator<TKey> tempIt;
            if (isMultiHashMap)
                return false;
            return TryReplaceFirstValueAtomic(data, key, item, out tempIt);
        }
       
        public static unsafe bool TryReplaceFirstValueAtomic(NativeHashMapData* data, TKey key, TValue item,
            out NativeMultiHashMapIterator<TKey> it)
        {
            it.key = key;
            if (data->allocatedIndexLength <= 0)
            {
                it.EntryIndex = it.NextEntryIndex = -1;
                return false;
            }
            // First find the slot based on the hash
            int* buckets                      = (int*) data->buckets;
            int  bucket                       = key.GetHashCode() & data->bucketCapacityMask;
            it.EntryIndex = it.NextEntryIndex = buckets[bucket];
            return TryReplaceNextValueAtomic(data, item, ref it);
        }

        public static unsafe bool TryReplaceNextValueAtomic(NativeHashMapData* data, TValue item,
            ref NativeMultiHashMapIterator<TKey> it)
        {
            int entryIdx = it.NextEntryIndex;
            it.NextEntryIndex = -1;
            it.EntryIndex     = -1;
            if (entryIdx < 0 || entryIdx >= data->keyCapacity)
                return false;
            int* nextPtrs = (int*) data->next;
            while (!UnsafeUtility.ReadArrayElement<TKey>(data->keys, entryIdx).Equals(it.key))
            {
                entryIdx = nextPtrs[entryIdx];
                if (entryIdx < 0 || entryIdx >= data->keyCapacity)
                    return false;
            }
            it.NextEntryIndex = nextPtrs[entryIdx];
            it.EntryIndex     = entryIdx;

            // Write the value
            UnsafeUtility.WriteArrayElement(data->keys, entryIdx, it.key);
            UnsafeUtility.WriteArrayElement(data->values, entryIdx, item);
            return true;
        }

Maded on Collections version 0.0.9-preview.11.
For using just replace NativeHashMap.cs inside your ProjectFolder\Library\PackageCache\com.unity.collections@0.0.9-preview.11\Unity.Collections location.

4216720–374389–NativeHashMap.cs (44.5 KB)

6 Likes

Thx and bye bye tryget - remove - tryadd :wink:

This is great but I’m not a fan of replacing the source in packages. Conveniently I already have an imposter for the hashmaps from the work I did iterating them.

Here is the same code but as an extension method so it doesn’t require you to replace anything in the package, without using any reflection

    /// <summary>
    /// The NativeHashMapExtensions.
    /// </summary>
    public static class NativeHashMapExtensions
    {
        /// <remarks>
        /// Based off work by eizenhorn https://discussions.unity.com/t/732252 .
        /// </remarks>
        public static unsafe bool TryReplaceValue<TKey, TValue>(this NativeHashMap<TKey, TValue> hashMap, TKey key, TValue item)
            where TKey : struct, IEquatable<TKey>
            where TValue : struct
        {
            var imposter = (NativeHashMapImposter<TKey, TValue>)hashMap;

#if ENABLE_UNITY_COLLECTIONS_CHECKS
            AtomicSafetyHandle.CheckWriteAndThrow(imposter.Safety);
#endif
            return NativeHashMapImposter<TKey, TValue>.TryReplaceValue(imposter.Buffer, key, item, false);
        }
    }

And a few imposters that make it work

NativeHashMapImposter

[StructLayout(LayoutKind.Sequential)]
    internal unsafe struct NativeHashMapImposter<TKey, TValue>
        where TKey : struct, IEquatable<TKey>
        where TValue : struct
    {
        [NativeDisableUnsafePtrRestriction]
        public NativeHashMapDataImposter* Buffer;

#if ENABLE_UNITY_COLLECTIONS_CHECKS
        public AtomicSafetyHandle Safety;

        [NativeSetClassTypeToNullOnSchedule]
        public DisposeSentinel DisposeSentinel;
#endif

        public Allocator AllocatorLabel;

        public static implicit operator NativeHashMapImposter<TKey, TValue>(NativeHashMap<TKey, TValue> hashMap)
        {
            var ptr = UnsafeUtility.AddressOf(ref hashMap);
            UnsafeUtility.CopyPtrToStructure(ptr, out NativeHashMapImposter<TKey, TValue> imposter);
            return imposter;
        }


        internal static bool TryReplaceValue(NativeHashMapDataImposter* data, TKey key, TValue item, bool isMultiHashMap)
        {
            if (isMultiHashMap)
            {
                return false;
            }

            return TryReplaceFirstValueAtomic(data, key, item, out _);
        }

        private static bool TryReplaceFirstValueAtomic(NativeHashMapDataImposter* data, TKey key,
            TValue item, out NativeMultiHashMapIteratorImposter<TKey> it)
        {
            it.key = key;
            if (data->AllocatedIndexLength <= 0)
            {
                it.EntryIndex = it.NextEntryIndex = -1;
                return false;
            }

            // First find the slot based on the hash
            int* buckets = (int*)data->Buckets;
            int bucket = key.GetHashCode() & data->BucketCapacityMask;
            it.EntryIndex = it.NextEntryIndex = buckets[bucket];
            return TryReplaceNextValueAtomic(data, item, ref it);
        }

        private static bool TryReplaceNextValueAtomic(NativeHashMapDataImposter* data, TValue item, ref NativeMultiHashMapIteratorImposter<TKey> it)
        {
            int entryIdx = it.NextEntryIndex;
            it.NextEntryIndex = -1;
            it.EntryIndex = -1;
            if (entryIdx < 0 || entryIdx >= data->Capacity)
            {
                return false;
            }

            int* nextPtrs = (int*)data->Next;
            while (!UnsafeUtility.ReadArrayElement<TKey>(data->Keys, entryIdx).Equals(it.key))
            {
                entryIdx = nextPtrs[entryIdx];
                if (entryIdx < 0 || entryIdx >= data->Capacity)
                {
                    return false;
                }
            }

            it.NextEntryIndex = nextPtrs[entryIdx];
            it.EntryIndex = entryIdx;

            // Write the value
            UnsafeUtility.WriteArrayElement(data->Keys, entryIdx, it.key);
            UnsafeUtility.WriteArrayElement(data->Values, entryIdx, item);
            return true;
        }
    }

NativeHashMapDataImposter

[StructLayout(LayoutKind.Sequential)]
    internal unsafe struct NativeHashMapDataImposter
    {
        public byte* Values;
        public byte* Keys;
        public byte* Next;
        public byte* Buckets;
        public int Capacity;

        public int BucketCapacityMask; // = bucket capacity - 1

        // Add padding between fields to ensure they are on separate cache-lines
        private fixed byte padding1[60];

        public fixed int FirstFreeTLS[JobsUtility.MaxJobThreadCount * IntsPerCacheLine];
        public int AllocatedIndexLength;

        // 64 is the cache line size on x86, arm usually has 32 - so it is possible to save some memory there
        public const int IntsPerCacheLine = JobsUtility.CacheLineSize / sizeof(int);


    }

NativeMultiHashMapIteratorImposter

    internal struct NativeMultiHashMapIteratorImposter<TKey>
        where TKey : struct
    {
        public TKey key;
        public int NextEntryIndex;
        public int EntryIndex;

        public static unsafe implicit operator NativeMultiHashMapIteratorImposter<TKey>(NativeMultiHashMapIterator<TKey> it)
        {
            var ptr = UnsafeUtility.AddressOf(ref it);
            UnsafeUtility.CopyPtrToStructure(ptr, out NativeMultiHashMapIteratorImposter<TKey> imposter);
            return imposter;
        }
    }
2 Likes

Yep, cool too :slight_smile: also we have third solution - just put NHM into project with in our namespace and it’s not require any wrappers or source replacement :smile:

wow, what I actually need. Does this still work?

You don’t need it now, it’s already implemented in Collections months ago.

1 Like