Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion benchmarks/PureHDF.Benchmarks/PureHDF.Benchmarks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.13-nightly.20240519.155" />
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
<PackageReference Include="Intrinsics.ISA-L.PInvoke" Version="2.30.0" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
</ItemGroup>
Expand Down
135 changes: 135 additions & 0 deletions benchmarks/PureHDF.Benchmarks/ReflectionDispatch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
using BenchmarkDotNet.Attributes;
using PureHDF;
using System.Runtime.InteropServices;

namespace Benchmark;

// Isolates the per-call dispatch cost on the three sites where reflection
// caching was added:
//
// 1. NativeAttribute.Read<T> — (TResult, TElement) reader delegate cache
// 2. NativeDataset.Read<T> — same pattern
// 3. DatatypeMessage.GetDecodeInfo<T> — closure-tree cache, plus the inner
// GetDecodeInfoForUnmanagedElement(Type) per-Type delegate cache used while
// building the compound decoder.
//
// The payload on each Read is intentionally tiny (one scalar or one small
// blittable compound), so per-call cost is dominated by the dispatch path
// being measured rather than by the actual decode work. A high iteration
// count inside each [Benchmark] method amplifies the per-call signal.
//
// Compound variants additionally exercise the static
// GetDecodeInfoForUnmanagedElement(Type) cache because the compound branch
// of BuildDecodeInfo routes the known-compound case through the Type-keyed
// overload (DatatypeMessage.Reading.cs:438).
[MemoryDiagnoser]
public class ReflectionDispatch
{
// Pack = 1 keeps the on-disk size predictable (12 B: double + float, no
// trailing pad). Matches the shape used by VariableLengthCompoundRead on
// the perf branch so numbers are comparable.
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Sample
{
public double X;
public float Y;
}

private const int Iterations = 10_000;

private string _filePath = default!;
private IDisposable _file = default!;
private IH5Dataset _scalarIntDataset = default!;
private IH5Dataset _scalarSampleDataset = default!;
private IH5Attribute _scalarIntAttribute = default!;
private IH5Attribute _scalarSampleAttribute = default!;

[GlobalSetup]
public void GlobalSetup()
{
_filePath = Path.Combine(
Path.GetTempPath(),
$"purehdf-reflection-bench-{Guid.NewGuid():N}.h5");

var writeFile = new H5File
{
["scalar_int"] = new H5Dataset(data: 42),
["scalar_sample"] = new H5Dataset(data: new Sample { X = 1.5, Y = 2.5f })
};

writeFile.Attributes["scalar_int"] = 42;
writeFile.Attributes["scalar_sample"] = new Sample { X = 1.5, Y = 2.5f };

writeFile.Write(_filePath);

var root = H5File.OpenRead(_filePath);
_file = root;

_scalarIntDataset = root.Dataset("scalar_int");
_scalarSampleDataset = root.Dataset("scalar_sample");
_scalarIntAttribute = root.Attribute("scalar_int");
_scalarSampleAttribute = root.Attribute("scalar_sample");

// Warm the per-instance / per-Type caches so the measured loop is
// steady-state cache-hit behaviour, not cold-build.
_ = _scalarIntDataset.Read<int>();
_ = _scalarSampleDataset.Read<Sample>();
_ = _scalarIntAttribute.Read<int>();
_ = _scalarSampleAttribute.Read<Sample>();
}

[GlobalCleanup]
public void GlobalCleanup()
{
_file?.Dispose();

if (File.Exists(_filePath))
{
try { File.Delete(_filePath); } catch { /* ignore */ }
}
}

[Benchmark]
public int Dataset_ReadScalarInt()
{
var total = 0;

for (var i = 0; i < Iterations; i++)
total += _scalarIntDataset.Read<int>();

return total;
}

[Benchmark]
public double Dataset_ReadScalarCompound()
{
var total = 0.0;

for (var i = 0; i < Iterations; i++)
total += _scalarSampleDataset.Read<Sample>().X;

return total;
}

[Benchmark]
public int Attribute_ReadScalarInt()
{
var total = 0;

for (var i = 0; i < Iterations; i++)
total += _scalarIntAttribute.Read<int>();

return total;
}

[Benchmark]
public double Attribute_ReadScalarCompound()
{
var total = 0.0;

for (var i = 0; i < Iterations; i++)
total += _scalarSampleAttribute.Read<Sample>().X;

return total;
}
}
50 changes: 29 additions & 21 deletions src/PureHDF/VOL/Native/API.Reading/NativeAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Reflection;
using System.Collections.Concurrent;
using System.Reflection;

namespace PureHDF.VOL.Native;

Expand All @@ -12,6 +13,29 @@ public class NativeAttribute : IH5Attribute
private static readonly MethodInfo _methodInfoReadCoreLevel1_Generic = typeof(NativeAttribute)
.GetMethod(nameof(ReadCoreLevel1_generic), BindingFlags.NonPublic | BindingFlags.Instance)!;

// Delegate type for reads, including an instance parameter.
// Statically cached keyed by (TResult, TElement).
private delegate TResult? ReaderDelegate<TResult>(
NativeAttribute @this,
TResult? buffer,
IH5ReadStream source,
ulong[]? memoryDims);

private static readonly ConcurrentDictionary<(Type, Type), Delegate> _readerCache = new();

private static ReaderDelegate<TResult> GetReader<TResult>(Type elementType)
{
return (ReaderDelegate<TResult>)_readerCache.GetOrAdd(
(typeof(TResult), elementType),
static key =>
{
var method = _methodInfoReadCoreLevel1_Generic
.MakeGenericMethod(key.Item1, key.Item2);
var delegateType = typeof(ReaderDelegate<>).MakeGenericType(key.Item1);
return method.CreateDelegate(delegateType);
});
}

private IH5Dataspace? _space;
private IH5DataType? _type;
private readonly NativeReadContext _context;
Expand Down Expand Up @@ -74,19 +98,10 @@ public T Read<T>(
ulong[]? memoryDims = null)
{
var (elementType, _) = WriteUtils.GetElementType(typeof(T));

// TODO cache this
var method = _methodInfoReadCoreLevel1_Generic.MakeGenericMethod(typeof(T), elementType);
var reader = GetReader<T>(elementType);
var source = new SystemMemoryStream(Message.InputData);

var result = (T)method.Invoke(this,
[
default /* buffer */,
source,
memoryDims
])!;

return result;
return reader(this, buffer: default, source, memoryDims)!;
}

/// <inheritdoc />
Expand All @@ -95,17 +110,10 @@ public void Read<T>(
ulong[]? memoryDims = null)
{
var (elementType, _) = WriteUtils.GetElementType(typeof(T));

// TODO cache this
var method = _methodInfoReadCoreLevel1_Generic.MakeGenericMethod(typeof(T), elementType);
var reader = GetReader<T>(elementType);
var source = new SystemMemoryStream(Message.InputData);

method.Invoke(this,
[
buffer,
source,
memoryDims
]);
reader(this, buffer, source, memoryDims);
}

/* This overload is required because Span<T> is not allowed as generic argument and
Expand Down
53 changes: 36 additions & 17 deletions src/PureHDF/VOL/Native/API.Reading/NativeDataset.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using PureHDF.Selections;
using System.Buffers;
using System.Collections.Concurrent;
using System.Reflection;

namespace PureHDF.VOL.Native;
Expand All @@ -14,6 +15,32 @@ public class NativeDataset : NativeObject, IH5Dataset
private static readonly MethodInfo _methodInfoReadCoreLevel1_Generic = typeof(NativeDataset)
.GetMethod(nameof(ReadCoreLevel1_Generic), BindingFlags.NonPublic | BindingFlags.Instance)!;

// Delegate type for reads, including an instance parameter.
// Statically cached keyed by (TResult, TElement).
private delegate TResult? ReaderDelegate<TResult>(
NativeDataset @this,
TResult? buffer,
Selection? fileSelection,
Selection? memorySelection,
ulong[]? memoryDims,
H5DatasetAccess datasetAccess,
bool skipShuffle);

private static readonly ConcurrentDictionary<(Type, Type), Delegate> _readerCache = new();

private static ReaderDelegate<TResult> GetReader<TResult>(Type elementType)
{
return (ReaderDelegate<TResult>)_readerCache.GetOrAdd(
(typeof(TResult), elementType),
static key =>
{
var method = _methodInfoReadCoreLevel1_Generic
.MakeGenericMethod(key.Item1, key.Item2);
var delegateType = typeof(ReaderDelegate<>).MakeGenericType(key.Item1);
return method.CreateDelegate(delegateType);
});
}

private IH5Dataspace? _space;
private IH5DataType? _type;
private IH5DataLayout? _layout;
Expand Down Expand Up @@ -196,21 +223,16 @@ public T Read<T>(
ulong[]? memoryDims = default)
{
var (elementType, _) = WriteUtils.GetElementType(typeof(T));
var reader = GetReader<T>(elementType);

// TODO cache this
var method = _methodInfoReadCoreLevel1_Generic.MakeGenericMethod(typeof(T), elementType);

var result = (T)method.Invoke(this,
[
default /* buffer */,
return reader(
this,
buffer: default,
fileSelection,
memorySelection,
memoryDims,
datasetAccess,
/* skip shuffle: */ false
])!;

return result;
skipShuffle: false)!;
}

/// <summary>
Expand All @@ -230,19 +252,16 @@ public void Read<T>(
ulong[]? memoryDims = default)
{
var (elementType, _) = WriteUtils.GetElementType(typeof(T));
var reader = GetReader<T>(elementType);

// TODO cache this
var method = _methodInfoReadCoreLevel1_Generic.MakeGenericMethod(typeof(T), elementType);

method.Invoke(this,
[
reader(
this,
buffer,
fileSelection,
memorySelection,
memoryDims,
datasetAccess,
/* skip shuffle: */ false
]);
skipShuffle: false);
}

/* The following two methods are required because Span<T> is not allowed as generic
Expand Down
Loading