Skip to content

Commit 29075a0

Browse files
author
Nik Karpinsky
committed
Add new WriteRecords benchmark and performance fix.
The new benchmark measures the performance of WriteRecords given an `IEnumerable<T>` records. The performance fix is to adjust `ObjectRecordWriter` to take all expressions and put them into a single block expression that writes all fields at once, instead of combining multiple expressions into a single multi-cast delegate. This results in fewer delegate invocations since it is called per record instead of per field, and only a single delegate compile. The current benchmark runs ~15% faster with the new code. Now that delegates are not combined `RecordWriter.CombineDelegates` could be removed, though I opted not to since it is on a public class and I didn't want to break compatibility. I also noted this was added since Silverlight doesn't have `Delegate.Combine` and I'm not sure if Silverlight has `Expression.Block` though given it is out of support as of Oct 2021 I figured this was ok.
1 parent 33970e5 commit 29075a0

File tree

5 files changed

+75
-9
lines changed

5 files changed

+75
-9
lines changed

performance/CsvHelper.Benchmarks/BenchmarkEnumerateRecords.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
namespace CsvHelper.Benchmarks;
88

9-
[MemoryDiagnoser]
109
public class BenchmarkEnumerateRecords
1110
{
1211
private const int entryCount = 10000;
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
using BenchmarkDotNet.Running;
2+
using BenchmarkDotNet.Configs;
3+
using BenchmarkDotNet.Jobs;
24

35
namespace CsvHelper.Benchmarks;
46

57
internal class BenchmarkMain
68
{
79
static void Main(string[] args)
810
{
9-
_ = BenchmarkRunner.Run<BenchmarkEnumerateRecords>();
11+
BenchmarkSwitcher.FromAssembly(System.Reflection.Assembly.GetExecutingAssembly()).Run(
12+
args,
13+
DefaultConfig.Instance
14+
.AddJob(BenchmarkDotNet.Jobs.Job.Default.WithEnvironmentVariables(System.Array.Empty<BenchmarkDotNet.Jobs.EnvironmentVariable>()).AsMutator())
15+
.AddDiagnoser(new Microsoft.VSDiagnostics.CPUUsageDiagnoser())
16+
); return;
1017
}
1118
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Globalization;
4+
using System.IO;
5+
using BenchmarkDotNet.Attributes;
6+
7+
namespace CsvHelper.Benchmarks;
8+
9+
public class BenchmarkWriteCsv
10+
{
11+
private const int entryCount = 10000;
12+
private readonly List<Simple> records = new(entryCount);
13+
14+
public class Simple
15+
{
16+
public int Id1 { get; set; }
17+
public int Id2 { get; set; }
18+
public string Name1 { get; set; }
19+
public string Name2 { get; set; }
20+
}
21+
22+
[GlobalSetup]
23+
public void GlobalSetup()
24+
{
25+
var random = new Random(42);
26+
var chars = new char[10];
27+
string getRandomString()
28+
{
29+
for (int i = 0; i < 10; ++i)
30+
chars[i] = (char)random.Next('a', 'z' + 1);
31+
return new string(chars);
32+
}
33+
34+
for (int i = 0; i < entryCount; ++i)
35+
{
36+
records.Add(new Simple
37+
{
38+
Id1 = random.Next(),
39+
Id2 = random.Next(),
40+
Name1 = getRandomString(),
41+
Name2 = getRandomString(),
42+
});
43+
}
44+
}
45+
46+
[Benchmark]
47+
public void WriteRecords()
48+
{
49+
using var stream = new MemoryStream();
50+
using var streamWriter = new StreamWriter(stream);
51+
using var writer = new CsvHelper.CsvWriter(streamWriter, CultureInfo.InvariantCulture);
52+
writer.WriteRecords(records);
53+
streamWriter.Flush();
54+
}
55+
}

performance/CsvHelper.Benchmarks/CsvHelper.Benchmarks.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
</PropertyGroup>
77

88
<ItemGroup>
9-
<PackageReference Include="BenchmarkDotNet" Version="0.15.0" />
9+
<PackageReference Include="BenchmarkDotNet" Version="0.15.2" />
10+
<PackageReference Include="Microsoft.VisualStudio.DiagnosticsHub.BenchmarkDotNetDiagnosers" Version="18.0.36328.1" />
1011
</ItemGroup>
1112

1213
<ItemGroup>

src/CsvHelper/Expressions/ObjectRecordWriter.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ protected override Action<T> CreateWriteDelegate<T>(Type type)
4646
throw new WriterException(Writer.Context, $"No properties are mapped for type '{type.FullName}'.");
4747
}
4848

49-
var delegates = new List<Action<T>>();
49+
var expressions = new List<Expression>(members.Count);
5050

5151
foreach (var memberMap in members)
5252
{
@@ -59,7 +59,7 @@ protected override Action<T> CreateWriteDelegate<T>(Type type)
5959
var args = Expression.New(constructor, recordParameterConverted);
6060
Expression exp = Expression.Invoke(memberMap.Data.WritingConvertExpression, args);
6161
exp = Expression.Call(Expression.Constant(Writer), nameof(Writer.WriteField), null, exp);
62-
delegates.Add(Expression.Lambda<Action<T>>(exp, recordParameter).Compile());
62+
expressions.Add(exp);
6363
continue;
6464
}
6565

@@ -110,12 +110,16 @@ protected override Action<T> CreateWriteDelegate<T>(Type type)
110110
}
111111

112112
var writeFieldMethodCall = Expression.Call(Expression.Constant(Writer), nameof(Writer.WriteConvertedField), null, fieldExpression, Expression.Constant(memberMap.Data.Type));
113-
114-
delegates.Add(Expression.Lambda<Action<T>>(writeFieldMethodCall, recordParameter).Compile());
113+
expressions.Add(writeFieldMethodCall);
115114
}
116115

117-
var action = CombineDelegates(delegates) ?? new Action<T>((T parameter) => { });
116+
if (expressions.Count == 0)
117+
{
118+
return new Action<T>((T parameter) => { });
119+
}
118120

119-
return action;
121+
// Combine all field writes into a single block
122+
var block = Expression.Block(expressions);
123+
return Expression.Lambda<Action<T>>(block, recordParameter).Compile();
120124
}
121125
}

0 commit comments

Comments
 (0)