Skip to content

Commit

Permalink
update README.md. Several bug fixes. +semver:patch
Browse files Browse the repository at this point in the history
  • Loading branch information
fryderykhuang committed Jan 20, 2024
1 parent a7ebeb1 commit 637fa1c
Show file tree
Hide file tree
Showing 33 changed files with 412 additions and 202 deletions.
64 changes: 46 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,51 @@ Currently this project contains 3 components:
2. Value type (struct) only collections
3. Linq operators

### Setup

Use this line to setup the AllocatorContext, which is used internally in `ValueArray<T>` and any other locations that need to allocate unmanaged memory.

```csharp
AllocatorContext.SetImplementation(new DefaultAllocatorContextImpl().ConfigureDefault());
```

### Memory allocation strategies

Two types of memory allocation strategy are supported:

### Arena
#### 1. Arena

```csharp
// all 'T' is struct if not specifically mentioned.
using (AllocatorContext.BeginAllocationScope())
{
var list = new ValueList<T>();
var list = new ValueList<T>(); // plain old 'new'
var dict = new ValueDictionary<T>();
var obj = new Allocated<T>(); // let struct works like a class (struct is allocated on the unmanaged heap.)
...
} // all Value* collections are automatically disposed as they go out of scope.
} // all value collections are automatically disposed as they go out of scope, no need to explicitly call Dispose().
```

### Explicit lifetime
#### 2. Explicit lifetime

You can utilize the unmanaged allocator anywhere like this (including inside of arena scope):

```csharp
// You can construct a value type collection anywhere(including inside of arena scope)
// using this overload:
var list = new ValueList<T>(AllocatorTypes.DefaultUnscoped);
// use the overload with parameter 'AllocatorTypes', then specify the unscoped, globally available allocator type.
var list = new ValueList<T>(AllocatorTypes.DefaultUnscoped);
var obj = new Allocated<T>(AllocatorTypes.DefaultUnscoped);
...
// Anywhere after sometime:
// Anywhere after usage:
list.Dispose();
```

To avoid double-free, when these collections are passed by value, Borrow() should be used. After calling Borrow(), all copies of the original collection can be safely disposed without double-free.
#### Pass by ref or by value

Since we are using struct everywhere, how to pass a struct which works like a reference type is a little bit tricky.

Under most circumstances, use `ref` modifier will be sufficient, but there's still somewhere that cannot use the `ref` modifier such as struct field (`ref` type can only be put in a `ref struct`, which is incovienient).

To avoid double-free, when those value collections are passed by value, Borrow() should be used. After calling Borrow(), all copies of the original collection can be safely disposed without double-free.

```csharp
var list = new ValueList<T>(AllocatorTypes.DefaultUnscoped);
Expand All @@ -54,10 +74,16 @@ var list = new ValueList<T>(AllocatorTypes.DefaultUnscoped);
SomeListRefConsumingMethod(in list);
SomeListRefConsumingMethod(ref list);

// value passing should call Borrow()
// value passing should call Borrow() unless you're certain the passed one will not be disposed.
SomeListConsumingMethod(list.Borrow())
```

#### 3. Interop with managed object

if you have to use managed object(classes) inside a struct, you can use
`Pinned<T>` to pin the object down so that its address is fixed and can be stored on a non-GC rooted place.


### Custom collection types

* ValueArray&lt;T&gt;
Expand All @@ -82,8 +108,8 @@ The LINQ interface has 3 variations:

```csharp
SomeCollection.LinqValue()... // Enumerate by value. All types implement IEnumerable<T> are supported
SomeCollection.LinqRef()... // Enumerate by ref. Besides built-in value typed collections, only Enumerators that exposes 'ref T Current' are supported (e.g. normal array types)
SomeCollection.LinqPtr()... // Enumerate by pointer. Only built-in value typed collections are supported.
SomeCollection.LinqRef()... // Enumerate by ref. Besides value collections, only Enumerators that exposes 'ref T Current' are supported (e.g. normal array types)
SomeCollection.LinqPtr()... // Enumerate by pointer. Only built-in value typed collections are supported. (Because object address must be fixed to be able to use unmanaged pointer)
```

Most extension methods that needs a delegate type parameter has an overloads with `in` or `ref` modifier to avoid copying too much data if the Linqed type is a big struct.
Expand All @@ -97,25 +123,27 @@ Most extension methods has overloads with a `TArg arg` parameter to avoid unnece

```csharp
TArg someTArg;
.LinqRef()...Select((in T x) => new SomeStruct(in x, someTArg))... // Everytime this line executes, a new capture object for `someTArg` must be allocated .
.LinqRef()...Select(static (in T x, TArg a) => new SomeStruct(in x, a), someTArg)... // No capture is happening.
...Select((in T x) => new SomeStruct(in x, someTArg))... // Everytime this line executes, a new capture object for `someTArg` must be allocated on the managed heap.
...Select(static (in T x, TArg a) => new SomeStruct(in x, a), someTArg)... // No capture is happening. ('static' is not mandatory, just a explicit declaration)
```

## Things to do

1. More examples.
1. More documentations.
2. Larger test coverage.
3. More collection types.
4. More LINQ providers and support range.
5. Roslyn analyzer for struct lifetime/ownership enforcing. (The actual lifetime is not being enforced, such as the early dispose from the owner side or mutation from the borrower side is still unpreventable, static analysis with attribute markers should be the way to go.)

## Thanks to

Emma Maassen from <https://github.com/Enichan/Arenas>
Angouri from <https://github.com/asc-community/HonkPerf.NET>
* Emma Maassen from <https://github.com/Enichan/Arenas>

* Angouri from <https://github.com/asc-community/HonkPerf.NET>

Details in [THIRD-PARTY-NOTICES.md](https://github.com/fryderykhuang/NullGC/blob/main/THIRD-PARTY-NOTICES.md)

## How to contribute

These framework-like projects will not become generally useful without being battle tested in real world. If your project can protentially benefit from this, feel free to submit an Issue and talk about your use case. Any type of contributions are welcomed.
Framework projects like this will not become generally useful without being battle tested in real world. If your project can protentially benefit from this library, feel free to submit an Issue and talk about your use case. Any type of contributions are welcomed.
10 changes: 6 additions & 4 deletions src/BenchmarkResultPageGenerator/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@

namespace BenchmarkResultPageGenerator;

internal class Program
internal partial class Program
{
private static readonly Regex FileNamePattern = new(@"^(\w+\.)+Benchmarks\.(?<class>\w+)\-report",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex FileNamePattern = MyRegex();

private static async Task Main(string[] args)
{
Expand All @@ -35,7 +34,10 @@ private static async Task Main(string[] args)
await File.WriteAllTextAsync(output, await (await new RazorEngine().CompileAsync(await sr.ReadToEndAsync()))
.RunAsync(new
{
Files = lst
Files = lst.OrderBy(x=>x.Title)
}));
}

[GeneratedRegex(@"^(\w+\.)+Benchmarks\.(?<class>\w+)\-report", RegexOptions.Compiled | RegexOptions.CultureInvariant)]
private static partial Regex MyRegex();
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using NullGC.Allocators;

namespace NullGC;

/// <summary>
/// A wrapper to make unmanaged struct work like a class (a pointer) without using 'ref', by allocating struct memory on unmanaged heap.
/// A wrapper to make unmanaged struct work like a class (a pointer) without using 'ref', by allocating struct on unmanaged heap.
/// </summary>
/// <typeparam name="T"></typeparam>
public readonly struct Class<T> : IDisposable where T : unmanaged
/// <remarks>
/// <see cref="Borrow"/> is needed when passed by value to avoid double-free.
/// </remarks>
public readonly struct Allocated<T> : ISingleDisposable<Allocated<T>> where T : unmanaged
{
public readonly unsafe T* Value;
public readonly int AllocatorProviderId;

public Class(int allocatorProviderId = (int) AllocatorTypes.Default)
private unsafe Allocated(T* value, int allocatorProviderId)
{
Value = value;
AllocatorProviderId = allocatorProviderId;
}

public Allocated(AllocatorTypes allocatorProviderId) : this((int) allocatorProviderId)
{
}

public Allocated(int allocatorProviderId = (int) AllocatorTypes.Default)
{
AllocatorProviderId = allocatorProviderId;
unsafe
Expand All @@ -31,13 +43,13 @@ public Class(int allocatorProviderId = (int) AllocatorTypes.Default)
/// Allocate memory for <typeparamref name="T"/> from default scoped allocator, dispose is not mandatory.
/// </summary>
/// <returns></returns>
public static Class<T> CreateScoped() => new((int) AllocatorTypes.Default);
public static Allocated<T> CreateScoped() => new((int) AllocatorTypes.Default);

/// <summary>
/// Allocate memory for <typeparamref name="T"/> from default unscoped allocator, dispose is mandatory when lifetime ends.
/// </summary>
/// <returns></returns>
public static Class<T> CreateUnscoped() => new((int) AllocatorTypes.DefaultUnscoped);
public static Allocated<T> CreateUnscoped() => new((int) AllocatorTypes.DefaultUnscoped);

public ref T Ref
{
Expand All @@ -51,11 +63,19 @@ public ref T Ref
}
}

public Allocated<T> Borrow()
{
unsafe
{
return new Allocated<T>(Value, AllocatorProviderId);
}
}

public void Dispose()
{
if (AllocatorProviderId == (int) AllocatorTypes.Invalid)
return;

unsafe
{
AllocatorContext.GetAllocator(AllocatorProviderId).Free((UIntPtr) Value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
/// A marker interface denotes that the implemented type contains only objects that have a fixed memory address.
/// </summary>
/// <remarks>
/// Currently used in Linq implementation to enable faster algorithms on native-heap allocated objects.
/// Currently used in Linq implementation to enable faster algorithms on unmanaged-heap-allocated objects.
/// </remarks>
public interface IItemAddressFixed
public interface IAddressFixed
{
}
5 changes: 4 additions & 1 deletion src/NullGC.Abstractions/ISingleDisposable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
public interface ISingleDisposable<out T> : IDisposable where T : struct, IDisposable
{
/// <summary>
/// Get a copy of the implementer itself that will not cause double-dispose problem (e.g. by set a non-dispose flag).
/// Get a copy of the implementer itself that will not cause double-free problem (e.g. by set a non-dispose flag).
/// </summary>
/// <returns></returns>
/// <remarks>
/// Since this is not a built-in lifetime/ownership management system, the actual lifetime is not being enforced, the early dispose from the owner side or mutation from the borrower side is still unpreventable thus should be used with caution.
/// </remarks>
T Borrow();
}
2 changes: 1 addition & 1 deletion src/NullGC.Abstractions/Linq/ILinqPtrEnumerator.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace NullGC.Linq;

// public interface ILinqPtrEnumerator<T> : ILinqEnumerator<T>, IItemAddressFixed
// public interface ILinqPtrEnumerator<T> : ILinqEnumerator<T>, IAddressFixed
// {
// unsafe T* Current { get; }
// }
2 changes: 1 addition & 1 deletion src/NullGC.Abstractions/Linq/LinqFixedRefEnumerable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
namespace NullGC.Linq;

public readonly struct LinqFixedRefEnumerable<T, TEnumerator> : ILinqEnumerable<T, TEnumerator>
where TEnumerator : struct, ILinqRefEnumerator<T>, IItemAddressFixed
where TEnumerator : struct, ILinqRefEnumerator<T>, IAddressFixed
{
private readonly TEnumerator _enumerator;

Expand Down
26 changes: 19 additions & 7 deletions src/NullGC.Abstractions/Pinned.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
#pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type

namespace NullGC;

Expand All @@ -10,21 +11,23 @@ namespace NullGC;
/// <remarks>
/// Since pin a managed object on the GC heap will reduce the effectiveness of GC compaction, either use and dispose in a short time or pin as early in the application lifetime as possible.
/// </remarks>
public struct Pinned<T> : IDisposable where T : class
public struct Pinned<T> : IAddressFixed, ISingleDisposable<Pinned<T>> where T : class
{
#pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type
public readonly unsafe T* Ptr;
#pragma warning restore CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type
private GCHandle _pin;

private unsafe Pinned(T* ptr)
{
Ptr = ptr;
_pin = default;
}

public Pinned(T obj)
{
unsafe
{
_pin = GCHandle.Alloc(obj, GCHandleType.Pinned);
#pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type
Ptr = (T*) Unsafe.AsPointer(ref obj);
#pragma warning restore CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type
Ptr = (T*) _pin.AddrOfPinnedObject();
}
}

Expand All @@ -39,8 +42,17 @@ public ref T Ref
}
}

public Pinned<T> Borrow()
{
unsafe
{
return new Pinned<T>(Ptr);
}
}

public void Dispose()
{
_pin.Free();
if (_pin.IsAllocated)
_pin.Free();
}
}
17 changes: 16 additions & 1 deletion src/NullGC.Collections/SlidingTimeWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace NullGC.Collections;

public struct SlidingTimeWindow<T> : IDisposable where T : unmanaged, INumber<T>
public struct SlidingTimeWindow<T> : ISingleDisposable<SlidingTimeWindow<T>> where T : unmanaged, INumber<T>
{
private ValueFixedSizeDeque<Bucket> _wnd;

Expand Down Expand Up @@ -44,6 +44,16 @@ internal Bucket(T sum)
public int Count => _count;
}

private SlidingTimeWindow(ValueFixedSizeDeque<Bucket> wnd, int resolutionMs, long lastBucket, T wndSum,
int wndCount, Bucket noDataBucket)
{
_wnd = wnd;
_resolutionMs = resolutionMs;
_lastBucket = lastBucket;
_wndSum = wndSum;
_wndCount = wndCount;
_noDataBucket = noDataBucket;
}

/// <summary>
/// </summary>
Expand Down Expand Up @@ -108,6 +118,11 @@ public CurrentWindow Update(T newItem)
return new CurrentWindow(ref _wnd, _wndSum, _wndCount);
}

public SlidingTimeWindow<T> Borrow()
{
return new SlidingTimeWindow<T>(_wnd.Borrow(), _resolutionMs, _lastBucket, _wndSum, _wndCount, _noDataBucket);
}

public void Dispose()
{
_wnd.Dispose();
Expand Down
2 changes: 1 addition & 1 deletion src/NullGC.Collections/UnsafeArrayEnumerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace NullGC.Collections;

public struct UnsafeArrayEnumerator<T> : ILinqRefEnumerator<T>, ILinqValueEnumerator<T>, IUnsafeArray<T>, IItemAddressFixed where T : unmanaged
public struct UnsafeArrayEnumerator<T> : ILinqRefEnumerator<T>, ILinqValueEnumerator<T>, IUnsafeArray<T>, IAddressFixed where T : unmanaged
{
private readonly unsafe T* _items;
private readonly int _length;
Expand Down
Loading

0 comments on commit 637fa1c

Please sign in to comment.