In this article, we will find out the fastest way to read a text file in C#. We will discuss and implement various methods that we can use to perform this action. Subsequently, we will compare the performance of these methods using the BenchmarkDotNet library.
Let’s dive in!
Preparation of Source File
Before we discuss the different methods to access, let’s create a source file that we can use in all our scenarios.
First, let’s add a string to our project:
private const string ExpectedText = """ Integer facilisis ex libero, ut suscipit leo blandit non. Vivamus nec ipsum orci. Proin nec mauris dui. Proin at felis et eros commodo aliquet. Pellentesque lacinia porta leo, non accumsan turpis sagittis et. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere. """;
We will use this string to create a temporary file in our test class:
private static readonly string TempFilePath = Path.GetTempFileName();
The GetTempFileName()
method of the Path class creates a temporary file for us in our temporary folder. Then, it returns the full path to the file.
This file will serve as the sample text file for our test methods.
Read a Text File With the ReadAllLines Method
With all the configurations done, let’s look at the first method to read a text file:
public string UseFileReadAllLines() { var lines = File.ReadAllLines(_sampleFilePath); var stringBuilder = new StringBuilder(); foreach (var line in lines) { stringBuilder.AppendLine(line); } stringBuilder.Length -= Environment.NewLine.Length; return stringBuilder.ToString(); }
Here, we invoke the File.ReadAllLines()
method to read all the lines in our text file into the memory. This method reads and converts all the lines from our text file into elements within a string array. This string array is returned to us once the entire file has been read.
Then, we define a StringBuilder object that will contain the text from our file.
We utilize the StringBuilder.AppendLine()
method to append lines to the final string. However, it’s important to note that whenever we use this method, there might be some differences between the resulting string and the original content of the file.
These differences may arise because, by default, the AppendLine()
method uses the newline character set by Environment.NewLine
. If, for instance, the file uses \r
as its line ending character while the current system adopts \r\n
, the returned string might not accurately represent the initial text.
After that, we remove the last newline character by subtracting the Environment.NewLine.Length
property value from the stringBuilder
‘s length.
Finally, we build and return the string by calling the ToString()
method.
How to Read a Text File With the ReadAllText Method
Next up, is the ReadAllText()
method:
public string UseFileReadAllText() => File.ReadAllText(_sampleFilePath);
In this method, we invoke the File.ReadAllText()
method with our sample file path as the argument. With this concise method, we can directly read all the lines in our file and return them as a single string.
Use the ReadLines Method To Read a Text File
Another approach for reading a text file is by using the File.ReadLines()
method. To start, let’s define a UseFileReadLines()
method:
public string UseFileReadLines() { var lines = File.ReadLines(_sampleFilePath); var stringBuilder = new StringBuilder(); foreach (var line in lines) { stringBuilder.AppendLine(line); } stringBuilder.Length -= Environment.NewLine.Length; return stringBuilder.ToString(); }
Here, we make use of the File.ReadLines()
method to read through our text file line by line.
This method reads the lines from our file and returns an IEnumerable<string> instance that we can use to iterate over our file. Unlike the File.ReadAllLines()
method, this method doesn’t load the entire file into memory. Rather, it reads the file line by line in a lazy manner.
Therefore, during iteration over the IEnumerable
, each step loads a new line from our file into memory. After the loop, we remove the last newline character from the stringBuilder
.
Lastly, we build and return the string by calling the ToString()
method.
Read a Text File With the ReadLine Method
Now, let’s talk about the StreamReader.ReadLine()
method:
public string UseStreamReaderReadLine() { using var streamReader = new StreamReader(_sampleFilePath); var stringBuilder = new StringBuilder(); while (streamReader.ReadLine() is { } fileLine) { stringBuilder.AppendLine(fileLine); } stringBuilder.Length -= Environment.NewLine.Length; return stringBuilder.ToString(); }
Here, we read the lines in our file one by one using a StreamReader object.
We start by initializing a StreamReader
instance and providing it with the path to our sample file. We utilize the using
statement to ensure proper disposal of the StreamReader
at the end of the method execution.
Next, we define a StringBuilder
object that we will use in the loop.
Our main action takes place within the while
loop. In our loop condition, we make use of pattern-based matching to read lines from our file. As we read each line, we assign it to the variable fileLine
. This line is then added to our stringBuilder
instance using the stringBuilder.AppendLine()
method. This continues until we reach the end of our file and the StreamReader
returns a null
value.
Again, once we’ve finished reading the file, we make sure to delete all the newline characters at the end of our stringBuilder
. Then, we build and return the string by calling the ToString()
method.
Read a Text File With the ReadToEnd Method
Moving forward, we have the ReadToEnd()
method:
public string UseStreamReaderReadToEnd() { using var streamReader = new StreamReader(_sampleFilePath); return streamReader.ReadToEnd(); }
Here, we create a StreamReader
instance with our sample file.
Then, we read the entire content of the stream by invoking the StreamReader.ReadToEnd()
method.
With this method, we take a different route by reading the entire file content directly into memory all at once.
Use the ReadBlock Method
Additionally, we can use the StreamReader.ReadBlock()
method to read a text file:
public string UseStreamReaderReadBlock() { using var streamReader = new StreamReader(_sampleFilePath); var buffer = new char[4096]; int numberRead; var stringBuilder = new StringBuilder(); while ((numberRead = streamReader.ReadBlock(buffer, 0, buffer.Length)) > 0) { stringBuilder.Append(buffer[..numberRead]); } return stringBuilder.ToString(); }
Initially, we define a StreamReader
for our target file. Then, we create an array of 4096 characters, as this is the default buffer size that FileStream
uses.
Also, we define an integer, numberRead
to store the number of characters we read during each iteration of the loop. Next, we define a StringBuilder
object that we will use in the loop.
In each iteration of the loop, we check whether the number of characters read is still positive. If it is, that means there’s more content in our stream. So, we read the data and add it to our StringBuilder
instance.
Inside the loop, we pass in characters from the start of the buffer array up to the character at index numberRead - 1
to the Append()
method. With this, we append the string characters that have just been read during the ReadBlock()
method operation. We repeat this process until the value of numberRead
drops to zero or lower, signifying the end of our file, and then we exit the loop.
When we finish the while loop, we build and return the string by calling the ToString()
method.
Moreover, we can utilize the ArrayPool class from the System.Buffer
namespace to define our buffer:
public string UseStreamReaderReadBlockWithArrayPool() { using var streamReader = new StreamReader(_sampleFilePath); var buffer = ArrayPool<char>.Shared.Rent(4096); int numberRead; var stringBuilder = new StringBuilder(); while ((numberRead = streamReader.ReadBlock(buffer, 0, buffer.Length)) > 0) { stringBuilder.Append(buffer[..numberRead]); } ArrayPool<char>.Shared.Return(buffer); return stringBuilder.ToString(); }
Here, our buffer is a shared instance of the ArrayPool<char>
class. We create this instance using the Shared
property of the ArrayPool
class.
The ArrayPool
is a class in .NET that helps us to efficiently create and reuse array instances. By using this class, we reduce our application’s memory demands and enhance its overall performance.
Then, we proceed to call the Rent()
method to retrieve a buffer with a length of 4096. After we are done with the buffer, we return it to the ArrayPool
by invoking the Return()
method. At the end, we build and return the string by calling the ToString()
method.
Read a Text File With the ReadBlock Method and the Span Class
Here, let’s look at another way of using the ReadBlock()
method by incorporating the Span class:
public string UseStreamReaderReadBlockWithSpan() { using var streamReader = new StreamReader(_sampleFilePath); var buffer = new char[4096].AsSpan(); int numberRead; var stringBuilder = new StringBuilder(); while ((numberRead = streamReader.ReadBlock(buffer)) > 0) { stringBuilder.Append(buffer[..numberRead]); } return stringBuilder.ToString(); }
In this method, we convert our buffer to a Span<char>
using the AsSpan()
method.
Then, within our while loop definition, we simplify the ReadBlock()
method invocation by passing only our buffer, now as a Span
. Then we iterate through the file and append the characters in it to our stringBuilder
object.
When the loop ends, we build and return the string.
How to Read a Text File With a BufferedStream Object
Next, let’s see how to read a text file by creating a BufferedStream object:
public string UseBufferedStreamObjectWithNoFileStreamBuffer() { var stringBuilder = new StringBuilder(); using var fileStream = new FileStream(_sampleFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); using var bufferedStream = new BufferedStream(fileStream); using var streamReader = new StreamReader(bufferedStream); while (streamReader.ReadLine() is { } fileLine) { stringBuilder.AppendLine(fileLine); } stringBuilder.Length -= Environment.NewLine.Length; return stringBuilder.ToString(); }
In this method, first, we create a new StringBuilder
to gather our text. After that, we create an open FileStream
instance from our sample file. With that FileStream
, we set up a BufferedStream
. And using that BufferedStream
, we create a StreamReader
.
Now comes the real action – the while
loop. As long as we keep getting lines using the StreamReader.ReadLine()
method, we add them to our stringBuilder
using the AppendLine()
method. The moment fileLine
becomes null, it’s a sign that we’re done reading from our bufferedStream
. It also means that we are at the end of our file and we break out of the loop.
When we are done with everything, we remove the last newline character and build and return the string by calling the ToString()
method.
Although this method does the reading accurately, the FileStream
class employs an internal buffer of 4096 bytes by default. This can lead to double buffering, the buffering of the FileStream
and the buffering of the BufferedStream
.
Read a Text File With a BufferedStream Object While FileStream Buffering Is Disabled
Lastly, let’s address how we can handle the “double buffering” situation. To do this, let’s disable our FileStream
buffering:
public string UseBufferedStreamObjectWithNoFileStreamBuffer() { var stringBuilder = new StringBuilder(); using var fileStream = new FileStream(_sampleFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, 0); using var bufferedStream = new BufferedStream(fileStream); using var streamReader = new StreamReader(bufferedStream); while (streamReader.ReadLine() is { } fileLine) { stringBuilder.AppendLine(fileLine); } stringBuilder.Length -= Environment.NewLine.Length; return stringBuilder.ToString(); }
In the FileStream
constructor, we set a bufferSize
of 0 to disable buffering. After this, we go through the same process and then return the string.
Test the Performance of the Ways to Read a Text File in C#
With all our methods ready, we can now compare their time and memory performance. To help us with this, we are going to utilize the BenchmarkDotNet library.
Now, let’s run our benchmark and inspect the results on the console:
| Method | Mean | Error | StdDev | Median | Allocated | |------------------------------------------- |---------:|----------:|----------:|---------:|----------:| | StreamReaderReadToEnd | 1.517 ms | 0.0300 ms | 0.0390 ms | 1.506 ms | 410.81 KB | | StreamReaderReadBlockWithSpan | 1.557 ms | 0.0288 ms | 0.0385 ms | 1.567 ms | 418.69 KB | | StreamReaderReadBlockWithArrayPool | 1.696 ms | 0.0333 ms | 0.0342 ms | 1.704 ms | 609.87 KB | | StreamReaderReadBlock | 1.698 ms | 0.0201 ms | 0.0178 ms | 1.694 ms | 617.9 KB | | FileReadAllLines | 1.779 ms | 0.0301 ms | 0.0267 ms | 1.773 ms | 691.01 KB | | BufferedStreamObject | 1.787 ms | 0.0335 ms | 0.0329 ms | 1.775 ms | 666.96 KB | | BufferedStreamWithNoFileStreamBuffer | 1.815 ms | 0.0303 ms | 0.0404 ms | 1.812 ms | 666.9 KB | | FileReadAllText | 1.882 ms | 0.0609 ms | 0.1796 ms | 1.936 ms | 410.81 KB | | StreamReaderReadLine | 1.977 ms | 0.0940 ms | 0.2757 ms | 1.832 ms | 666.9 KB | | FileReadLines | 2.508 ms | 0.0642 ms | 0.1893 ms | 2.543 ms | 666.95 KB |
As we can see from our benchmark results, the fastest way to read a text file in C# is by using the StreamReader.ReadToEnd()
method. This is because this method reads the entire content of our file into a string in only one I/O operation.
Following that, we have the three StreamReader.ReadBlock()
methods. The one that uses a Span
is the fastest and uses less memory than the other two variants.
The ReadBlock()
methods are quite fast because they read the content of our file in blocks. This is generally faster than reading it character by character or line by line.
At the bottom of our ranking, the StreamReader.ReadLine()
method and the File.ReadLines()
method are the slowest methods in our benchmark. The reason for this is that they read our file one line at a time, leading to a high number of I/O operations.
It’s worth noting that methods like the StreamReader.ReadToEnd()
method, which loads the complete file into memory, can be effective for small files. However, when dealing with larger files, such methods may lead to substantial memory allocation, potentially impacting the overall performance of our application. In such scenarios, methods like the File.ReadLines()
method may be a more suitable choice.
Conclusion
In this article, we have seen ten methods that we can use to read a text file in C#. In the end, we compared the time and memory performance of all the methods using benchmark tests. From our benchmark results, we concluded that the StreamReader.ReadToEnd() method is the fastest way to read a text file in C#.
Hi, could you please point me a right direction about this line
while (streamReader.ReadLine() is { } fileLine)
. I have never seen this empty curly brackets {} syntax and cannot get my head around it. Thank you!!!Hi. This could definitely help: https://devblogs.microsoft.com/dotnet/do-more-with-patterns-in-c-8-0/#property-patterns