Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit dadbb55

Browse files
committed
Fix regression in StreamWriter.Write perf for small inputs (#17251)
* Streamline StreamWriter/Reader.CheckAsyncTaskInProgress and make it inlineable * Re-inline StreamWriter.WriteSpan Prior to my span changes, the code in WriteCore was manually inlined into each of its call sites. I'd refactored it out into a shared helper to reduce code duplication, but that introduced a regression for small inputs. I'm keeping the factoring but making it AggressiveInlining to avoid that regression while still keeping the C# code reuse. * Use NoInlining on WriteSpan call sites In case methods get devirtualized and JIT attempts to inline them and WriteSpan. Signed-off-by: dotnet-bot-corefx-mirror <dotnet-bot@microsoft.com>
1 parent 305bebc commit dadbb55

File tree

2 files changed

+45
-35
lines changed

2 files changed

+45
-35
lines changed

src/Common/src/CoreLib/System/IO/StreamReader.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,21 +70,21 @@ public class StreamReader : TextReader
7070
// We don't guarantee thread safety on StreamReader, but we should at
7171
// least prevent users from trying to read anything while an Async
7272
// read from the same thread is in progress.
73-
private volatile Task _asyncReadTask;
73+
private Task _asyncReadTask = Task.CompletedTask;
7474

7575
private void CheckAsyncTaskInProgress()
7676
{
7777
// We are not locking the access to _asyncReadTask because this is not meant to guarantee thread safety.
7878
// We are simply trying to deter calling any Read APIs while an async Read from the same thread is in progress.
79-
80-
Task t = _asyncReadTask;
81-
82-
if (t != null && !t.IsCompleted)
79+
if (!_asyncReadTask.IsCompleted)
8380
{
84-
throw new InvalidOperationException(SR.InvalidOperation_AsyncIOInProgress);
81+
ThrowAsyncIOInProgress();
8582
}
8683
}
8784

85+
private static void ThrowAsyncIOInProgress() =>
86+
throw new InvalidOperationException(SR.InvalidOperation_AsyncIOInProgress);
87+
8888
// StreamReader by default will ignore illegal UTF8 characters. We don't want to
8989
// throw here because we want to be able to read ill-formed data without choking.
9090
// The high level goal is to be tolerant of encoding errors when we read and very strict

src/Common/src/CoreLib/System/IO/StreamWriter.cs

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System.Diagnostics;
6+
using System.Runtime.CompilerServices;
67
using System.Runtime.InteropServices;
78
using System.Text;
89
using System.Threading;
@@ -44,19 +45,21 @@ public class StreamWriter : TextWriter
4445
// We don't guarantee thread safety on StreamWriter, but we should at
4546
// least prevent users from trying to write anything while an Async
4647
// write from the same thread is in progress.
47-
private volatile Task _asyncWriteTask;
48+
private Task _asyncWriteTask = Task.CompletedTask;
4849

4950
private void CheckAsyncTaskInProgress()
5051
{
5152
// We are not locking the access to _asyncWriteTask because this is not meant to guarantee thread safety.
5253
// We are simply trying to deter calling any Write APIs while an async Write from the same thread is in progress.
53-
54-
Task t = _asyncWriteTask;
55-
56-
if (t != null && !t.IsCompleted)
57-
throw new InvalidOperationException(SR.InvalidOperation_AsyncIOInProgress);
54+
if (!_asyncWriteTask.IsCompleted)
55+
{
56+
ThrowAsyncIOInProgress();
57+
}
5858
}
5959

60+
private static void ThrowAsyncIOInProgress() =>
61+
throw new InvalidOperationException(SR.InvalidOperation_AsyncIOInProgress);
62+
6063
// The high level goal is to be tolerant of encoding errors when we read and very strict
6164
// when we write. Hence, default StreamWriter encoding will throw on encoding error.
6265
// Note: when StreamWriter throws on invalid encoding chars (for ex, high surrogate character
@@ -323,14 +326,13 @@ public override void Write(char value)
323326
}
324327
}
325328

329+
[MethodImpl(MethodImplOptions.NoInlining)] // prevent WriteSpan from bloating call sites
326330
public override void Write(char[] buffer)
327331
{
328-
if (buffer != null)
329-
{
330-
WriteCore(buffer, _autoFlush);
331-
}
332+
WriteSpan(buffer, appendNewLine: false);
332333
}
333334

335+
[MethodImpl(MethodImplOptions.NoInlining)] // prevent WriteSpan from bloating call sites
334336
public override void Write(char[] buffer, int index, int count)
335337
{
336338
if (buffer == null)
@@ -350,14 +352,15 @@ public override void Write(char[] buffer, int index, int count)
350352
throw new ArgumentException(SR.Argument_InvalidOffLen);
351353
}
352354

353-
WriteCore(new ReadOnlySpan<char>(buffer, index, count), _autoFlush);
355+
WriteSpan(buffer.AsSpan(index, count), appendNewLine: false);
354356
}
355357

358+
[MethodImpl(MethodImplOptions.NoInlining)] // prevent WriteSpan from bloating call sites
356359
public override void Write(ReadOnlySpan<char> buffer)
357360
{
358361
if (GetType() == typeof(StreamWriter))
359362
{
360-
WriteCore(buffer, _autoFlush);
363+
WriteSpan(buffer, appendNewLine: false);
361364
}
362365
else
363366
{
@@ -367,7 +370,8 @@ public override void Write(ReadOnlySpan<char> buffer)
367370
}
368371
}
369372

370-
private unsafe void WriteCore(ReadOnlySpan<char> buffer, bool autoFlush)
373+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
374+
private unsafe void WriteSpan(ReadOnlySpan<char> buffer, bool appendNewLine)
371375
{
372376
CheckAsyncTaskInProgress();
373377

@@ -422,41 +426,47 @@ private unsafe void WriteCore(ReadOnlySpan<char> buffer, bool autoFlush)
422426
}
423427
}
424428

425-
if (autoFlush)
429+
if (appendNewLine)
430+
{
431+
char[] coreNewLine = CoreNewLine;
432+
for (int i = 0; i < coreNewLine.Length; i++) // Generally 1 (\n) or 2 (\r\n) iterations
433+
{
434+
if (_charPos == _charLen)
435+
{
436+
Flush(false, false);
437+
}
438+
439+
_charBuffer[_charPos] = coreNewLine[i];
440+
_charPos++;
441+
}
442+
}
443+
444+
if (_autoFlush)
426445
{
427446
Flush(true, false);
428447
}
429448
}
430449

450+
[MethodImpl(MethodImplOptions.NoInlining)] // prevent WriteSpan from bloating call sites
431451
public override void Write(string value)
432452
{
433-
if (value != null)
434-
{
435-
WriteCore(value.AsSpan(), _autoFlush);
436-
}
453+
WriteSpan(value, appendNewLine: false);
437454
}
438455

439-
//
440-
// Optimize the most commonly used WriteLine overload. This optimization is important for System.Console in particular
441-
// because of it will make one WriteLine equal to one call to the OS instead of two in the common case.
442-
//
456+
[MethodImpl(MethodImplOptions.NoInlining)] // prevent WriteSpan from bloating call sites
443457
public override void WriteLine(string value)
444458
{
445459
CheckAsyncTaskInProgress();
446-
if (value != null)
447-
{
448-
WriteCore(value.AsSpan(), autoFlush: false);
449-
}
450-
WriteCore(new ReadOnlySpan<char>(CoreNewLine), autoFlush: true);
460+
WriteSpan(value, appendNewLine: true);
451461
}
452462

463+
[MethodImpl(MethodImplOptions.NoInlining)] // prevent WriteSpan from bloating call sites
453464
public override void WriteLine(ReadOnlySpan<char> value)
454465
{
455466
if (GetType() == typeof(StreamWriter))
456467
{
457468
CheckAsyncTaskInProgress();
458-
WriteCore(value, autoFlush: false);
459-
WriteCore(new ReadOnlySpan<char>(CoreNewLine), autoFlush: true);
469+
WriteSpan(value, appendNewLine: true);
460470
}
461471
else
462472
{

0 commit comments

Comments
 (0)