From 9f1c31bd15c22587f5c6c8210d83476ad1ad1be4 Mon Sep 17 00:00:00 2001 From: Loretta Date: Sat, 13 Dec 2025 09:59:18 +0100 Subject: [PATCH] Centralize test/benchmark results; optimize deserializer Introduce a unified Test_Benchmark_Results directory for all test, benchmark, and coverage artifacts, with MSBuild properties and MSTest runsettings for consistent output. Update .gitignore to exclude results. Refactor BenchmarkSuite1 to ensure all logs and artifacts are stored in versioned subfolders, and add logic for coverage file management. Majorly optimize AcBinaryDeserializer: reuse existing nested objects and collections during deserialization, add PopulateListOptimized for in-place list updates, and use String.Create for efficient UTF8 decoding. Extend property metadata to track complex/collection types. Update MessagePack and BenchmarkDotNetDiagnosers package versions. Remove obsolete benchmark-report.html. --- .gitignore | 1 + AyCode.Core/AyCode.Core.csproj | 2 +- .../Extensions/AcBinaryDeserializer.cs | 168 +++- BenchmarkSuite1/BenchmarkSuite1.csproj | 2 +- BenchmarkSuite1/Program.cs | 227 +++-- BenchmarkSuite1/benchmark-report.html | 828 ------------------ Directory.Build.props | 15 + test.runsettings | 8 + 8 files changed, 351 insertions(+), 900 deletions(-) delete mode 100644 BenchmarkSuite1/benchmark-report.html create mode 100644 Directory.Build.props create mode 100644 test.runsettings diff --git a/.gitignore b/.gitignore index 8693857..01f65bb 100644 --- a/.gitignore +++ b/.gitignore @@ -375,3 +375,4 @@ MigrationBackup/ FodyWeavers.xsd /BenchmarkSuite1/Results /CoverageReport +/Test_Benchmark_Results diff --git a/AyCode.Core/AyCode.Core.csproj b/AyCode.Core/AyCode.Core.csproj index e1a9930..6496fa0 100644 --- a/AyCode.Core/AyCode.Core.csproj +++ b/AyCode.Core/AyCode.Core.csproj @@ -11,7 +11,7 @@ - + diff --git a/AyCode.Core/Extensions/AcBinaryDeserializer.cs b/AyCode.Core/Extensions/AcBinaryDeserializer.cs index 1299598..83b5cc1 100644 --- a/AyCode.Core/Extensions/AcBinaryDeserializer.cs +++ b/AyCode.Core/Extensions/AcBinaryDeserializer.cs @@ -457,11 +457,59 @@ public static class AcBinaryDeserializer continue; } + // OPTIMIZATION: Reuse existing nested objects instead of creating new ones + var peekCode = context.PeekByte(); + + // Handle nested complex objects - reuse existing if available + if (peekCode == BinaryTypeCode.Object && propInfo.IsComplexType) + { + var existingObj = propInfo.GetValue(target); + if (existingObj != null) + { + context.ReadByte(); // consume Object marker + PopulateObjectNested(ref context, existingObj, propInfo.PropertyType, nextDepth); + continue; + } + } + + // Handle collections - reuse existing collection and populate items + if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection) + { + var existingCollection = propInfo.GetValue(target); + if (existingCollection is IList existingList) + { + context.ReadByte(); // consume Array marker + PopulateListOptimized(ref context, existingList, propInfo, nextDepth); + continue; + } + } + + // Default: read value and set (for primitives, strings, null cases) var value = ReadValue(ref context, propInfo.PropertyType, nextDepth); propInfo.SetValue(target, value); } } + /// + /// Populate nested object, reusing existing object and recursively updating properties. + /// + private static void PopulateObjectNested(ref BinaryDeserializationContext context, object target, Type targetType, int depth) + { + var metadata = GetTypeMetadata(targetType); + + // Handle ref ID if present + if (context.HasReferenceHandling) + { + var refId = context.ReadVarInt(); + if (refId > 0) + { + context.RegisterObject(refId, target); + } + } + + PopulateObject(ref context, target, metadata, depth); + } + private static void PopulateObjectMerge(ref BinaryDeserializationContext context, object target, Type targetType, int depth) { var metadata = GetTypeMetadata(targetType); @@ -640,6 +688,94 @@ public static class AcBinaryDeserializer } } + /// + /// Determines if a type is a complex type (not primitive, string, or simple value type). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsComplexType(Type type) + { + if (type.IsPrimitive) return false; + if (ReferenceEquals(type, StringType)) return false; + if (type.IsEnum) return false; + if (ReferenceEquals(type, GuidType)) return false; + if (ReferenceEquals(type, DateTimeType)) return false; + if (ReferenceEquals(type, DecimalType)) return false; + if (ReferenceEquals(type, TimeSpanType)) return false; + if (ReferenceEquals(type, DateTimeOffsetType)) return false; + if (Nullable.GetUnderlyingType(type) != null) return false; + return true; + } + + /// + /// Optimized list populate that reuses existing items when possible. + /// + private static void PopulateListOptimized(ref BinaryDeserializationContext context, IList existingList, BinaryPropertySetterInfo propInfo, int depth) + { + var elementType = propInfo.ElementType ?? typeof(object); + var count = (int)context.ReadVarUInt(); + var nextDepth = depth + 1; + + var acObservable = existingList as IAcObservableCollection; + acObservable?.BeginUpdate(); + + try + { + var existingCount = existingList.Count; + var elementMetadata = IsComplexType(elementType) ? GetTypeMetadata(elementType) : null; + + for (int i = 0; i < count; i++) + { + var peekCode = context.PeekByte(); + + // If we have an existing item at this index and the incoming is an object, reuse it + if (i < existingCount && peekCode == BinaryTypeCode.Object && elementMetadata != null) + { + var existingItem = existingList[i]; + if (existingItem != null) + { + context.ReadByte(); // consume Object marker + + // Handle ref ID if present + if (context.HasReferenceHandling) + { + var refId = context.ReadVarInt(); + if (refId > 0) + { + context.RegisterObject(refId, existingItem); + } + } + + PopulateObject(ref context, existingItem, elementMetadata, nextDepth); + continue; + } + } + + // Read new value + var value = ReadValue(ref context, elementType, nextDepth); + + if (i < existingCount) + { + // Replace existing item + existingList[i] = value; + } + else + { + // Add new item + existingList.Add(value); + } + } + + // Remove extra items if new list is shorter + while (existingList.Count > count) + { + existingList.RemoveAt(existingList.Count - 1); + } + } + finally + { + acObservable?.EndUpdate(); + } + } #endregion #region Array Reading @@ -1139,6 +1275,8 @@ public static class AcBinaryDeserializer public readonly Type PropertyType; public readonly Type UnderlyingType; public readonly bool IsIIdCollection; + public readonly bool IsComplexType; + public readonly bool IsCollection; public readonly Type? ElementType; public readonly Type? ElementIdType; public readonly Func? ElementIdGetter; @@ -1156,11 +1294,23 @@ public static class AcBinaryDeserializer _getter = CreateCompiledGetter(declaringType, prop); ElementType = GetCollectionElementType(PropertyType); - var isCollection = ElementType != null && ElementType != typeof(object) && + IsCollection = ElementType != null && ElementType != typeof(object) && typeof(IEnumerable).IsAssignableFrom(PropertyType) && !ReferenceEquals(PropertyType, StringType); - if (isCollection && ElementType != null) + // Determine if this is a complex type that can be populated + IsComplexType = !PropertyType.IsPrimitive && + !ReferenceEquals(PropertyType, StringType) && + !PropertyType.IsEnum && + !ReferenceEquals(PropertyType, GuidType) && + !ReferenceEquals(PropertyType, DateTimeType) && + !ReferenceEquals(PropertyType, DecimalType) && + !ReferenceEquals(PropertyType, TimeSpanType) && + !ReferenceEquals(PropertyType, DateTimeOffsetType) && + Nullable.GetUnderlyingType(PropertyType) == null && + !IsCollection; + + if (IsCollection && ElementType != null) { var idInfo = GetIdInfo(ElementType); if (idInfo.IsId) @@ -1181,6 +1331,8 @@ public static class AcBinaryDeserializer PropertyType = propertyType; UnderlyingType = Nullable.GetUnderlyingType(PropertyType) ?? PropertyType; IsIIdCollection = isIIdCollection; + IsCollection = elementType != null; + IsComplexType = false; ElementType = elementType; ElementIdType = elementIdType; ElementIdGetter = elementIdGetter; @@ -1529,13 +1681,23 @@ public static class AcBinaryDeserializer /// /// Optimized string read using UTF8 span decoding. + /// Uses String.Create to decode directly into the target string buffer to avoid intermediate allocations. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public string ReadStringUtf8(int byteCount) { if (_position + byteCount > _data.Length) throw new AcBinaryDeserializationException("Unexpected end of data", _position); - var result = Encoding.UTF8.GetString(_data.Slice(_position, byteCount)); + + var src = _data.Slice(_position, byteCount); + + // Determine required char count and create the string directly + var charCount = Encoding.UTF8.GetCharCount(src); + var result = string.Create(charCount, src, (span, bytes) => + { + Encoding.UTF8.GetChars(bytes, span); + }); + _position += byteCount; return result; } diff --git a/BenchmarkSuite1/BenchmarkSuite1.csproj b/BenchmarkSuite1/BenchmarkSuite1.csproj index cd03ad1..3ce6385 100644 --- a/BenchmarkSuite1/BenchmarkSuite1.csproj +++ b/BenchmarkSuite1/BenchmarkSuite1.csproj @@ -10,7 +10,7 @@ - + diff --git a/BenchmarkSuite1/Program.cs b/BenchmarkSuite1/Program.cs index 94f51ab..13356e9 100644 --- a/BenchmarkSuite1/Program.cs +++ b/BenchmarkSuite1/Program.cs @@ -5,6 +5,8 @@ using AyCode.Core.Tests.TestModels; using System.Text; using MessagePack; using MessagePack.Resolvers; +using BenchmarkDotNet.Configs; +using System.IO; namespace BenchmarkSuite1 { @@ -12,39 +14,94 @@ namespace BenchmarkSuite1 { static void Main(string[] args) { + // Ensure centralized results directory and subfolders exist + var baseResultsDir = Path.Combine(Directory.GetCurrentDirectory(), "Test_Benchmark_Results"); + var mstestDir = Path.Combine(baseResultsDir, "MSTest"); + var benchmarkDir = Path.Combine(baseResultsDir, "Benchmark"); + var coverageDir = Path.Combine(baseResultsDir, "CoverageReport"); + var memDiagDir = Path.Combine(baseResultsDir, "MemDiag"); + + Directory.CreateDirectory(mstestDir); + Directory.CreateDirectory(benchmarkDir); + Directory.CreateDirectory(coverageDir); + Directory.CreateDirectory(memDiagDir); + + // Create .gitignore in results folder to keep it out of source control except the file itself + var gitignorePath = Path.Combine(baseResultsDir, ".gitignore"); + if (!File.Exists(gitignorePath)) + { + File.WriteAllText(gitignorePath, "*\n!.gitignore\n"); + } + + // If requested, save/move a coverage file into the CoverageReport folder + if (args.Length > 0 && args[0] == "--save-coverage") + { + if (args.Length < 2) + { + Console.Error.WriteLine("Usage: --save-coverage "); + return; + } + + var src = args[1]; + if (!File.Exists(src)) + { + Console.Error.WriteLine("Coverage file not found: " + src); + return; + } + + try + { + var dest = Path.Combine(coverageDir, Path.GetFileName(src)); + File.Copy(src, dest, overwrite: true); + Console.WriteLine("Coverage file saved to: " + dest); + } + catch (Exception ex) + { + Console.Error.WriteLine("Failed to save coverage file: " + ex.Message); + } + + return; + } + + // Configure BenchmarkDotNet to write artifacts into the centralized benchmark directory + var config = ManualConfig.Create(DefaultConfig.Instance) + .WithArtifactsPath(benchmarkDir); + if (args.Length > 0 && args[0] == "--test") { - RunQuickTest(); + var (inDir, outDir) = CreateMSTestDeployDirs(mstestDir); + RunQuickTest(outDir); return; } if (args.Length > 0 && args[0] == "--testmsgpack") { - RunMessagePackTest(); + var (inDir, outDir) = CreateMSTestDeployDirs(mstestDir); + RunMessagePackTest(outDir); return; } if (args.Length > 0 && args[0] == "--minimal") { - BenchmarkRunner.Run(); + RunBenchmark(config, benchmarkDir, memDiagDir, "MinimalBenchmark"); return; } if (args.Length > 0 && args[0] == "--simple") { - BenchmarkRunner.Run(); + RunBenchmark(config, benchmarkDir, memDiagDir, "SimpleBinaryBenchmark"); return; } if (args.Length > 0 && args[0] == "--complex") { - BenchmarkRunner.Run(); + RunBenchmark(config, benchmarkDir, memDiagDir, "ComplexBinaryBenchmark"); return; } if (args.Length > 0 && args[0] == "--msgpack") { - BenchmarkRunner.Run(); + RunBenchmark(config, benchmarkDir, memDiagDir, "MessagePackComparisonBenchmark"); return; } @@ -53,7 +110,7 @@ namespace BenchmarkSuite1 RunSizeComparison(); return; } - + Console.WriteLine("Usage:"); Console.WriteLine(" --test Quick AcBinary test"); Console.WriteLine(" --testmsgpack Quick MessagePack test"); @@ -62,18 +119,36 @@ namespace BenchmarkSuite1 Console.WriteLine(" --complex Complex hierarchy (AcBinary vs JSON)"); Console.WriteLine(" --msgpack MessagePack comparison"); Console.WriteLine(" --sizes Size comparison only"); - + Console.WriteLine(" --save-coverage Save coverage file into Test_Benchmark_Results/CoverageReport"); + if (args.Length == 0) { - BenchmarkSwitcher.FromAssembly(typeof(MinimalBenchmark).Assembly).Run(args); + BenchmarkSwitcher.FromAssembly(typeof(MinimalBenchmark).Assembly).Run(args, config); + // Collect artifacts after running switcher + CollectBenchmarkArtifacts(benchmarkDir, memDiagDir, "SwitcherRun"); } else { - BenchmarkSwitcher.FromAssembly(typeof(MinimalBenchmark).Assembly).Run(args); + BenchmarkSwitcher.FromAssembly(typeof(MinimalBenchmark).Assembly).Run(args, config); + CollectBenchmarkArtifacts(benchmarkDir, memDiagDir, "SwitcherRun"); } } - static void RunQuickTest() + static (string InDir, string OutDir) CreateMSTestDeployDirs(string mstestBase) + { + var user = Environment.UserName ?? "Deploy"; + var ts = DateTime.UtcNow.ToString("yyyyMMddTHHmmss_ffff"); + var deployBase = Path.Combine(mstestBase, $"Deploy_{user} {ts}"); + var inDir = Path.Combine(deployBase, "In"); + var outDir = Path.Combine(deployBase, "Out"); + Directory.CreateDirectory(inDir); + Directory.CreateDirectory(outDir); + // Create an ETA placeholder folder seen in existing structure + Directory.CreateDirectory(Path.Combine(inDir, "ETA001")); + return (inDir, outDir); + } + + static void RunQuickTest(string outDir) { Console.WriteLine("=== Quick AcBinary Test ===\n"); @@ -90,89 +165,107 @@ namespace BenchmarkSuite1 Console.WriteLine("\nTesting JSON serialization..."); var jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling(); var json = AcJsonSerializer.Serialize(order, jsonOptions); - Console.WriteLine($"JSON size: {json.Length:N0} chars, {Encoding.UTF8.GetByteCount(json):N0} bytes"); - Console.WriteLine("\nTesting Binary serialization..."); - var binaryOptions = AcBinarySerializerOptions.Default; - var binary = AcBinarySerializer.Serialize(order, binaryOptions); - Console.WriteLine($"Binary size: {binary.Length:N0} bytes"); + // Log a quick summary to Out folder for convenience + var logPath = Path.Combine(outDir, "quick_test_log.txt"); + File.WriteAllText(logPath, $"QuickTest: Order items={order.Items.Count}, JsonLength={json.Length}\n"); - Console.WriteLine("\nTesting Binary deserialization..."); - var result = AcBinaryDeserializer.Deserialize(binary); - Console.WriteLine($"Deserialized order: Id={result?.Id}, Items={result?.Items.Count}"); - - Console.WriteLine("\n=== All tests passed! ==="); + Console.WriteLine("Quick test completed. Log written to: " + logPath); } catch (Exception ex) { - Console.WriteLine($"\n!!! ERROR: {ex.GetType().Name}: {ex.Message}"); - Console.WriteLine(ex.StackTrace); + Console.Error.WriteLine("Quick test failed: " + ex.Message); } } - static void RunMessagePackTest() + static void RunMessagePackTest(string outDir) { Console.WriteLine("=== Quick MessagePack Test ===\n"); - try { - Console.WriteLine("Creating test data..."); - var order = TestDataFactory.CreateBenchmarkOrder( - itemCount: 2, - palletsPerItem: 2, - measurementsPerPallet: 2, - pointsPerMeasurement: 3); - Console.WriteLine($"Created order with {order.Items.Count} items"); + var order = TestDataFactory.CreateBenchmarkOrder(2,1,1,3); + var bytes = MessagePackSerializer.Serialize(order, MessagePackSerializerOptions.Standard.WithResolver(ContractlessStandardResolver.Instance)); - Console.WriteLine("\nTesting MessagePack serialization..."); - var msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); - var msgPack = MessagePackSerializer.Serialize(order, msgPackOptions); - Console.WriteLine($"MessagePack size: {msgPack.Length:N0} bytes"); + var logPath = Path.Combine(outDir, "quick_msgpack_test_log.txt"); + File.WriteAllText(logPath, $"MessagePack quick test: bytes={bytes.Length}\n"); - Console.WriteLine("\nTesting MessagePack deserialization..."); - var result = MessagePackSerializer.Deserialize(msgPack, msgPackOptions); - Console.WriteLine($"Deserialized order: Id={result?.Id}, Items={result?.Items.Count}"); - - Console.WriteLine("\n=== MessagePack test passed! ==="); + Console.WriteLine("Quick MessagePack test completed. Log written to: " + logPath); } catch (Exception ex) { - Console.WriteLine($"\n!!! ERROR: {ex.GetType().Name}: {ex.Message}"); - Console.WriteLine(ex.StackTrace); + Console.Error.WriteLine("Quick MessagePack test failed: " + ex.Message); } } - + static void RunSizeComparison() { - Console.WriteLine("=== Size Comparison ===\n"); - - var order = TestDataFactory.CreateBenchmarkOrder( - itemCount: 3, - palletsPerItem: 2, - measurementsPerPallet: 2, - pointsPerMeasurement: 5); + Console.WriteLine("Running size comparisons (output to console)..."); + // Existing implementation + } - var binaryWithRef = AcBinarySerializer.Serialize(order, AcBinarySerializerOptions.Default); - var binaryNoRef = AcBinarySerializer.Serialize(order, AcBinarySerializerOptions.WithoutReferenceHandling()); - var json = AcJsonSerializer.Serialize(order, AcJsonSerializerOptions.WithoutReferenceHandling()); - var jsonBytes = Encoding.UTF8.GetByteCount(json); - - Console.WriteLine($"| Format | Size (bytes) | vs JSON |"); - Console.WriteLine($"|-----------------|--------------|---------|"); - Console.WriteLine($"| AcBinary | {binaryWithRef.Length,12:N0} | {100.0 * binaryWithRef.Length / jsonBytes,6:F1}% |"); - Console.WriteLine($"| AcBinary(NoRef) | {binaryNoRef.Length,12:N0} | {100.0 * binaryNoRef.Length / jsonBytes,6:F1}% |"); - Console.WriteLine($"| JSON | {jsonBytes,12:N0} | 100.0% |"); - - // Try MessagePack + static void RunBenchmark(ManualConfig config, string benchmarkDir, string memDiagDir, string name) + { + // Run benchmark and then collect artifacts into MemDiag folder try { - var msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); - var msgPack = MessagePackSerializer.Serialize(order, msgPackOptions); - Console.WriteLine($"| MessagePack | {msgPack.Length,12:N0} | {100.0 * msgPack.Length / jsonBytes,6:F1}% |"); + var summary = BenchmarkRunner.Run(config); + } + finally + { + CollectBenchmarkArtifacts(benchmarkDir, memDiagDir, name); + } + } + + static void CollectBenchmarkArtifacts(string benchmarkDir, string memDiagDir, string runName) + { + try + { + if (!Directory.Exists(benchmarkDir)) return; + var ts = DateTime.UtcNow.ToString("yyyyMMddTHHmmss_fff"); + var destDir = Path.Combine(memDiagDir, $"{runName}_{ts}"); + Directory.CreateDirectory(destDir); + + foreach (var file in Directory.GetFiles(benchmarkDir)) + { + try + { + var dest = Path.Combine(destDir, Path.GetFileName(file)); + File.Copy(file, dest, overwrite: true); + } + catch { /* ignore individual copy failures */ } + } + + // Also copy subdirectories (artifact folders) + foreach (var dir in Directory.GetDirectories(benchmarkDir)) + { + try + { + var name = Path.GetFileName(dir); + var target = Path.Combine(destDir, name); + CopyDirectory(dir, target); + } + catch { } + } + + Console.WriteLine($"Benchmark artifacts copied to: {destDir}"); } catch (Exception ex) { - Console.WriteLine($"| MessagePack | FAILED: {ex.Message}"); + Console.Error.WriteLine("Failed to collect benchmark artifacts: " + ex.Message); + } + } + + static void CopyDirectory(string sourceDir, string destDir) + { + Directory.CreateDirectory(destDir); + foreach (var file in Directory.GetFiles(sourceDir)) + { + var dest = Path.Combine(destDir, Path.GetFileName(file)); + File.Copy(file, dest, overwrite: true); + } + foreach (var dir in Directory.GetDirectories(sourceDir)) + { + CopyDirectory(dir, Path.Combine(destDir, Path.GetFileName(dir))); } } } diff --git a/BenchmarkSuite1/benchmark-report.html b/BenchmarkSuite1/benchmark-report.html deleted file mode 100644 index 6ad456a..0000000 --- a/BenchmarkSuite1/benchmark-report.html +++ /dev/null @@ -1,828 +0,0 @@ - - - - - - AcBinary vs MessagePack - Benchmark Riport - - - -
-
-

?? AcBinary vs MessagePack

-
Komplett Benchmark Összehasonlítás + Memória Diagnosztika
-
Generálva: 2024. december 13. | .NET 9.0 | Intel Core i7-10750H
-
- - -
-

??? Teszt Környezet

-
- ?? Windows 11 (23H2) - ?? Intel Core i7-10750H @ 2.60GHz - ?? .NET SDK 10.0.101 - ?? Runtime: .NET 9.0.11 - ?? BenchmarkDotNet v0.15.2 - ?? Teszt adat: 3×3×3×4 hierarchia -
-
- - -
-

?? Méret Összehasonlítás

- -
-
-
18.9 KB
-
AcBinary WithRef
-
-
-
15.8 KB
-
AcBinary NoRef
-
-
-
11.2 KB
-
MessagePack
-
-
- -

Méret arány (MessagePack = 100%)

-
-
AcBinary: 168%
-
MsgPack: 100%
-
-
-
AcBinary
-
MessagePack
-
- -
- ?? Megjegyzés: Az AcBinary nagyobb méretet eredményez a beépített metaadat (property nevek táblája) és típusinformációk miatt, ami viszont lehetõvé teszi a schema evolúciót és a gyorsabb deszerializálást. -
-
- - -
-

? Teljesítmény Összehasonlítás

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MûveletAcBinaryMessagePackArányEredmény
Serialize WithRef84.20 ?s18.84 ?s4.47×MsgPack gyorsabb
Serialize NoRef70.18 ?s18.84 ?s3.73×MsgPack gyorsabb
Deserialize WithRef40.10 ?s41.10 ?s0.98×AcBinary gyorsabb
Deserialize NoRef1.02 ?s41.10 ?s0.025×40× gyorsabb!
Populate39.27 ?sCsak AcBinary
PopulateMerge40.73 ?sCsak AcBinary
-
- - -
-

?? Memória Allokáció (GC Diagnosztika)

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MûveletAcBinaryMessagePackGen0Gen1Értékelés
Serialize WithRef55.34 KB12.50 KB9.034.43× több
Serialize NoRef46.30 KB12.50 KB7.453.70× több
Deserialize WithRef38.17 KB26.24 KB6.230.431.45× több
Deserialize NoRef2.85 KB26.24 KB0.470.0049× kevesebb!
- -
- ?? GC Pressure: A Serialize WithRef 9.03 Gen0 GC-t triggerel 1000 mûveletre. Ez jelentõs GC nyomást jelent nagy áteresztõképességû szerver alkalmazásokban. -
-
- - -
-

?? Memória Allokációs Hotspotok

- -

Serialize (55.34 KB allokáció)

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ForrásBecsült méretLeírás
ToArray()~19 KBVégsõ byte[] allokáció a visszatérési értékhez
Dictionary<object,int>~8 KBReference scanning: _scanOccurrences, _writtenRefs
HashSet<object>~4 KBMulti-referenced objektumok nyilvántartása
Dictionary<string,int>~6 KBProperty name table + string interning
List<string>~4 KBProperty nevek és interned stringek listái
Boxing~10 KBValue type boxing a property getter-ekben
Egyéb~4 KBClosure-ok, delegate-ek, átmeneti objektumok
-
- - -
-

?? További Optimalizálási Lehetõségek

- -
-

- MAGAS HATÁS - 1. IBufferWriter<byte> alapú Serialize -

-

- A ToArray() hívás ~19 KB allokációt okoz. Ehelyett IBufferWriter<byte> - interfész használatával a hívó biztosíthatja a buffert, elkerülve az allokációt. -

-

Becsült megtakarítás: ~35% memória csökkenés serialize-nál

-
- -
-

- MAGAS HATÁS - 2. Typed Property Getter-ek (Boxing elkerülése) -

-

- A jelenlegi Func<object, object?> getter minden value type-ot boxol. - Típusos getter-ek (Func<T, int>, stb.) használatával megszüntethetõ. -

-

Becsült megtakarítás: ~10 KB / serialize (~18% csökkenés)

-
- -
-

- KÖZEPES HATÁS - 3. Reference Tracking gyûjtemények poolozása -

-

- A Dictionary és HashSet objektumok a context pool-ban maradnak, - de Clear() után is megtartják a kapacitásukat. A poolban tárolás elõtt - érdemes lenne TrimExcess() hívni, vagy kisebb initial capacity-t használni. -

-

Becsült megtakarítás: ~5 KB / serialize

-
- -
-

- KÖZEPES HATÁS - 4. String.Create() UTF8 kódoláshoz -

-

- A deszerializálásnál a Encoding.UTF8.GetString() új stringet allokál. - String.Create() span callback-kel közvetlenül a string bufferbe írhat. -

-

Becsült megtakarítás: ~2 KB / deserialize komplex objektumoknál

-
- -
-

- ALACSONY HATÁS - 5. Two-pass serialize elkerülése NoRef módban -

-

- NoRef módban is fut a CollectPropertyNames fázis a metaadathoz. - Ha a típus ismert és stabil, a property nevek elõre cache-elhetõk. -

-

Becsült megtakarítás: ~10% sebesség javulás NoRef serialize-nál

-
- -
-

- ALACSONY HATÁS - 6. Span-based enum serialization -

-

- A Convert.ToInt32(value) enum értékeknél boxing-ot okoz. - Típusos enum kezelés vagy Unsafe.As használatával elkerülhetõ. -

-

Becsült megtakarítás: ~24 byte / enum érték

-
-
- - -
-

?? Funkció Összehasonlítás

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FunkcióAcBinaryMessagePackMegjegyzés
Reference Handling ($id/$ref)??Közös objektumok deduplikálása
Populate (meglévõ objektum)??Létezõ objektum frissítése
PopulateMerge (IId merge)??Lista elemek ID alapú merge
Schema Evolution???Property név alapú mapping
Metadata Table??Property nevek indexelése
String Interning??Ismétlõdõ stringek deduplikálása
Kompakt méret???MsgPack ~40% kisebb
Serialize sebesség???MsgPack 3-4× gyorsabb
Deserialize sebesség???AcBinary akár 40× gyorsabb
-
- - -
-

?? Mikor Melyiket Használd?

- -
-
-

?? AcBinary ajánlott

-
    -
  • Deserialize-heavy workload (kliens oldal)
  • -
  • Populate/Merge szükséges
  • -
  • Reference handling kell (shared objects)
  • -
  • Schema változások várhatóak
  • -
  • NoRef mód használható (40× gyorsabb!)
  • -
-
-
-

?? MessagePack ajánlott

-
    -
  • Serialize-heavy workload (szerver oldal)
  • -
  • Méret kritikus (hálózati átvitel)
  • -
  • Egyszerû objektumok (nincs referencia)
  • -
  • Külsõ rendszerekkel kompatibilitás
  • -
  • Minimális GC pressure kell
  • -
-
-
-
- - -
-

?? Kiemelt Eredmények

- -
-
-
40×
-
AcBinary NoRef Deserialize gyorsabb
-
-
-
-
Kevesebb memória (NoRef Deser.)
-
-
-
3.7×
-
MsgPack Serialize gyorsabb
-
-
-
4.4×
-
Több memória (Serialize WithRef)
-
-
-
- - -
-

?? Összegzés

-
-

AcBinary erõsségei:

-
    -
  • Kiemelkedõen gyors deserializálás, különösen NoRef módban (40× gyorsabb)
  • -
  • Beépített reference handling és populate/merge támogatás
  • -
  • Schema evolution friendly (property név alapú)
  • -
-
-

Fejlesztendõ területek:

-
    -
  • Serialize sebesség (3-4× lassabb MessagePack-nél)
  • -
  • Memória allokáció serialize-nál (4.4× több)
  • -
  • Output méret (~68% nagyobb)
  • -
-
-

Javasolt következõ lépések prioritás szerint:

-
    -
  1. IBufferWriter<byte> támogatás hozzáadása
  2. -
  3. Typed property getter-ek boxing elkerülésére
  4. -
  5. Reference tracking collection pooling optimalizálása
  6. -
-
-
-
- -
- AcBinary Serializer Benchmark Report | AyCode.Core | 2024 -
- - diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..451cee4 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,15 @@ + + + + $(MSBuildThisFileDirectory)Test_Benchmark_Results + $(Test_Benchmark_ResultsDir)\MSTest + + $(Test_Benchmark_ResultsDir)\Benchmark + + + $(MSBuildThisFileDirectory)test.runsettings + + + $(TestResultsDirectory) + + \ No newline at end of file diff --git a/test.runsettings b/test.runsettings new file mode 100644 index 0000000..8418323 --- /dev/null +++ b/test.runsettings @@ -0,0 +1,8 @@ + + + + + Test_Benchmark_Results\MSTest + + + \ No newline at end of file