Lightning.NET icon indicating copy to clipboard operation
Lightning.NET copied to clipboard

MDBValue potential improvements

Open sebastienros opened this issue 1 month ago • 3 comments

Seeing your latest PR and calling AspSpan I wondered how MDBValue was designed.

Here is what Opus is suggesting:

MDBValue Performance Optimizations

This document outlines potential performance optimizations for the MDBValue struct in LightningDB using modern .NET marshalling and memory helpers.

Current Implementation

The existing MDBValue struct is a managed version of the native MDB_val type, designed to be blittable for P/Invoke marshalling.

Proposed Optimizations

1. Use MemoryMarshal for AsSpan()

The current implementation is efficient, but MemoryMarshal.CreateReadOnlySpan provides a more explicit approach:

using System. Runtime.InteropServices;

public ReadOnlySpan<byte> AsSpan() => MemoryMarshal.CreateReadOnlySpan(ref *data, (int)size);

2. Add a Writable Span Accessor

For scenarios requiring write access to the buffer:

public Span<byte> AsWritableSpan() => new Span<byte>(data, (int)size);

3. Add SkipLocalsInit Attribute

Skip zero-initialization for performance-critical structs:

using System.Runtime.CompilerServices;

[SkipLocalsInit]
public unsafe struct MDBValue

4. Add Typed Accessors with MemoryMarshal

For common use cases where the data type is known:

public T Read<T>() where T : unmanaged => MemoryMarshal. Read<T>(AsSpan());

public ReadOnlySpan<T> AsSpan<T>() where T : unmanaged => MemoryMarshal.Cast<byte, T>(AsSpan());

5. Use Unsafe.CopyBlockUnaligned for Bulk Copies

For frequent data copying scenarios:

using System.Runtime.CompilerServices;

public void CopyTo(Span<byte> destination)
{
    Unsafe.CopyBlockUnaligned(ref MemoryMarshal.GetReference(destination), ref *data, (uint)size);
}

Complete Optimized Implementation

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace LightningDB;

[SkipLocalsInit]
public unsafe struct MDBValue
{
    internal MDBValue(int bufferSize, byte* pinnedOrStackAllocBuffer)
    {
        size = bufferSize;
        data = pinnedOrStackAllocBuffer;
    }

    //DO NOT REORDER
    internal nint size;

    //DO NOT REORDER
    internal byte* data;

    [MethodImpl(MethodImplOptions. AggressiveInlining)]
    public ReadOnlySpan<byte> AsSpan() => new(data, (int)size);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public Span<byte> AsWritableSpan() => new(data, (int)size);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public T Read<T>() where T : unmanaged => MemoryMarshal.Read<T>(AsSpan());

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public ReadOnlySpan<T> Cast<T>() where T : unmanaged => MemoryMarshal.Cast<byte, T>(AsSpan());

    public byte[] CopyToNewArray() => AsSpan().ToArray();

    public void CopyTo(Span<byte> destination)
    {
        Unsafe.CopyBlockUnaligned(
            ref MemoryMarshal.GetReference(destination),
            ref *data,
            (uint)size);
    }
}

Key Benefits

Optimization Benefit
AggressiveInlining Eliminates method call overhead by hinting the JIT to inline small methods
SkipLocalsInit Avoids zero-initialization overhead (safe when fields are always assigned in constructor)
Generic typed accessors Avoids manual casting and leverages MemoryMarshal for zero-copy reinterpretation
Unsafe.CopyBlockUnaligned Can be faster than Span.CopyTo for known-size copies

Considerations

  • The struct layout must remain unchanged to maintain blittability for P/Invoke
  • SkipLocalsInit is safe here because all fields are assigned in the constructor
  • Generic methods with unmanaged constraint ensure type safety while enabling high-performance scenarios

sebastienros avatar Dec 12 '25 18:12 sebastienros

Timely issue, this was going to be my next area to look into. Thanks @sebastienros

CoreyKaylor avatar Dec 12 '25 18:12 CoreyKaylor

Hope you have some benchmarks available to test these. If you point to the repository Opus is able to read the source directly and understand the internal structures, or find gaps between the original API and the ports.

sebastienros avatar Dec 12 '25 18:12 sebastienros

I'm already using claude code locally. I'll probably make a few passes at it. I'll be adding benchmarks for the new comparer options before releasing, yes.

CoreyKaylor avatar Dec 12 '25 18:12 CoreyKaylor

Released in v0.21.0

CoreyKaylor avatar Dec 24 '25 13:12 CoreyKaylor