Add profiler mode & optimize AcBinary string interning

- Add "profiler" mode for memory profiling AcBinary serialization
- Reduce warmup iterations from 10 to 5 for faster benchmarks
- Save large test binary output to separate .output file (hex dump)
- Improve robustness of AcBinary vs MessagePack result comparison
- Use DisplayName for test data in result output for clarity
- Optimize AcBinary string interning: use single contiguous buffer
- Update WriteFooterStrings to avoid per-string allocations
- Clarify WithoutReferenceHandling() disables string interning for speed
This commit is contained in:
Loretta 2026-01-24 13:18:33 +01:00
parent 6df5c53937
commit 145cc0a493
3 changed files with 210 additions and 72 deletions

View File

@ -43,7 +43,7 @@ public static class Program
private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
private static int WarmupIterations = 10;
private static int WarmupIterations = 5;
private static int TestIterations = 1000;
public static void Main(string[] args)
@ -55,10 +55,17 @@ public static class Program
if (mode == "quick")
{
WarmupIterations = 10;
WarmupIterations = 5;
TestIterations = 100;
mode = "all";
}
// Profiler mode: warmup only, then exit (for memory profiler analysis)
if (mode == "profiler")
{
RunProfilerMode();
return;
}
System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════╗");
System.Console.WriteLine("║ COMPREHENSIVE SERIALIZER BENCHMARK SUITE ║");
@ -90,6 +97,47 @@ public static class Program
System.Console.WriteLine("\n✓ Benchmark complete!");
}
/// <summary>
/// Profiler mode: warmup only, then EXIT immediately.
/// Usage: dotnet run -- profiler
/// </summary>
private static void RunProfilerMode()
{
System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════╗");
System.Console.WriteLine("║ PROFILER MODE (AcBinary only) ║");
System.Console.WriteLine("╚══════════════════════════════════════════════════════════════════════╝");
System.Console.WriteLine($"Build: {BuildConfiguration} | .NET: {Environment.Version}");
System.Console.WriteLine();
// Create medium test data
TestDataFactory.ResetIdCounter();
var sharedTag = TestDataFactory.CreateTag("SharedTag");
var sharedUser = TestDataFactory.CreateUser("shareduser");
var order = TestDataFactory.CreateOrder(
itemCount: 3,
palletsPerItem: 3,
measurementsPerPallet: 3,
pointsPerMeasurement: 4,
sharedTag: sharedTag,
sharedUser: sharedUser);
var options = AcBinarySerializerOptions.WithoutReferenceHandling();
// Warmup (fills caches)
System.Console.WriteLine("Warming up (10 iterations)...");
for (var i = 0; i < 10; i++)
{
_ = AcBinarySerializer.Serialize(order, options);
}
System.Console.WriteLine("Warmup complete. Caches are now populated.");
System.Console.WriteLine();
System.Console.WriteLine(">>> ATTACH MEMORY PROFILER NOW <<<");
System.Console.WriteLine("Press any key to exit...");
System.Console.ReadKey(intercept: true);
System.Console.WriteLine();
System.Console.WriteLine("✓ Profiler mode complete. Exiting now.");
}
#region Test Data Creation
@ -766,28 +814,49 @@ public static class Program
System.Console.WriteLine($"{"Fastest Round-trip",-20} │ {fastestRt.Name,-25} │ {fastestRt.AvgTime,15:F2} ms");
// Overall AcBinary Default vs MessagePack comparison
var msgPackAvgSer = results.Where(r => r.SerializerName == SerializerMessagePack && r.SerializeTimeMs > 0).Average(r => r.SerializeTimeMs);
var msgPackAvgDes = results.Where(r => r.SerializerName == SerializerMessagePack && r.DeserializeTimeMs > 0).Average(r => r.DeserializeTimeMs);
var msgPackAvgRt = results.Where(r => r.SerializerName == SerializerMessagePack && r.RoundTripTimeMs > 0).Average(r => r.RoundTripTimeMs);
var msgPackSerResults = results.Where(r => r.SerializerName == SerializerMessagePack && r.SerializeTimeMs > 0).ToList();
var msgPackDesResults = results.Where(r => r.SerializerName == SerializerMessagePack && r.DeserializeTimeMs > 0).ToList();
var msgPackRtResults = results.Where(r => r.SerializerName == SerializerMessagePack && r.RoundTripTimeMs > 0).ToList();
var acBinarySerResults = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.SerializeTimeMs > 0).ToList();
var acBinaryDesResults = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.DeserializeTimeMs > 0).ToList();
var acBinaryRtResults = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.RoundTripTimeMs > 0).ToList();
// Skip comparison if no data available
if (msgPackRtResults.Count == 0 || acBinaryRtResults.Count == 0)
{
System.Console.WriteLine();
System.Console.WriteLine($"── {SerializerAcBinaryDefault} vs {SerializerMessagePack} (Overall) ──");
System.Console.WriteLine(" (Comparison requires both serialize and deserialize data)");
return;
}
var msgPackAvgSer = msgPackSerResults.Count > 0 ? msgPackSerResults.Average(r => r.SerializeTimeMs) : 0;
var msgPackAvgDes = msgPackDesResults.Average(r => r.DeserializeTimeMs);
var msgPackAvgRt = msgPackRtResults.Average(r => r.RoundTripTimeMs);
var msgPackAvgSize = results.Where(r => r.SerializerName == SerializerMessagePack).Average(r => r.SerializedSize);
var acBinaryAvgSer = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.SerializeTimeMs > 0).Average(r => r.SerializeTimeMs);
var acBinaryAvgDes = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.DeserializeTimeMs > 0).Average(r => r.DeserializeTimeMs);
var acBinaryAvgRt = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.RoundTripTimeMs > 0).Average(r => r.RoundTripTimeMs);
var acBinaryAvgSer = acBinarySerResults.Count > 0 ? acBinarySerResults.Average(r => r.SerializeTimeMs) : 0;
var acBinaryAvgDes = acBinaryDesResults.Average(r => r.DeserializeTimeMs);
var acBinaryAvgRt = acBinaryRtResults.Average(r => r.RoundTripTimeMs);
var acBinaryAvgSize = results.Where(r => r.SerializerName == SerializerAcBinaryDefault).Average(r => r.SerializedSize);
System.Console.WriteLine();
System.Console.WriteLine($"── {SerializerAcBinaryDefault} vs {SerializerMessagePack} (Overall) ──");
var serPctAll = (acBinaryAvgSer / msgPackAvgSer - 1) * 100;
// Only show serialize comparison if data available
if (msgPackAvgSer > 0 && acBinaryAvgSer > 0)
{
var serPctAll = (acBinaryAvgSer / msgPackAvgSer - 1) * 100;
System.Console.ForegroundColor = serPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
System.Console.WriteLine($" Serialize: {serPctAll:+0;-0}% ({acBinaryAvgSer:F2} ms vs {msgPackAvgSer:F2} ms)");
System.Console.ResetColor();
}
var desPctAll = (acBinaryAvgDes / msgPackAvgDes - 1) * 100;
var rtPctAll = (acBinaryAvgRt / msgPackAvgRt - 1) * 100;
var sizePctAll = (acBinaryAvgSize / msgPackAvgSize - 1) * 100;
System.Console.ForegroundColor = serPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
System.Console.WriteLine($" Serialize: {serPctAll:+0;-0}% ({acBinaryAvgSer:F2} ms vs {msgPackAvgSer:F2} ms)");
System.Console.ResetColor();
System.Console.ForegroundColor = desPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
System.Console.WriteLine($" Deserialize: {desPctAll:+0;-0}% ({acBinaryAvgDes:F2} ms vs {msgPackAvgDes:F2} ms)");
System.Console.ResetColor();
@ -806,9 +875,33 @@ public static class Program
Directory.CreateDirectory(ResultsDirectory);
var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
var fileName = $"Console.FullBenchmark_{BuildConfiguration}_{timestamp}.log";
var filePath = Path.Combine(ResultsDirectory, fileName);
var baseFileName = $"Console.FullBenchmark_{BuildConfiguration}_{timestamp}";
var logFilePath = Path.Combine(ResultsDirectory, $"{baseFileName}.log");
var outputFilePath = Path.Combine(ResultsDirectory, $"{baseFileName}.output");
// Save binary output to separate .output file
var largeTestData = testDataSets.FirstOrDefault(t => t.Name.StartsWith("Large"));
if (largeTestData != null)
{
var outputSb = new StringBuilder();
outputSb.AppendLine("╔══════════════════════════════════════════════════════════════════════════════════════════════════════╗");
outputSb.AppendLine("║ SERIALIZED BINARY OUTPUT ║");
outputSb.AppendLine($"║ Generated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}".PadRight(100) + "║");
outputSb.AppendLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝");
outputSb.AppendLine();
outputSb.AppendLine("=== SERIALIZED BYTES: Large (5x5x5x10) - AcBinary (Default) ===");
var serializedBytes = AcBinarySerializer.Serialize(largeTestData.Order, AcBinarySerializerOptions.Default);
outputSb.AppendLine($"Size: {serializedBytes.Length:N0} bytes");
outputSb.AppendLine();
outputSb.AppendLine("Hex dump:");
outputSb.AppendLine(FormatHexDump(serializedBytes));
File.WriteAllText(outputFilePath, outputSb.ToString(), Utf8NoBom);
System.Console.WriteLine($"✓ Binary output saved to: {outputFilePath}");
}
// Save benchmark results to .log file
var sb = new StringBuilder();
sb.AppendLine("╔══════════════════════════════════════════════════════════════════════════════════════════════════════╗");
sb.AppendLine("║ SERIALIZER BENCHMARK RESULTS ║");
@ -818,25 +911,12 @@ public static class Program
sb.AppendLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝");
sb.AppendLine();
// Serialized bytes for Large test data (AcBinary Default)
var largeTestData = testDataSets.FirstOrDefault(t => t.Name.StartsWith("Large"));
if (largeTestData != null)
{
sb.AppendLine("=== SERIALIZED BYTES: Large (5x5x5x10) - AcBinary (Default) ===");
var serializedBytes = AcBinarySerializer.Serialize(largeTestData.Order, AcBinarySerializerOptions.Default);
sb.AppendLine($"Size: {serializedBytes.Length:N0} bytes");
sb.AppendLine();
sb.AppendLine("Hex dump:");
sb.AppendLine(FormatHexDump(serializedBytes));
sb.AppendLine();
}
// CSV-like data for easy import
sb.AppendLine("=== RAW DATA (CSV) ===");
sb.AppendLine("TestData,Serializer,Size,SerializeMs,DeserializeMs,RoundTripMs");
foreach (var testData in testDataSets)
{
var testResults = results.Where(r => r.TestDataName == testData.Name).ToList();
var testResults = results.Where(r => r.TestDataName == testData.DisplayName).ToList();
foreach (var result in testResults)
{
sb.AppendLine($"{result.TestDataName},{result.SerializerName},{result.SerializedSize},{result.SerializeTimeMs:F2},{result.DeserializeTimeMs:F2},{result.RoundTripTimeMs:F2}");
@ -851,12 +931,12 @@ public static class Program
foreach (var testData in testDataSets)
{
var testResults = results.Where(r => r.TestDataName == testData.Name).OrderBy(r => r.RoundTripTimeMs).ToList();
var testResults = results.Where(r => r.TestDataName == testData.DisplayName).OrderBy(r => r.RoundTripTimeMs).ToList();
var msgPackResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerMessagePack);
var acBinaryResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerAcBinaryDefault);
sb.AppendLine();
sb.AppendLine($"--- {testData.Name} ---");
sb.AppendLine($"--- {testData.DisplayName} ---");
sb.AppendLine($"{"#",-4} {"Serializer",-26} {"Size",-12} {"Serialize",-14} {"Deserialize",-14} {"Round-trip",-14}");
sb.AppendLine(new string('-', 86));
@ -890,23 +970,41 @@ public static class Program
sb.AppendLine();
sb.AppendLine($"=== {SerializerAcBinaryDefault} vs {SerializerMessagePack} (Overall) ===");
var msgPackAvgSer = results.Where(r => r.SerializerName == SerializerMessagePack && r.SerializeTimeMs > 0).Average(r => r.SerializeTimeMs);
var msgPackAvgDes = results.Where(r => r.SerializerName == SerializerMessagePack && r.DeserializeTimeMs > 0).Average(r => r.DeserializeTimeMs);
var msgPackAvgRt = results.Where(r => r.SerializerName == SerializerMessagePack && r.RoundTripTimeMs > 0).Average(r => r.RoundTripTimeMs);
var msgPackAvgSize = results.Where(r => r.SerializerName == SerializerMessagePack).Average(r => r.SerializedSize);
var msgPackSerResults2 = results.Where(r => r.SerializerName == SerializerMessagePack && r.SerializeTimeMs > 0).ToList();
var msgPackDesResults2 = results.Where(r => r.SerializerName == SerializerMessagePack && r.DeserializeTimeMs > 0).ToList();
var msgPackRtResults2 = results.Where(r => r.SerializerName == SerializerMessagePack && r.RoundTripTimeMs > 0).ToList();
var acBinaryAvgSer = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.SerializeTimeMs > 0).Average(r => r.SerializeTimeMs);
var acBinaryAvgDes = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.DeserializeTimeMs > 0).Average(r => r.DeserializeTimeMs);
var acBinaryAvgRt = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.RoundTripTimeMs > 0).Average(r => r.RoundTripTimeMs);
var acBinaryAvgSize = results.Where(r => r.SerializerName == SerializerAcBinaryDefault).Average(r => r.SerializedSize);
var acBinarySerResults2 = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.SerializeTimeMs > 0).ToList();
var acBinaryDesResults2 = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.DeserializeTimeMs > 0).ToList();
var acBinaryRtResults2 = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.RoundTripTimeMs > 0).ToList();
sb.AppendLine($" Serialize: {((acBinaryAvgSer / msgPackAvgSer - 1) * 100):+0;-0}% ({acBinaryAvgSer:F2} ms vs {msgPackAvgSer:F2} ms)");
sb.AppendLine($" Deserialize: {((acBinaryAvgDes / msgPackAvgDes - 1) * 100):+0;-0}% ({acBinaryAvgDes:F2} ms vs {msgPackAvgDes:F2} ms)");
sb.AppendLine($" Round-trip: {((acBinaryAvgRt / msgPackAvgRt - 1) * 100):+0;-0}% ({acBinaryAvgRt:F2} ms vs {msgPackAvgRt:F2} ms)");
sb.AppendLine($" Size: {((acBinaryAvgSize / msgPackAvgSize - 1) * 100):+0;-0}% ({acBinaryAvgSize:F0} B vs {msgPackAvgSize:F0} B)");
if (msgPackSerResults2.Count > 0 && acBinarySerResults2.Count > 0)
{
var msgPackAvgSer2 = msgPackSerResults2.Average(r => r.SerializeTimeMs);
var acBinaryAvgSer2 = acBinarySerResults2.Average(r => r.SerializeTimeMs);
sb.AppendLine($" Serialize: {((acBinaryAvgSer2 / msgPackAvgSer2 - 1) * 100):+0;-0}% ({acBinaryAvgSer2:F2} ms vs {msgPackAvgSer2:F2} ms)");
}
if (msgPackDesResults2.Count > 0 && acBinaryDesResults2.Count > 0)
{
var msgPackAvgDes2 = msgPackDesResults2.Average(r => r.DeserializeTimeMs);
var acBinaryAvgDes2 = acBinaryDesResults2.Average(r => r.DeserializeTimeMs);
sb.AppendLine($" Deserialize: {((acBinaryAvgDes2 / msgPackAvgDes2 - 1) * 100):+0;-0}% ({acBinaryAvgDes2:F2} ms vs {msgPackAvgDes2:F2} ms)");
}
if (msgPackRtResults2.Count > 0 && acBinaryRtResults2.Count > 0)
{
var msgPackAvgRt2 = msgPackRtResults2.Average(r => r.RoundTripTimeMs);
var acBinaryAvgRt2 = acBinaryRtResults2.Average(r => r.RoundTripTimeMs);
sb.AppendLine($" Round-trip: {((acBinaryAvgRt2 / msgPackAvgRt2 - 1) * 100):+0;-0}% ({acBinaryAvgRt2:F2} ms vs {msgPackAvgRt2:F2} ms)");
}
var msgPackAvgSize2 = results.Where(r => r.SerializerName == SerializerMessagePack).Average(r => r.SerializedSize);
var acBinaryAvgSize2 = results.Where(r => r.SerializerName == SerializerAcBinaryDefault).Average(r => r.SerializedSize);
sb.AppendLine($" Size: {((acBinaryAvgSize2 / msgPackAvgSize2 - 1) * 100):+0;-0}% ({acBinaryAvgSize2:F0} B vs {msgPackAvgSize2:F0} B)");
File.WriteAllText(filePath, sb.ToString(), Utf8NoBom);
System.Console.WriteLine($"\n✓ Results saved to: {filePath}");
File.WriteAllText(logFilePath, sb.ToString(), Utf8NoBom);
System.Console.WriteLine($"✓ Results saved to: {logFilePath}");
}
/// <summary>

View File

@ -67,6 +67,11 @@ public static partial class AcBinarySerializer
private Dictionary<string, int>? _internedStrings;
private List<string>? _internedStringList;
// Single contiguous buffer for all interned string UTF8 bytes (reused across serializations)
private byte[]? _internedStringBuffer;
private int _internedStringBufferPos;
private List<int>? _internedStringLengths;
private Dictionary<string, int>? _propertyNames;
private List<string>? _propertyNameList;
private int[]? _propertyIndexBuffer;
@ -125,7 +130,10 @@ public static partial class AcBinarySerializer
_propertyNameList?.Clear();
_internedStringList?.Clear();
_internedStringUtf8?.Clear();
_internedStringLengths?.Clear();
// Reset intern buffer position (no deallocation - buffer is reused!)
_internedStringBufferPos = 0;
// Reset cached property indices
ResetCachedPropertyIndices();
@ -170,25 +178,23 @@ public static partial class AcBinarySerializer
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
_propertyStateBuffer = null;
}
// _internedStringBuffer is a simple byte[] - no pool return needed, GC handles it
_internedStringBuffer = null;
}
#region String Interning
/// <summary>
/// Cached UTF8 bytes for interned strings to avoid re-encoding in FinalizeHeaderSections.
/// </summary>
private List<byte[]>? _internedStringUtf8;
/// <summary>
/// Registers a string for interning. Returns the index of the string.
/// Uses CollectionsMarshal.GetValueRefOrAddDefault for single-operation lookup+add.
/// Uses single contiguous buffer for UTF8 bytes to minimize allocations.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int RegisterInternedString(string value)
{
_internedStrings ??= new Dictionary<string, int>(InitialInternCapacity, StringComparer.Ordinal);
_internedStringList ??= new List<string>(InitialInternCapacity);
_internedStringUtf8 ??= new List<byte[]>(InitialInternCapacity);
_internedStringLengths ??= new List<int>(InitialInternCapacity);
// Single operation: lookup + conditional add
ref var index = ref CollectionsMarshal.GetValueRefOrAddDefault(_internedStrings, value, out var exists);
@ -197,28 +203,57 @@ public static partial class AcBinarySerializer
return index;
}
// New string - add to lists
// New string - add to list and write UTF8 to buffer
index = _internedStringList.Count;
_internedStringList.Add(value);
_internedStringUtf8.Add(GetUtf8BytesCached(value));
// Calculate UTF8 byte length
var utf8Length = Ascii.IsValid(value) ? value.Length : Utf8NoBom.GetByteCount(value);
// Ensure intern buffer has capacity
EnsureInternBufferCapacity(utf8Length);
// Write UTF8 bytes to contiguous buffer
if (Ascii.IsValid(value))
{
Ascii.FromUtf16(value.AsSpan(), _internedStringBuffer.AsSpan(_internedStringBufferPos, utf8Length), out _);
}
else
{
Utf8NoBom.GetBytes(value.AsSpan(), _internedStringBuffer.AsSpan(_internedStringBufferPos, utf8Length));
}
_internedStringLengths.Add(utf8Length);
_internedStringBufferPos += utf8Length;
return index;
}
/// <summary>
/// Get UTF8 bytes for a string, optimized for ASCII strings.
/// Ensures the intern buffer has enough capacity for additional bytes.
/// Initial size is calculated from MaxStringInternLength * InitialInternCapacity.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static byte[] GetUtf8BytesCached(string value)
private void EnsureInternBufferCapacity(int additionalBytes)
{
// Fast path for ASCII strings - direct char to byte conversion
if (Ascii.IsValid(value))
var required = _internedStringBufferPos + additionalBytes;
if (_internedStringBuffer == null)
{
var bytes = new byte[value.Length];
Ascii.FromUtf16(value.AsSpan(), bytes, out _);
return bytes;
// Initial size: MaxStringInternLength * InitialInternCapacity (e.g., 64 * 32 = 2048)
var initialSize = MaxStringInternLength * InitialInternCapacity;
_internedStringBuffer = new byte[Math.Max(initialSize, required)];
return;
}
// Standard path for multi-byte UTF8
return Utf8NoBom.GetBytes(value);
if (required <= _internedStringBuffer.Length)
{
return;
}
// Grow buffer (double size)
var newSize = Math.Max(_internedStringBuffer.Length * 2, required);
Array.Resize(ref _internedStringBuffer, newSize);
}
#endregion
@ -989,19 +1024,21 @@ public static partial class AcBinarySerializer
/// <summary>
/// Writes interned strings to the footer (end of stream).
/// No shifting or estimation needed - just append.
/// Uses contiguous buffer - no re-encoding needed.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WriteFooterStrings()
{
WriteVarUInt((uint)_internedStringList!.Count);
// Use cached UTF8 bytes - no re-encoding needed!
for (var i = 0; i < _internedStringUtf8!.Count; i++)
// Write from contiguous buffer using stored lengths
var offset = 0;
for (var i = 0; i < _internedStringLengths!.Count; i++)
{
var utf8Bytes = _internedStringUtf8[i];
WriteVarUInt((uint)utf8Bytes.Length);
WriteBytes(utf8Bytes);
var length = _internedStringLengths[i];
WriteVarUInt((uint)length);
WriteBytes(_internedStringBuffer.AsSpan(offset, length));
offset += length;
}
}

View File

@ -135,10 +135,13 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
public static AcBinarySerializerOptions WithMaxDepth(byte maxDepth) => new() { MaxDepth = maxDepth };
/// <summary>
/// Creates options without reference handling.
/// Creates options without reference handling (and string interning disabled for speed).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static AcBinarySerializerOptions WithoutReferenceHandling() => new() { ReferenceHandling = ReferenceHandlingMode.None };
public static AcBinarySerializerOptions WithoutReferenceHandling() => new()
{
ReferenceHandling = ReferenceHandlingMode.None,
};
/// <summary>
/// Creates options without metadata (faster but less flexible).