-
Notifications
You must be signed in to change notification settings - Fork 273
/
Perf.FileStream.cs
459 lines (406 loc) · 22.3 KB
/
Perf.FileStream.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Extensions;
using MicroBenchmarks;
namespace System.IO.Tests
{
[BenchmarkCategory(Categories.Libraries)]
public class Perf_FileStream
{
private const int OneKibibyte = 1 << 10; // 1024
private const int HalfKibibyte = OneKibibyte >> 1;
private const int FourKibibytes = OneKibibyte << 2; // default Stream buffer size
private const int SixteenKibibytes = FourKibibytes << 2; // default Stream buffer size * 4
private const int OneMibibyte = OneKibibyte << 10;
private const int HundredMibibytes = OneMibibyte * 100;
private Dictionary<long, string> _sourceFilePaths, _destinationFilePaths;
private Dictionary<int, byte[]> _userBuffers;
private void Setup(params long[] fileSizes)
{
_userBuffers = new Dictionary<int, byte[]>()
{
{ HalfKibibyte, ValuesGenerator.Array<byte>(HalfKibibyte) },
{ OneKibibyte, ValuesGenerator.Array<byte>(OneKibibyte) },
{ FourKibibytes, ValuesGenerator.Array<byte>(FourKibibytes) },
{ SixteenKibibytes, ValuesGenerator.Array<byte>(SixteenKibibytes) },
};
_sourceFilePaths = fileSizes.ToDictionary(size => size, size => CreateFileWithRandomContent(size));
_destinationFilePaths = fileSizes.ToDictionary(size => size, size => CreateFileWithRandomContent(size));
static string CreateFileWithRandomContent(long fileSize)
{
string filePath = FileUtils.GetTestFilePath();
File.WriteAllBytes(filePath, ValuesGenerator.Array<byte>((int)fileSize));
return filePath;
}
}
[GlobalCleanup]
public void Cleanup()
{
if (_sourceFilePaths != null)
{
foreach (string filePath in _sourceFilePaths.Values.Concat(_destinationFilePaths.Values))
{
File.Delete(filePath);
}
}
}
[GlobalSetup(Targets = new[] { nameof(OpenClose), nameof(LockUnlock), nameof(SeekForward), nameof(SeekBackward),
nameof(GetLength), nameof(ReadByte), nameof(WriteByte), nameof(Flush), nameof(FlushAsync) })]
public void SetuOneKibibyteBenchmarks() => Setup(OneKibibyte );
[Benchmark]
[Arguments(OneKibibyte, FileOptions.None)] // sync (default)
[Arguments(OneKibibyte, FileOptions.Asynchronous)] // async
public bool OpenClose(long fileSize, FileOptions options)
{
string filePath = _sourceFilePaths[fileSize];
using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, FourKibibytes, options))
{
return fileStream.IsAsync; // return something just to consume the stream
}
}
[Benchmark]
[Arguments(OneKibibyte , FileOptions.None)]
[Arguments(OneKibibyte , FileOptions.Asynchronous)]
[OperatingSystemsFilter(allowed: true, OS.Windows, OS.Linux)] // "Lock and Unlock are supported only on Windows and Linux"
#if NET6_0_OR_GREATER // the method was marked as unsupported on macOS in .NET 6.0
[System.Runtime.Versioning.UnsupportedOSPlatform("macos")]
#endif
public void LockUnlock(long fileSize, FileOptions options)
{
string filePath = _sourceFilePaths[fileSize];
using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite, FourKibibytes, options))
{
fileStream.Lock(0, fileStream.Length);
fileStream.Unlock(0, fileStream.Length);
}
}
[Benchmark]
[Arguments(OneKibibyte, FileOptions.None)]
[Arguments(OneKibibyte, FileOptions.Asynchronous)]
public void SeekForward(long fileSize, FileOptions options)
{
string filePath = _sourceFilePaths[fileSize];
using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, FourKibibytes, options))
{
for (long offset = 0; offset < fileSize; offset++)
{
fileStream.Seek(offset, SeekOrigin.Begin);
}
}
}
[Benchmark]
[Arguments(OneKibibyte, FileOptions.None)]
[Arguments(OneKibibyte, FileOptions.Asynchronous)]
public void SeekBackward(long fileSize, FileOptions options)
{
string filePath = _sourceFilePaths[fileSize];
using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, FourKibibytes, options))
{
for (long offset = -1; offset >= -fileSize; offset--)
{
fileStream.Seek(offset, SeekOrigin.End);
}
}
}
[Benchmark(OperationsPerInvoke = OneKibibyte)]
[Arguments(FileShare.Read)]
[Arguments(FileShare.Write)]
public long GetLength(FileShare options) // since other benchmarks use "options" name, we use it as well to avoid introducing new column in the results
{
string filePath = _sourceFilePaths[OneKibibyte];
using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, options, FourKibibytes, FileOptions.None))
{
long length = 0;
for (long i = 0; i < OneKibibyte; i++)
{
length = fileStream.Length;
}
return length;
}
}
[Benchmark]
[Arguments(OneKibibyte, FileOptions.None)]
[Arguments(OneKibibyte, FileOptions.Asynchronous)] // calling ReadByte() on bigger files makes no sense, so we don't have more test cases
public int ReadByte(long fileSize, FileOptions options)
{
int result = default;
using (FileStream fileStream = new FileStream(_sourceFilePaths[fileSize], FileMode.Open, FileAccess.Read, FileShare.Read, FourKibibytes, options))
{
for (long i = 0; i < fileSize; i++)
{
result += fileStream.ReadByte();
}
}
return result;
}
[Benchmark]
[Arguments(OneKibibyte, FileOptions.None)]
[Arguments(OneKibibyte, FileOptions.Asynchronous)]
public void WriteByte(long fileSize, FileOptions options)
{
using (FileStream fileStream = new FileStream(_destinationFilePaths[fileSize], FileMode.Create, FileAccess.Write, FileShare.Read, FourKibibytes, options))
{
for (int i = 0; i < fileSize; i++)
{
fileStream.WriteByte(default);
}
}
}
// Can't use nameof for those that are conditionally compiled
[GlobalSetup(Targets = new[] { nameof(Read), nameof(Read_NoBuffering), "ReadAsync", "ReadAsync_NoBuffering",
nameof(Write), nameof(Write_NoBuffering), "WriteAsync", "WriteAsync_NoBuffering", nameof(CopyToFile), nameof(CopyToFileAsync), nameof(Append), "AppendAsync" })]
public void SetupBigFileBenchmarks() => Setup(OneKibibyte, OneMibibyte, HundredMibibytes);
public IEnumerable<object[]> SyncArguments()
{
// long fileSize, int userBufferSize, FileOptions options
foreach (FileOptions options in new FileOptions[] { FileOptions.None, FileOptions.Asynchronous })
{
yield return new object[] { OneKibibyte, OneKibibyte, options }; // small file size, user buffer size == file size
yield return new object[] { OneMibibyte, HalfKibibyte, options }; // medium size file, user buffer size * 8 == default stream buffer size (buffering is beneficial)
yield return new object[] { OneMibibyte, FourKibibytes, options }; // medium size file, user buffer size == default stream buffer size (buffering is not beneficial)
yield return new object[] { HundredMibibytes, FourKibibytes, options }; // big file, user buffer size == default stream buffer size (buffering is not beneficial)
}
}
public IEnumerable<object[]> SyncArguments_NoBuffering()
{
// long fileSize, int userBufferSize, FileOptions options
foreach (FileOptions options in new FileOptions[] { FileOptions.None, FileOptions.Asynchronous })
{
yield return new object[] { OneMibibyte, SixteenKibibytes, options }; // medium size file, user buffer size == 4 * default stream buffer size
yield return new object[] { HundredMibibytes, SixteenKibibytes, options }; // big file, user buffer size == 4 * default stream buffer size
}
}
[Benchmark]
[ArgumentsSource(nameof(SyncArguments))]
public long Read(long fileSize, int userBufferSize, FileOptions options)
=> Read(fileSize, userBufferSize, options, streamBufferSize: FourKibibytes);
[Benchmark]
[ArgumentsSource(nameof(SyncArguments_NoBuffering))]
public long Read_NoBuffering(long fileSize, int userBufferSize, FileOptions options)
=> Read(fileSize, userBufferSize, options, streamBufferSize: 1);
private long Read(long fileSize, int userBufferSize, FileOptions options, int streamBufferSize)
{
byte[] userBuffer = _userBuffers[userBufferSize];
long bytesRead = 0;
using (FileStream fileStream = new FileStream(
_sourceFilePaths[fileSize], FileMode.Open, FileAccess.Read, FileShare.Read, streamBufferSize, options))
{
while (bytesRead < fileSize)
{
bytesRead += fileStream.Read(userBuffer, 0, userBuffer.Length);
}
}
return bytesRead;
}
[Benchmark]
[ArgumentsSource(nameof(SyncArguments))]
public void Write(long fileSize, int userBufferSize, FileOptions options)
=> Write(FileMode.Create, fileSize, userBufferSize, options, streamBufferSize: FourKibibytes);
[Benchmark]
[ArgumentsSource(nameof(SyncArguments_NoBuffering))]
public void Write_NoBuffering(long fileSize, int userBufferSize, FileOptions options)
=> Write(FileMode.Create, fileSize, userBufferSize, options, streamBufferSize: 1);
[Benchmark]
[Arguments(OneMibibyte, FourKibibytes, FileOptions.DeleteOnClose)]
public void Append(long fileSize, int userBufferSize, FileOptions options)
=> Write(FileMode.Append, fileSize, userBufferSize, options, streamBufferSize: FourKibibytes);
private void Write(FileMode mode, long fileSize, int userBufferSize, FileOptions options, int streamBufferSize)
{
byte[] userBuffer = _userBuffers[userBufferSize];
using (FileStream fileStream = new FileStream(_destinationFilePaths[fileSize], mode, FileAccess.Write, FileShare.Read, streamBufferSize, options))
{
for (int i = 0; i < fileSize / userBufferSize; i++)
{
fileStream.Write(userBuffer, 0, userBuffer.Length);
}
}
}
#if !NETFRAMEWORK // APIs added in .NET Core 2.0
public IEnumerable<object[]> AsyncArguments()
{
// long fileSize, int userBufferSize, FileOptions options
yield return new object[] { OneKibibyte, OneKibibyte, FileOptions.Asynchronous }; // small file size, user buffer size == file size
yield return new object[] { OneKibibyte, OneKibibyte, FileOptions.None }; // same as above, but sync open, later async usage (common use case)
yield return new object[] { OneMibibyte, HalfKibibyte, FileOptions.Asynchronous }; // medium size file, user buffer size * 8 == default stream buffer size (buffering is beneficial)
yield return new object[] { OneMibibyte, HalfKibibyte, FileOptions.None }; // same as above, but sync open, later async usage (common use case)
yield return new object[] { OneMibibyte, FourKibibytes, FileOptions.Asynchronous }; // medium size file, user buffer size == default stream buffer size (buffering is not beneficial)
yield return new object[] { OneMibibyte, FourKibibytes, FileOptions.None }; // same as above, but sync open, later async usage (common use case)
yield return new object[] { HundredMibibytes, FourKibibytes, FileOptions.Asynchronous }; // big file, user buffer size == default stream buffer size (buffering is not beneficial)
yield return new object[] { HundredMibibytes, FourKibibytes, FileOptions.None }; // same as above, but sync open, later async usage (common use case)
}
public IEnumerable<object[]> AsyncArguments_NoBuffering()
{
// long fileSize, int userBufferSize, FileOptions options
yield return new object[] { OneMibibyte, SixteenKibibytes, FileOptions.Asynchronous }; // medium size file, user buffer size == 4 * default stream buffer size
yield return new object[] { OneMibibyte, SixteenKibibytes, FileOptions.None }; // same as above, but sync open, later async usage
yield return new object[] { HundredMibibytes, SixteenKibibytes, FileOptions.Asynchronous }; // big file, user buffer size == 4 * default stream buffer size
yield return new object[] { HundredMibibytes, SixteenKibibytes, FileOptions.None }; // same as above, but sync open, later async usage
}
[Benchmark]
[ArgumentsSource(nameof(AsyncArguments))]
[BenchmarkCategory(Categories.NoWASM)]
public Task<long> ReadAsync(long fileSize, int userBufferSize, FileOptions options)
=> ReadAsync(fileSize, userBufferSize, options, streamBufferSize: FourKibibytes);
[Benchmark]
[ArgumentsSource(nameof(AsyncArguments_NoBuffering))]
[BenchmarkCategory(Categories.NoWASM)]
public Task<long> ReadAsync_NoBuffering(long fileSize, int userBufferSize, FileOptions options)
=> ReadAsync(fileSize, userBufferSize, options, streamBufferSize: 1);
private async Task<long> ReadAsync(long fileSize, int userBufferSize, FileOptions options, int streamBufferSize)
{
CancellationToken cancellationToken = CancellationToken.None;
Memory<byte> userBuffer = new Memory<byte>(_userBuffers[userBufferSize]);
long bytesRead = 0;
using (FileStream fileStream = new FileStream(_sourceFilePaths[fileSize], FileMode.Open, FileAccess.Read, FileShare.Read, streamBufferSize, options))
{
while (bytesRead < fileSize)
{
bytesRead += await fileStream.ReadAsync(userBuffer, cancellationToken);
}
}
return bytesRead;
}
[Benchmark]
[ArgumentsSource(nameof(AsyncArguments))]
[BenchmarkCategory(Categories.NoWASM)]
public Task WriteAsync(long fileSize, int userBufferSize, FileOptions options)
=> WriteAsync(FileMode.Create, fileSize, userBufferSize, options, streamBufferSize: FourKibibytes);
[Benchmark]
[ArgumentsSource(nameof(AsyncArguments_NoBuffering))]
[BenchmarkCategory(Categories.NoWASM)]
public Task WriteAsync_NoBuffering(long fileSize, int userBufferSize, FileOptions options)
=> WriteAsync(FileMode.Create, fileSize, userBufferSize, options, streamBufferSize: 1);
[Benchmark]
[Arguments(OneMibibyte, FourKibibytes, FileOptions.DeleteOnClose | FileOptions.Asynchronous)]
[BenchmarkCategory(Categories.NoWASM)]
public Task AppendAsync(long fileSize, int userBufferSize, FileOptions options)
=> WriteAsync(FileMode.Append, fileSize, userBufferSize, options, streamBufferSize: FourKibibytes);
private async Task WriteAsync(FileMode mode, long fileSize, int userBufferSize, FileOptions options, int streamBufferSize)
{
CancellationToken cancellationToken = CancellationToken.None;
Memory<byte> userBuffer = new Memory<byte>(_userBuffers[userBufferSize]);
using (FileStream fileStream = new FileStream(_destinationFilePaths[fileSize], mode, FileAccess.Write, FileShare.Read, streamBufferSize, options))
{
for (int i = 0; i < fileSize / userBufferSize; i++)
{
await fileStream.WriteAsync(userBuffer, cancellationToken);
}
}
}
#endif
#if NET6_0_OR_GREATER // APIs added in .NET 6
private string _nonExistingFile;
[GlobalSetup(Targets = new[] { nameof(Write_NoBuffering_PreallocationSize), nameof(WriteAsync_NoBuffering_PreallocationSize) })]
public void SetupNonExistingFile()
{
_userBuffers = new Dictionary<int, byte[]>()
{
{ SixteenKibibytes, ValuesGenerator.Array<byte>(SixteenKibibytes) },
};
string filePath = FileUtils.GetTestFilePath();
if (File.Exists(filePath))
{
File.Delete(filePath);
}
_nonExistingFile = filePath;
}
[Benchmark]
[Arguments(OneMibibyte, SixteenKibibytes, FileOptions.None)]
[Arguments(HundredMibibytes, SixteenKibibytes, FileOptions.None)]
public void Write_NoBuffering_PreallocationSize(long fileSize, int userBufferSize, FileOptions options)
{
byte[] userBuffer = _userBuffers[userBufferSize];
FileStreamOptions fsOptions = new () { Mode = FileMode.CreateNew, Access = FileAccess.Write, Share = FileShare.None,
BufferSize = 0, Options = options | FileOptions.DeleteOnClose, PreallocationSize = fileSize };
using (FileStream fileStream = new FileStream(_nonExistingFile, fsOptions))
{
for (int i = 0; i < fileSize / userBufferSize; i++)
{
fileStream.Write(userBuffer, 0, userBuffer.Length);
}
}
}
[Benchmark]
[BenchmarkCategory(Categories.NoWASM)]
[Arguments(OneMibibyte, SixteenKibibytes, FileOptions.Asynchronous)]
[Arguments(HundredMibibytes, SixteenKibibytes, FileOptions.Asynchronous)]
public async Task WriteAsync_NoBuffering_PreallocationSize(long fileSize, int userBufferSize, FileOptions options)
{
CancellationToken cancellationToken = CancellationToken.None;
Memory<byte> userBuffer = new Memory<byte>(_userBuffers[userBufferSize]);
FileStreamOptions fsOptions = new () { Mode = FileMode.CreateNew, Access = FileAccess.Write, Share = FileShare.None,
BufferSize = 0, Options = options | FileOptions.DeleteOnClose, PreallocationSize = fileSize };
using (FileStream fileStream = new FileStream(_nonExistingFile, fsOptions))
{
for (int i = 0; i < fileSize / userBufferSize; i++)
{
await fileStream.WriteAsync(userBuffer, cancellationToken);
}
}
}
#endif
[Benchmark]
[Arguments(OneKibibyte, FileOptions.None)]
[Arguments(OneKibibyte, FileOptions.Asynchronous)]
public void Flush(long fileSize, FileOptions options)
{
using (FileStream fileStream = new FileStream(_destinationFilePaths[fileSize], FileMode.Create, FileAccess.Write, FileShare.Read, FourKibibytes, options))
{
for (int i = 0; i < fileSize; i++)
{
fileStream.WriteByte(default); // make sure that Flush has something to actualy flush to disk
fileStream.Flush();
}
}
}
[BenchmarkCategory(Categories.NoWASM)]
[Benchmark]
[Arguments(OneKibibyte, FileOptions.None)]
[Arguments(OneKibibyte, FileOptions.Asynchronous)]
public async Task FlushAsync(long fileSize, FileOptions options)
{
using (FileStream fileStream = new FileStream(_destinationFilePaths[fileSize], FileMode.Create, FileAccess.Write, FileShare.Read, FourKibibytes, options))
{
for (int i = 0; i < fileSize; i++)
{
fileStream.WriteByte(default);
await fileStream.FlushAsync();
}
}
}
[Benchmark]
[Arguments(OneKibibyte, FileOptions.None)]
[Arguments(OneMibibyte, FileOptions.None)]
[Arguments(HundredMibibytes, FileOptions.None)]
public void CopyToFile(long fileSize, FileOptions options)
{
using (FileStream source = new FileStream(_sourceFilePaths[fileSize], FileMode.Open, FileAccess.Read, FileShare.Read, FourKibibytes, options))
using (FileStream destination = new FileStream(_destinationFilePaths[fileSize], FileMode.Create, FileAccess.Write, FileShare.Read, FourKibibytes, options))
{
source.CopyTo(destination);
}
}
[Benchmark]
[Arguments(OneKibibyte, FileOptions.Asynchronous)]
[Arguments(OneKibibyte, FileOptions.None)]
[Arguments(OneMibibyte, FileOptions.Asynchronous)]
[Arguments(OneMibibyte, FileOptions.None)]
[Arguments(HundredMibibytes, FileOptions.Asynchronous)]
[Arguments(HundredMibibytes, FileOptions.None)]
[BenchmarkCategory(Categories.NoWASM)]
public async Task CopyToFileAsync(long fileSize, FileOptions options)
{
using (FileStream source = new FileStream(_sourceFilePaths[fileSize], FileMode.Open, FileAccess.Read, FileShare.Read, FourKibibytes, options))
using (FileStream destination = new FileStream(_destinationFilePaths[fileSize], FileMode.Create, FileAccess.Write, FileShare.Read, FourKibibytes, options))
{
await source.CopyToAsync(destination);
}
}
}
}