The StreamReader.ReadLine() implementation that ships with .NET has been suprisingly decent in recent years, and with current .NET versions it is likely the fastest thing you will ever need in most practical scenarios. It is not like the early times where you had to bring a home-brewed line reader to coding challenges in order to avoid a time-out simply reading the inputs.
In fact, if you do stuff with the lines you read - take substrings, call string.Split() or int.Parse() and so on - then the time taken by StreamReader.ReadLine() often becomes negligible compared to the rest.
Having said that, StreamReader.ReadLine() does have an obvious Achilles heel and that is the requirement to allocate a string for each newly-read line. You can make things significantly faster by avoiding this allocation and processing the line in byte form directly in the buffer where the input routine has placed the raw bytes of a chunk of your input file.
It used to be that the fastest way of scanning lines was placing a sentinel line feed after the valid portion of the data buffer and doing something on the lines of
for (var b = m_buffer; b[h] != ASC_LF; ) ++h;
No need to check the current offset against an end offset or something like that, because if there is nothing to find in the valid portion of the buffer then the scan will simply run into the sentinel.
However, .NET like Java insists on doing a length check anyway (native code listing taken from the 'IL+Native' display of the amazingly amazing LINQPad):
L0060 inc esi L0062 cmp esi, ecx L0064 jae short L00db L0066 mov edx, esi L0068 cmp byte ptr [rax+rdx+0x10], 0xa L006d jne short L0060
With modern processors this makes no odds because of branch prediction and superscalar execution, but 20 years ago things were a lot different.
The length check can be eliminated by invoking the dark side of the Force like so:
unsafe { fixed (byte *buffer_h = &m_buffer[h]) { var p = buffer_h; while (*p != ASC_LF) ++p; h += (int) (p - buffer_h); } }
This gives the following machine code for the loop proper:
L006f inc rcx L0072 cmp byte ptr [rcx], 0xa L0075 jne short L006f
Lean and mean, but it doesn't make any difference because modern processors have been bred to wade through bloated junk with the same efficiency as if they were eating finely crafted hand-smithed code.
But we can go faster still. For a couple of years now, Array.IndexOf() has been slightly but consistently faster than the hand-rolled loop even for the .NET Framework, but on newer .NET versions it makes the line reading twice as fast because it can use vectorisation under the hood.
So nowadays the scan for line feeds looks somewhat like this (no sentinels needed anymore, yay):
h = Array.IndexOf(m_buffer, ASC_LF, h, unread_bytes_left);
I've used a little line reader class based on these principles for over ten years now, especially for parsing huge logs (up to several gigabytes per file).
Here's a timing comparison for a 512 MiB HTTP log file that is my current reference for performance measurements. It resides on an SSD and it contains full HTTP messages as captured on the wire; I'm using it as reference because it represents the toughest production use case for my parsing code, but it has been cut down somewhat so as not to try my patience too much.
3526760 lines 536870912 bytes 106.7 ms 33041 lines/ms via ZU.LineReader.BufferNextLine() 3526760 lines 536870912 bytes 111.9 ms 31531 lines/ms via ZU.LineReader.BufferNextLine() 3526760 lines 536870912 bytes 111.9 ms 31524 lines/ms via ZU.LineReader.BufferNextLine() 3526760 lines - bytes 411.5 ms 8570 lines/ms via StreamReader.ReadLine() 3526760 lines - bytes 380.6 ms 9264 lines/ms via StreamReader.ReadLine() 3526760 lines - bytes 388.1 ms 9086 lines/ms via StreamReader.ReadLine()
Note: there are not byte counts for StreamReader.ReadLine() because it delivers text, not bytes.
The line reader used to be 10 times as fast as StreamReader.ReadLine(), now it is not even 4 times. Convert the line bytes to a string and suddenly the whole shebang is hardly any faster than StreamReader.ReadLine(). That should give you an idea how fast the stock StreamReader.ReadLine() actually is!
To be significantly faster than code based on StreamReader.ReadLine() you have to process data in the byte domain, not as text, and you have to avoid or minimise allocations (e.g. ValueTask instead of Task for async code). For coding challenges, consider hand-rolled number-to-text or text-to-number conversions that process multiple digits at a time, and - in the case of number-to-text - placing converted digits directly in the output buffer at the proper position instead of converting into a separate buffer and then copying stuff around.
Part of my test/monitoring code is a little class for allocation-free async parsing of HTTP messages (HTTP/1.0 and HTTP/1.1), which is based on exactly the same principles as the line reader. When fed our typical API traffic from a MemoryStream instead of a network stream then it buffers lines to the tune of 140000 per millisecond per core and it parses HTTP messages at a rate of 3000 per millisecond per core on my laptop¹.
So, you can get a lot faster than StreamReader.ReadLine() if you need to, but it is not exactly easy. For an idea of how involved it gets to push the boundaries, have a look at the Kestrel source code. You can study that amazing thing for months and years and still discover new tricks. Kudos.
¹) in this bench the data comes from the processor's L2 and L3 caches, so it is not representative for real-world usage; I'm using it for finding and eliminating unnecessary slow-downs in the code
Fastestyou mean from performance or development perspectives?filestream = new FileStreaminusing()statement to avoid possible annoying issues with locked file handle