Skip to content

Commit

Permalink
moving all experiments from the Benchmarks to Experiments project and…
Browse files Browse the repository at this point in the history
… splitting their tests into UnitTests project
  • Loading branch information
dadhi committed Feb 21, 2023
1 parent 0defe27 commit 9039117
Show file tree
Hide file tree
Showing 30 changed files with 571 additions and 629 deletions.
7 changes: 7 additions & 0 deletions ImTools.sln
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImTools.SimpleDIPlayground"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "ImTools.UnionCsVsFsBenchmarks", "playground\ImTools.UnionCsVsFsBenchmarks\ImTools.UnionCsVsFsBenchmarks.fsproj", "{239B6CA0-2860-4A67-859F-FD4EF6DAA547}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Experiments", "playground\Experiments\Experiments.csproj", "{88D8A585-1936-41DC-9A6E-74589EB6EA97}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -82,6 +84,10 @@ Global
{239B6CA0-2860-4A67-859F-FD4EF6DAA547}.Debug|Any CPU.Build.0 = Debug|Any CPU
{239B6CA0-2860-4A67-859F-FD4EF6DAA547}.Release|Any CPU.ActiveCfg = Release|Any CPU
{239B6CA0-2860-4A67-859F-FD4EF6DAA547}.Release|Any CPU.Build.0 = Release|Any CPU
{88D8A585-1936-41DC-9A6E-74589EB6EA97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{88D8A585-1936-41DC-9A6E-74589EB6EA97}.Debug|Any CPU.Build.0 = Debug|Any CPU
{88D8A585-1936-41DC-9A6E-74589EB6EA97}.Release|Any CPU.ActiveCfg = Release|Any CPU
{88D8A585-1936-41DC-9A6E-74589EB6EA97}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -93,6 +99,7 @@ Global
{EFBD055A-3C8A-45C7-8718-D9C53A66521D} = {D109DF34-CB08-42E5-8F8F-CD2D4D4A8DB3}
{60B58862-31B3-48DC-BFF3-42A9B0A194A5} = {D109DF34-CB08-42E5-8F8F-CD2D4D4A8DB3}
{239B6CA0-2860-4A67-859F-FD4EF6DAA547} = {D109DF34-CB08-42E5-8F8F-CD2D4D4A8DB3}
{88D8A585-1936-41DC-9A6E-74589EB6EA97} = {D109DF34-CB08-42E5-8F8F-CD2D4D4A8DB3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B8B8E41A-6F08-46C3-828D-C74B91428793}
Expand Down
11 changes: 11 additions & 0 deletions playground/Experiments/Experiments.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net7.0;net6.0;net472</TargetFrameworks>
<NoWarn>1701;1702;AD0001;NU1608</NoWarn>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\ImTools\ImTools.csproj" />
</ItemGroup>
</Project>
217 changes: 217 additions & 0 deletions playground/Experiments/HashArrayMappedTrie.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
using System;
using System.Collections.Generic;

namespace ImTools.UnitTests.Playground
{
public delegate T UpdateMethod<T>(T old, T newOne);

/// <summary>
/// Immutable Hash Array Mapped Trie (http://en.wikipedia.org/wiki/Hash_array_mapped_trie)
/// similar to the one described at http://lampwww.epfl.ch/papers/idealhashtrees.pdf.
/// It is basically a http://en.wikipedia.org/wiki/Trie built on hash chunks. It provides O(1) access-time and
/// does not require self-balancing. The maximum number of tree levels would be (32 bits of hash / 5 bits level chunk = 7).
/// In addition it is space efficient and requires single integer (to store index bitmap) per 1 to 32 values.
/// TODO: ? Optimize get/add speed with mutable sparse array (for insert) at root level. That safe cause bitmapIndex will Not see new inserted values.
/// </summary>
/// <typeparam name="V">Type of value stored in trie.</typeparam>
public sealed class HashTrie<V>
{
public static readonly HashTrie<V> Empty = new HashTrie<V>();

public bool IsEmpty => _indexBitmap == 0;

public HashTrie<V> AddOrUpdate(int hash, V value, UpdateMethod<V> updateValue = null)
{
var index = hash & LEVEL_MASK; // index from 0 to 31
var restOfHash = hash >> LEVEL_BITS;
if (_indexBitmap == 0)
return new HashTrie<V>(1u << index, restOfHash == 0 ? (object)value : Empty.AddOrUpdate(restOfHash, value));

var nodeCount = _nodes.Length;

var pastIndexBitmap = _indexBitmap >> index;
if ((pastIndexBitmap & 1) == 0) // no nodes at the index, could be inserted.
{
var subnode = restOfHash == 0 ? (object)value : Empty.AddOrUpdate(restOfHash, value);

var pastIndexCount = pastIndexBitmap == 0 ? 0 : GetSetBitsCount(pastIndexBitmap);
var insertIndex = nodeCount - pastIndexCount;

var nodesToInsert = new object[nodeCount + 1];
if (insertIndex != 0)
Array.Copy(_nodes, 0, nodesToInsert, 0, insertIndex);
nodesToInsert[insertIndex] = subnode;
if (pastIndexCount != 0)
Array.Copy(_nodes, insertIndex, nodesToInsert, insertIndex + 1, pastIndexCount);

return new HashTrie<V>(_indexBitmap | (1u << index), nodesToInsert);
}

var updateIndex = nodeCount == 1 ? 0
: nodeCount - (pastIndexBitmap == 1 ? 1 : GetSetBitsCount(pastIndexBitmap));

var updatedNode = _nodes[updateIndex];
if (updatedNode is HashTrie<V>)
updatedNode = ((HashTrie<V>)updatedNode).AddOrUpdate(restOfHash, value, updateValue);
else if (restOfHash != 0) // if we need to update value with node we will move value down to new node sub-nodes at index 0.
updatedNode = new HashTrie<V>(1u, updatedNode).AddOrUpdate(restOfHash, value, updateValue);
else // here the actual update should go, cause old and new nodes contain values.
updatedNode = updateValue == null ? value : updateValue((V)updatedNode, value);

var nodesToUpdate = new object[nodeCount];
if (nodesToUpdate.Length > 1)
Array.Copy(_nodes, 0, nodesToUpdate, 0, nodesToUpdate.Length);
nodesToUpdate[updateIndex] = updatedNode;

return new HashTrie<V>(_indexBitmap, nodesToUpdate);
}

public V GetValueOrDefault(int hash, V defaultValue = default(V))
{
var node = this;
var pastIndexBitmap = node._indexBitmap >> (hash & LEVEL_MASK);
while ((pastIndexBitmap & 1) == 1)
{
var subnode = node._nodes[
node._nodes.Length - (pastIndexBitmap == 1 ? 1 : GetSetBitsCount(pastIndexBitmap))];

hash >>= LEVEL_BITS;
if (!(subnode is HashTrie<V>)) // reached the leaf value node
return hash == 0 ? (V)subnode : defaultValue;

node = (HashTrie<V>)subnode;
pastIndexBitmap = node._indexBitmap >> (hash & LEVEL_MASK);
}

return defaultValue;
}

public IEnumerable<V> Enumerate()
{
for (var i = 0; i < _nodes.Length; --i)
{
var n = _nodes[i];
if (n is HashTrie<V> trie)
foreach (var subnode in trie.Enumerate())
yield return subnode;
else
yield return (V)n;
}
}

#region Implementation

private const int LEVEL_MASK = 31; // Hash mask to find hash part on each trie level.
private const int LEVEL_BITS = 5; // Number of bits from hash corresponding to one level.

private readonly object[] _nodes; // Up to 32 nodes: sub nodes or values.
private readonly uint _indexBitmap; // Bits indicating nodes at what index are in use.

private HashTrie() { }

private HashTrie(uint indexBitmap, params object[] nodes)
{
_nodes = nodes;
_indexBitmap = indexBitmap;
}

// Variable-precision SWAR algorithm http://playingwithpointers.com/swar.html
// Fastest compared to the rest (but did not check pre-computed WORD counts): http://gurmeet.net/puzzles/fast-bit-counting-routines/
private static uint GetSetBitsCount(uint n)
{
n = n - ((n >> 1) & 0x55555555);
n = (n & 0x33333333) + ((n >> 2) & 0x33333333);
return (((n + (n >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24;
}

#endregion
}

public sealed class HashTrie<K, V>
{
public static readonly HashTrie<K, V> Empty = new HashTrie<K, V>(HashTrie<KV<K, V>>.Empty, null);

public HashTrie<K, V> AddOrUpdate(K key, V value)
{
return new HashTrie<K, V>(
_root.AddOrUpdate(key.GetHashCode(), new KV<K, V>(key, value), UpdateConflicts),
_updateValue);
}

public V GetValueOrDefault(K key, V defaultValue = default(V))
{
var kv = _root.GetValueOrDefault(key.GetHashCode());
return kv != null && (ReferenceEquals(key, kv.Key) || key.Equals(kv.Key))
? kv.Value : GetConflictedOrDefault(kv, key, defaultValue);
}

public IEnumerable<KV<K, V>> Enumerate()
{
foreach (var kv in _root.Enumerate())
{
yield return kv;
if (kv is KVWithConflicts conflicts)
foreach (var conflict in conflicts.Conflicts)
yield return conflict;
}
}

#region Implementation

private readonly HashTrie<KV<K, V>> _root;
private readonly UpdateMethod<V> _updateValue;

private HashTrie(HashTrie<KV<K, V>> root, UpdateMethod<V> updateValue)
{
_root = root;
_updateValue = updateValue;
}

private KV<K, V> UpdateConflicts(KV<K, V> old, KV<K, V> newOne)
{
var conflicts = old is KVWithConflicts withConflicts ? withConflicts.Conflicts : null;
if (ReferenceEquals(old.Key, newOne.Key) || old.Key.Equals(newOne.Key))
return conflicts == null
? UpdateValue(old, newOne)
: new KVWithConflicts(UpdateValue(old, newOne), conflicts);

if (conflicts == null)
return new KVWithConflicts(old, new[] { newOne });

var i = conflicts.Length - 1;
while (i >= 0 && !Equals(conflicts[i].Key, newOne.Key)) --i;
if (i != -1) newOne = UpdateValue(old, newOne);
return new KVWithConflicts(old, conflicts.AppendOrUpdate(newOne, i));
}

private KV<K, V> UpdateValue(KV<K, V> existing, KV<K, V> added)
{
return _updateValue == null
? added
: new KV<K, V>(existing.Key, _updateValue(existing.Value, added.Value));
}

private static V GetConflictedOrDefault(KV<K, V> item, K key, V defaultValue = default(V))
{
var conflicts = item is KVWithConflicts ? ((KVWithConflicts)item).Conflicts : null;
if (conflicts != null)
for (var i = 0; i < conflicts.Length; i++)
if (Equals(conflicts[i].Key, key))
return conflicts[i].Value;
return defaultValue;
}

private sealed class KVWithConflicts : KV<K, V>
{
public readonly KV<K, V>[] Conflicts;

public KVWithConflicts(KV<K, V> kv, KV<K, V>[] conflicts)
: base(kv.Key, kv.Value)
{
Conflicts = conflicts;
}
}

#endregion
}
}
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using NUnit.Framework;

namespace ImTools.V2
{
Expand Down Expand Up @@ -100,114 +99,4 @@ public Tree(int length, ImMap<object> tree)

#endregion
}

[TestFixture]
public class ImTreeArrayTests
{
[Test]
public void Append_to_end()
{
var store = ImArray<string>.Empty;
store = store
.Append("a")
.Append("b")
.Append("c")
.Append("d");

Assert.AreEqual("d", store.Get(3));
Assert.AreEqual("c", store.Get(2));
Assert.AreEqual("b", store.Get(1));
Assert.AreEqual("a", store.Get(0));
}

[Test]
public void Indexed_store_get_or_add()
{
var store = ImArray<string>.Empty;

store = store
.Append("a")
.Append("b")
.Append("c")
.Append("d");

var i = store.Length - 1;

Assert.AreEqual("d", store.Get(i));
}

[Test]
public void IndexOf_with_empty_store()
{
var store = ImArray<string>.Empty;

Assert.AreEqual(-1, store.IndexOf("a"));
}

[Test]
public void IndexOf_non_existing_item()
{
var store = ImArray<string>.Empty;

store = store.Append("a");

Assert.AreEqual(-1, store.IndexOf("b"));
}

[Test]
public void IndexOf_existing_item()
{
var store = ImArray<string>.Empty;

store = store
.Append("a")
.Append("b")
.Append("c");

Assert.AreEqual(1, store.IndexOf("b"));
}

[Test]
public void Append_for_full_node_and_get_node_last_item()
{
var nodeArrayLength = ImArray<int>.NODE_ARRAY_SIZE;
var array = ImArray<int>.Empty;
for (var i = 0; i <= nodeArrayLength; i++)
array = array.Append(i);

var item = array.Get(nodeArrayLength);

Assert.That(item, Is.EqualTo(nodeArrayLength));
}

/// <remarks>Issue #17 Append-able Array stops to work over 64 elements. (dev. branch)</remarks>
[Test]
public void Append_and_get_items_in_multiple_node_array()
{
var list = new List<Foo>();
var array = ImArray<Foo>.Empty;

for (var index = 0; index < 129; ++index)
{
var item = new Foo { Index = index };

list.Add(item);
array = array.Append(item);
}

for (var index = 0; index < list.Count; ++index)
{
var listItem = list[index];
var arrayItem = array.Get(index);

Assert.AreEqual(index, listItem.Index);
Assert.AreEqual(index, ((Foo)arrayItem).Index);
}
}

class Foo
{
public int Index;
}
}
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading;

namespace ImTools
namespace ImTools.Experiments
{
// todo: @perf improve the performance by storing the hash, especially when we expanding the hash map?
public struct RefKeyValue<K, V> where K : class
Expand All @@ -16,6 +12,8 @@ public RefKeyValue(K key, V value)
}
}

public struct X<T> {}

public struct RefEqHashMap<K, V> where K : class
{
//todo: @wip can we put first N slot on the stack here like in `ImTools.MapStack`
Expand Down
Loading

0 comments on commit 9039117

Please sign in to comment.