diff --git a/performance/CsvHelper.Benchmarks/BenchmarkMain.cs b/performance/CsvHelper.Benchmarks/BenchmarkMain.cs index 849ffcc1b..9b2c4ae68 100644 --- a/performance/CsvHelper.Benchmarks/BenchmarkMain.cs +++ b/performance/CsvHelper.Benchmarks/BenchmarkMain.cs @@ -6,6 +6,6 @@ internal class BenchmarkMain { static void Main(string[] args) { - _ = BenchmarkRunner.Run(); + _ = BenchmarkSwitcher.FromAssembly(typeof(BenchmarkMain).Assembly).Run(args); } } diff --git a/performance/CsvHelper.Benchmarks/BenchmarkShouldQuote.cs b/performance/CsvHelper.Benchmarks/BenchmarkShouldQuote.cs new file mode 100644 index 000000000..8e20a9eef --- /dev/null +++ b/performance/CsvHelper.Benchmarks/BenchmarkShouldQuote.cs @@ -0,0 +1,81 @@ +using BenchmarkDotNet.Attributes; + +using CsvHelper.Configuration; + +using System; +using System.Globalization; +using System.IO; + +namespace CsvHelper.Benchmarks; + +public class BenchmarkShouldQuote +{ + private const int FieldCount = 10000; + private readonly StringWriter stringWriter = new(); + private string[] fields = null!; + private static readonly Type StringType = typeof(string); + [GlobalSetup] + public void GlobalSetup() + { + // Pre-compute a mix of typical fields. Real CsvWriter is created per iteration below. + var random = new Random(42); + this.fields = new string[FieldCount]; + var chars = new char[10]; + for (int i = 0; i < FieldCount; i++) + { + string value; + var pick = i % 10; + if (pick == 0) + { + // Field that should be quoted (contains comma) + value = "a,b,c"; + } + else if (pick == 1) + { + // Field that should be quoted (contains quote) + value = "he said \"hi\""; + } + else if (pick < 6) + { + // Random lowercase strings (no special chars) + for (int j = 0; j < 10; j++) + { + chars[j] = (char)random.Next('a', 'z' + 1); + } + + value = new string (chars); + } + else + { + // Integer-as-string + value = random.Next().ToString(CultureInfo.InvariantCulture); + } + + this.fields[i] = value; + } + } + + [GlobalCleanup] + public void GlobalCleanup() + { + this.stringWriter.Dispose(); + } + + [Benchmark] + public void ShouldQuote() + { + using var csv = new CsvWriter(this.stringWriter, CultureInfo.InvariantCulture, true); + int quotedCount = 0; + var f = this.fields; + for (int i = 0; i < f.Length; i++) + { + var args = new ShouldQuoteArgs(f[i], StringType, csv); + if (ConfigurationFunctions.ShouldQuote(args)) + { + quotedCount++; + } + } + + _ = quotedCount; + } +} diff --git a/performance/CsvHelper.Benchmarks/BenchmarkWriteRecords.cs b/performance/CsvHelper.Benchmarks/BenchmarkWriteRecords.cs new file mode 100644 index 000000000..8cbcc8ef2 --- /dev/null +++ b/performance/CsvHelper.Benchmarks/BenchmarkWriteRecords.cs @@ -0,0 +1,55 @@ +using BenchmarkDotNet.Attributes; + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; + +namespace CsvHelper.Benchmarks; + +public class BenchmarkWriteRecords +{ + private const int entryCount = 10000; + private readonly MemoryStream stream = new(); + private List records = new(); + public class Simple + { + public int Id { get; set; } + public string Name { get; set; } + } + + [GlobalSetup] + public void GlobalSetup() + { + var random = new Random(42); // Pick a known seed to keep things consistent + var chars = new char[10]; + string getRandomString() + { + for (int i = 0; i < 10; ++i) + chars[i] = (char)random.Next('a', 'z' + 1); + return new string (chars); + } + + this.records = new List(entryCount); + for (int i = 0; i < entryCount; ++i) + { + this.records.Add(new Simple() { Id = random.Next(), Name = getRandomString() }); + } + } + + [GlobalCleanup] + public void GlobalCleanup() + { + this.stream.Dispose(); + } + + [Benchmark] + public void WriteRecords() + { + this.stream.Position = 0; + this.stream.SetLength(0); + using var streamWriter = new StreamWriter(this.stream, null, -1, true); + using var csv = new CsvWriter(streamWriter, CultureInfo.InvariantCulture, true); + csv.WriteRecords(this.records); + } +} diff --git a/performance/CsvHelper.Benchmarks/CsvHelper.Benchmarks.csproj b/performance/CsvHelper.Benchmarks/CsvHelper.Benchmarks.csproj index 248fbd13f..b08519930 100644 --- a/performance/CsvHelper.Benchmarks/CsvHelper.Benchmarks.csproj +++ b/performance/CsvHelper.Benchmarks/CsvHelper.Benchmarks.csproj @@ -7,6 +7,7 @@ + diff --git a/src/CsvHelper/Configuration/ConfigurationFunctions.cs b/src/CsvHelper/Configuration/ConfigurationFunctions.cs index 99ed29616..11a218980 100644 --- a/src/CsvHelper/Configuration/ConfigurationFunctions.cs +++ b/src/CsvHelper/Configuration/ConfigurationFunctions.cs @@ -103,14 +103,42 @@ public static bool ShouldQuote(ShouldQuoteArgs args) return false; } + var length = field.Length; + + // Cheapest checks first: leading/trailing space. + if (field[0] == ' ' || field[length - 1] == ' ') + { + return true; + } + + var quote = config.Quote; + var delimiter = config.Delimiter; + var isNewLineSet = config.IsNewLineSet; + + // Fast path: common configuration (single-char delimiter, default newline handling). + // Scan the field once, comparing against quote, delimiter, '\r', and '\n'. + if (!isNewLineSet && delimiter.Length == 1) + { + var delimiterChar = delimiter[0]; + for (int i = 0; i < length; i++) + { + var c = field[i]; + if (c == quote || c == delimiterChar || c == '\r' || c == '\n') + { + return true; + } + } + + return false; + } + + // Slow path: multi-char delimiter or custom NewLine. var shouldQuote = ( - field[0] == ' ' // Starts with a space - || field[field.Length - 1] == ' ' // Ends with a space - || field.Contains(config.Quote) // Contains quote - || !config.IsNewLineSet && field.IndexOfAny(lineEndingChars) > -1 // Contains line ending characters - || config.IsNewLineSet && field.Contains(config.NewLine) // Contains newline - || (config.Delimiter.Length > 0 && field.Contains(config.Delimiter)) // Contains delimiter + field.Contains(quote) // Contains quote + || !isNewLineSet && field.IndexOfAny(lineEndingChars) > -1 // Contains line ending characters + || isNewLineSet && field.Contains(config.NewLine) // Contains newline + || (delimiter.Length > 0 && field.Contains(delimiter)) // Contains delimiter ); return shouldQuote;