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