diff --git a/sandbox/Benchmark/Benchmark/ClassSerializationBenchmark.cs b/sandbox/Benchmark/Benchmark/ClassSerializationBenchmark.cs index 376e876..332261c 100644 --- a/sandbox/Benchmark/Benchmark/ClassSerializationBenchmark.cs +++ b/sandbox/Benchmark/Benchmark/ClassSerializationBenchmark.cs @@ -40,7 +40,7 @@ public void GlobalSetup() new () { Value = "Hammer3" } ] }; - testTomlSerializedObjectInSnakeCase = new Benchmark.Model.TestTomlSerializedObjectInSnakeCase() + testTomlSerializedObjectInSnakeCase = new TestTomlSerializedObjectInSnakeCase() { Str = testTomlSerializedObject.Str, Long = testTomlSerializedObject.Long, diff --git a/sandbox/Benchmark/Benchmark/Utf8TomlDocumentWriterBenchmark.cs b/sandbox/Benchmark/Benchmark/Utf8TomlDocumentWriterBenchmark.cs new file mode 100644 index 0000000..020782a --- /dev/null +++ b/sandbox/Benchmark/Benchmark/Utf8TomlDocumentWriterBenchmark.cs @@ -0,0 +1,38 @@ +using BenchmarkDotNet.Attributes; +using CsToml; +using System.Buffers; +using System.Text; + +namespace Benchmark; + +public class Utf8TomlDocumentWriterBenchmark +{ +#pragma warning disable CS8618 + private static readonly Encoding utf8 = Encoding.UTF8; + private StringObject stringObject; +#pragma warning restore CS8618 + + [Params(1, 10, 100, 1000, 10000)] + public int Size = 10; + + [GlobalSetup] + public void GlobalSetup() + { + stringObject = new StringObject() { Value = string.Join("", Enumerable.Repeat(1, Size).Select(i => i.ToString())) }; + } + + [Benchmark] + public string WriteString() + { + var bufferWriter = new ArrayBufferWriter(); + CsTomlSerializer.Serialize(ref bufferWriter, stringObject); + return utf8.GetString(bufferWriter.WrittenSpan); + } +} + +[TomlSerializedObject] +public partial class StringObject +{ + [TomlValueOnSerialized] + public string? Value { get; set; } +} \ No newline at end of file diff --git a/src/CsToml.Generator/CsToml.Generator.csproj b/src/CsToml.Generator/CsToml.Generator.csproj index f178833..9e02aa4 100644 --- a/src/CsToml.Generator/CsToml.Generator.csproj +++ b/src/CsToml.Generator/CsToml.Generator.csproj @@ -6,7 +6,7 @@ netstandard2.0 enable enable - 12 + 14 true cs $(TargetsForTfmSpecificContentInPackage);PackBuildOutputs diff --git a/src/CsToml/CsTomlReader.cs b/src/CsToml/CsTomlReader.cs index 3ac4c02..d49345c 100644 --- a/src/CsToml/CsTomlReader.cs +++ b/src/CsToml/CsTomlReader.cs @@ -92,6 +92,7 @@ private void ReadKeyToAllowUnicodeInBareKeys(ref ExtendableArray { case TomlCodes.Symbol.TAB: case TomlCodes.Symbol.SPACE: + Advance(1); SkipWhiteSpace(); continue; case TomlCodes.Symbol.EQUAL: @@ -150,6 +151,7 @@ private void ReadKeyToNotAllowUnicodeInBareKeys(ref ExtendableArray(ref bufferWriter); - dottedKeys = valueOnly ? [] : new List(); + dottedKeys = []; valueStates = [(TomlValueState.Default, -1)]; this.valueOnly = valueOnly; this.options = options ?? CsTomlSerializerOptions.Default; @@ -236,6 +236,8 @@ internal void WriteInt64InHexFormat(long value) } } + private static readonly SearchValues ExponentialBytes = SearchValues.Create(".eE"u8); + public void WriteDouble(double value) { switch(value) @@ -261,8 +263,9 @@ public void WriteDouble(double value) } writer.Advance(bytesWritten); - // integer check - if (!writtenSpan.Slice(0, bytesWritten).ContainsAny(".eE"u8)) + // If the formatted value has no decimal point or exponent (i.e., looks like an integer), + // append .0 so that it is represented as a valid TOML float rather than an integer. + if (!writtenSpan.Slice(0, bytesWritten).ContainsAny(ExponentialBytes)) { var writtenSpanEx = writer.GetWrittenSpan(2); writtenSpanEx[0] = TomlCodes.Symbol.DOT; diff --git a/src/CsToml/Values/TomlString.cs b/src/CsToml/Values/TomlString.cs index a5fa6c3..618e7bf 100644 --- a/src/CsToml/Values/TomlString.cs +++ b/src/CsToml/Values/TomlString.cs @@ -138,44 +138,58 @@ internal static void ToTomlBasicString(ref Utf8TomlDocumentWriter { writer.WriteBytes("\""u8); - for (int i = 0; i < byteSpan.Length; i++) + var index = byteSpan.IndexOfAny(EscapedChars); + if (index < 0) { - var ch = byteSpan[i]; - switch (ch) + writer.WriteBytes(byteSpan); + } + else + { + if (index > 0) { - case TomlCodes.Symbol.DOUBLEQUOTED: - writer.Write(TomlCodes.Symbol.BACKSLASH); - writer.Write(TomlCodes.Symbol.DOUBLEQUOTED); - continue; - case TomlCodes.Symbol.BACKSLASH: - writer.Write(TomlCodes.Symbol.BACKSLASH); - writer.Write(TomlCodes.Symbol.BACKSLASH); - continue; - case TomlCodes.Symbol.BACKSPACE: - writer.Write(TomlCodes.Symbol.BACKSLASH); - writer.Write(TomlCodes.Alphabet.b); - continue; - case TomlCodes.Symbol.TAB: - writer.Write(TomlCodes.Symbol.BACKSLASH); - writer.Write(TomlCodes.Alphabet.t); - continue; - case TomlCodes.Symbol.LINEFEED: - writer.Write(TomlCodes.Symbol.BACKSLASH); - writer.Write(TomlCodes.Alphabet.n); - continue; - case TomlCodes.Symbol.FORMFEED: - writer.Write(TomlCodes.Symbol.BACKSLASH); - writer.Write(TomlCodes.Alphabet.f); - continue; - case TomlCodes.Symbol.CARRIAGE: - writer.Write(TomlCodes.Symbol.BACKSLASH); - writer.Write(TomlCodes.Alphabet.r); - continue; - default: - writer.Write(ch); - continue; + writer.WriteBytes(byteSpan.Slice(0, index)); + byteSpan = byteSpan.Slice(index); } + for (index = 0; index < byteSpan.Length; index++) + { + var ch = byteSpan[index]; + switch (ch) + { + case TomlCodes.Symbol.DOUBLEQUOTED: + writer.Write(TomlCodes.Symbol.BACKSLASH); + writer.Write(TomlCodes.Symbol.DOUBLEQUOTED); + continue; + case TomlCodes.Symbol.BACKSLASH: + writer.Write(TomlCodes.Symbol.BACKSLASH); + writer.Write(TomlCodes.Symbol.BACKSLASH); + continue; + case TomlCodes.Symbol.BACKSPACE: + writer.Write(TomlCodes.Symbol.BACKSLASH); + writer.Write(TomlCodes.Alphabet.b); + continue; + case TomlCodes.Symbol.TAB: + writer.Write(TomlCodes.Symbol.BACKSLASH); + writer.Write(TomlCodes.Alphabet.t); + continue; + case TomlCodes.Symbol.LINEFEED: + writer.Write(TomlCodes.Symbol.BACKSLASH); + writer.Write(TomlCodes.Alphabet.n); + continue; + case TomlCodes.Symbol.FORMFEED: + writer.Write(TomlCodes.Symbol.BACKSLASH); + writer.Write(TomlCodes.Alphabet.f); + continue; + case TomlCodes.Symbol.CARRIAGE: + writer.Write(TomlCodes.Symbol.BACKSLASH); + writer.Write(TomlCodes.Alphabet.r); + continue; + default: + writer.Write(ch); + continue; + } + + } } writer.WriteBytes("\""u8); @@ -247,43 +261,58 @@ internal static void ToTomlMultiLineBasicString(ref Utf8TomlDocum { writer.WriteBytes("\"\"\""u8); - for (int i = 0; i < byteSpan.Length; i++) + var index = byteSpan.IndexOfAny(EscapedChars); + if (index < 0) + { + writer.WriteBytes(byteSpan); + } + else { - var ch = byteSpan[i]; - switch (ch) + if (index > 0) { - case TomlCodes.Symbol.DOUBLEQUOTED: - writer.Write(TomlCodes.Symbol.BACKSLASH); - writer.Write(TomlCodes.Symbol.DOUBLEQUOTED); - continue; - case TomlCodes.Symbol.BACKSLASH: - writer.Write(TomlCodes.Symbol.BACKSLASH); - writer.Write(TomlCodes.Symbol.BACKSLASH); - continue; - case TomlCodes.Symbol.BACKSPACE: - writer.Write(TomlCodes.Symbol.BACKSLASH); - writer.Write(TomlCodes.Alphabet.b); - continue; - case TomlCodes.Symbol.TAB: - writer.Write(TomlCodes.Symbol.BACKSLASH); - writer.Write(TomlCodes.Alphabet.t); - continue; - case TomlCodes.Symbol.LINEFEED: - writer.Write(TomlCodes.Symbol.BACKSLASH); - writer.Write(TomlCodes.Alphabet.n); - continue; - case TomlCodes.Symbol.FORMFEED: - writer.Write(TomlCodes.Symbol.BACKSLASH); - writer.Write(TomlCodes.Alphabet.f); - continue; - case TomlCodes.Symbol.CARRIAGE: - writer.Write(TomlCodes.Symbol.BACKSLASH); - writer.Write(TomlCodes.Alphabet.r); - continue; - default: - writer.Write(ch); - continue; + writer.WriteBytes(byteSpan.Slice(0, index)); + byteSpan = byteSpan.Slice(index); } + + for (index = 0; index < byteSpan.Length; index++) + { + var ch = byteSpan[index]; + switch (ch) + { + case TomlCodes.Symbol.DOUBLEQUOTED: + writer.Write(TomlCodes.Symbol.BACKSLASH); + writer.Write(TomlCodes.Symbol.DOUBLEQUOTED); + continue; + case TomlCodes.Symbol.BACKSLASH: + writer.Write(TomlCodes.Symbol.BACKSLASH); + writer.Write(TomlCodes.Symbol.BACKSLASH); + continue; + case TomlCodes.Symbol.BACKSPACE: + writer.Write(TomlCodes.Symbol.BACKSLASH); + writer.Write(TomlCodes.Alphabet.b); + continue; + case TomlCodes.Symbol.TAB: + writer.Write(TomlCodes.Symbol.BACKSLASH); + writer.Write(TomlCodes.Alphabet.t); + continue; + case TomlCodes.Symbol.LINEFEED: + writer.Write(TomlCodes.Symbol.BACKSLASH); + writer.Write(TomlCodes.Alphabet.n); + continue; + case TomlCodes.Symbol.FORMFEED: + writer.Write(TomlCodes.Symbol.BACKSLASH); + writer.Write(TomlCodes.Alphabet.f); + continue; + case TomlCodes.Symbol.CARRIAGE: + writer.Write(TomlCodes.Symbol.BACKSLASH); + writer.Write(TomlCodes.Alphabet.r); + continue; + default: + writer.Write(ch); + continue; + } + } + } writer.WriteBytes("\"\"\""u8); @@ -430,6 +459,8 @@ internal static void ToTomlMultiLineLiteralString(ref Utf8TomlDoc [DebuggerDisplay("{Utf16String}")] internal abstract partial class TomlString(string value) : TomlValue() { + protected static readonly SearchValues EscapedChars = SearchValues.Create("\"\\\b\t\n\f\r"u8); + protected readonly string value = value; public override bool HasValue => true; diff --git a/src/CsToml/Values/TomlTableNode.cs b/src/CsToml/Values/TomlTableNode.cs index fe20d14..513c20a 100644 --- a/src/CsToml/Values/TomlTableNode.cs +++ b/src/CsToml/Values/TomlTableNode.cs @@ -198,24 +198,26 @@ internal NodeStatus TryGetOrAddChildNode(TomlDottedKey key, out TomlTableNode ge getOrAddChildNode = Empty; return NodeStatus.Empty; } - if (nodes.TryGetValueOrAdd(key, out var existingNode, out var newNode)) + + var result = nodes.GetOrAddIfNotFound(key); + if (result.IsExistingValueFound) { - getOrAddChildNode = existingNode!; + getOrAddChildNode = result.ExistingValue!; return NodeStatus.Existed; } - getOrAddChildNode = newNode!; + + getOrAddChildNode = result.AddedValue!; return NodeStatus.NewAdd; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal bool TryGetChildNode(ReadOnlySpan key, out TomlTableNode? childNode) { - var nodes = this.nodes; - if (Value is TomlInlineTable t) - { - nodes = t.RootNode.nodes; - } + TomlTableNodeDictionary? nodes = + Value is TomlInlineTable inlineTable ? + inlineTable.RootNode.nodes : + this.nodes; if (nodes == null) { diff --git a/src/CsToml/Values/TomlTableNodeDictionary.cs b/src/CsToml/Values/TomlTableNodeDictionary.cs index 38054de..28aa952 100644 --- a/src/CsToml/Values/TomlTableNodeDictionary.cs +++ b/src/CsToml/Values/TomlTableNodeDictionary.cs @@ -22,6 +22,15 @@ namespace CsToml.Values.Internal; [DebuggerDisplay("Count = {Count}")] internal sealed class TomlTableNodeDictionary { + public readonly ref struct AnalysisResults(TomlTableNode? existingValue, TomlTableNode? addedValue, bool isExistingValueFound) + { + public TomlTableNode? ExistingValue => existingValue; + + public TomlTableNode? AddedValue => addedValue; + + public bool IsExistingValueFound => isExistingValueFound; + } + private int[] buckets; private Entry[] entries; [DebuggerBrowsable(DebuggerBrowsableState.Never)] @@ -95,11 +104,62 @@ public bool TryGetValueOrAdd(TomlDottedKey key, out TomlTableNode? existingValue return true; } - addedValue = new TomlTableNode(){ IsGroupingProperty = true, Value = TomlValue.Empty}; + addedValue = new TomlTableNode() { IsGroupingProperty = true, Value = TomlValue.Empty }; TryAddCore(key, hashCode, addedValue); return false; } + public AnalysisResults GetOrAddIfNotFound(TomlDottedKey key) + { + ref var buckets = ref this.buckets; + ref var entries = ref this.entries; + + if (buckets.Length == 0) + { + var capacity = HashHelpers.Primes[0]; + entries = new Entry[capacity]; + buckets = new int[capacity]; + } + + var hashCode = key.GetHashCodeFast(); + ref var bucket = ref GetBucket((uint)hashCode); + var index = bucket - 1; + var collisionCount = 0; + + while ((uint)index <= (uint)buckets.Length) + { + ref var e = ref entries[index]; + if (e.hashCode == hashCode && e.key.Equals(key)) + { + return new AnalysisResults(e.value, null, true); + } + + index = e.next; + if ((uint)buckets.Length < ++collisionCount) + throw new Exception(); + } + + var currentCount = count; + if (currentCount == entries.Length) + { + Reserve(HashHelpers.ExpandPrime(currentCount)); + bucket = ref GetBucket((uint)hashCode); + } + index = currentCount; + count++; + + var addedValue = new TomlTableNode() { IsGroupingProperty = true, Value = TomlValue.Empty }; + + ref Entry entry = ref entries[index]; + entry.hashCode = hashCode; + entry.next = bucket - 1; + entry.key = key; + entry.value = addedValue; + bucket = index + 1; + + return new AnalysisResults(null, addedValue, false); + } + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetValue(TomlDottedKey key, out TomlTableNode? value) @@ -157,17 +217,18 @@ private ref int GetBucket(uint hashCode) [MethodImpl(MethodImplOptions.NoInlining)] private void Reserve(int capacity) { - var newEntries = new Entry[capacity]; int count = this.count; - Array.Copy(this.entries, newEntries, count); + Entry[] newEntries = new Entry[capacity]; + Span newEntriesSpan = newEntries.AsSpan(0, count); + this.entries.AsSpan().CopyTo(newEntriesSpan); this.buckets = new int[capacity]; for (int i = 0; i < count; i++) { - if (newEntries[i].next >= -1) + if (newEntriesSpan[i].next >= -1) { - ref var bucket = ref GetBucket((uint)newEntries[i].hashCode); - newEntries[i].next = bucket - 1; + ref var bucket = ref GetBucket((uint)newEntriesSpan[i].hashCode); + newEntriesSpan[i].next = bucket - 1; bucket = i + 1; } }