Search code examples
c#usingusing-statement

Effective usage of the using-statement (not in MSDN)


I already read the corresponding Doc-Page, but my question is still not answered. Assume I want to use a disposable Object in a while loop, like this:

StreamReader reader;
while (!ShouldStop)
{
    using (reader = new StreamReader(networkStream))
    {
         // Some code here...    
    }
}

How you can see, I declare StreamReade reader outside the using-statement. I usually do this because I think that then an area of the memory is being allocated for that StreamReader only one time. And when I use the using-statement like this:

while (!ShouldStop)
{
    using (StreamReader reader = new StreamReader(networkStream))
    {
        // Some code here...            
    }
}

I think that there's a continuous allocation of the memory for the StreamReader-object and so it is much more less efficient & perfomant. However I dont know if the first usage of the using-statement calls the instance's Dispose()-function regularly. So does the first usage of the using-statement the same as the 2nd usage?


Solution

  • I usually do this because I think that then an area of the memory is being allocated for that StreamReader only one time.

    That's what you're getting wrong.

    There is a certain amount of stack space taken for the local variable for the duration of its use, and a certain amount of heap space taken by the object upon new. This isn't going to change.

    Indeed, the compiler ends up taking slightly more stack space with your approach. Just compare the IL of two methods using each approach. We'll use this C#:

    private static string LastLine1(NetworkStream networkStream)
    {
        string last = null;
        StreamReader reader;
        while(!ShouldStop)
        {
            using(reader = new StreamReader(networkStream))
            {
                string line = reader.ReadLine();
                if(line != null)
                    last = line;
            }
        }
        return last;
    }
    private static string LastLine2(NetworkStream networkStream)
    {
        string last = null;
        while(!ShouldStop)
        {
            using(StreamReader reader = new StreamReader(networkStream))
            {
                string line = reader.ReadLine();
                if(line != null)
                    last = line;
            }
        }
        return last;
    }
    

    And we get this CIL:

    .method private hidebysig static 
        string LastLine1 (
            class [System]System.Net.Sockets.NetworkStream networkStream
        ) cil managed 
    {
        .maxstack 2
        .locals init (
            [0] string,
            [1] class [mscorlib]System.IO.StreamReader,
            [2] string,
            [3] class [mscorlib]System.IO.StreamReader
        )
    
        IL_0000: ldnull
        IL_0001: stloc.0
        IL_0002: br.s IL_0025
        IL_0004: ldarg.0
        IL_0005: newobj instance void [mscorlib]System.IO.StreamReader::.ctor(class [mscorlib]System.IO.Stream)
        IL_000a: dup
        IL_000b: stloc.1
        IL_000c: stloc.3
        .try
        {
            IL_000d: ldloc.1
            IL_000e: callvirt instance string [mscorlib]System.IO.TextReader::ReadLine()
            IL_0013: stloc.2
            IL_0014: ldloc.2
            IL_0015: brfalse.s IL_0019
    
            IL_0017: ldloc.2
            IL_0018: stloc.0
    
            IL_0019: leave.s IL_0025
        }
        finally
        {
            IL_001b: ldloc.3
            IL_001c: brfalse.s IL_0024
    
            IL_001e: ldloc.3
            IL_001f: callvirt instance void [mscorlib]System.IDisposable::Dispose()
    
            IL_0024: endfinally
        }
        IL_0025: call bool Demonstrate.Program::get_ShouldStop()
        IL_002a: brfalse.s IL_0004
    
        IL_002c: ldloc.0
        IL_002d: ret
    }
    
    .method private hidebysig static 
        string LastLine2 (
            class [System]System.Net.Sockets.NetworkStream networkStream
        ) cil managed 
    {
        .maxstack 1
        .locals init (
            [0] string,
            [1] class [mscorlib]System.IO.StreamReader,
            [2] string
        )
    
        IL_0000: ldnull
        IL_0001: stloc.0
        IL_0002: br.s IL_0023
        IL_0004: ldarg.0
        IL_0005: newobj instance void [mscorlib]System.IO.StreamReader::.ctor(class [mscorlib]System.IO.Stream)
        IL_000a: stloc.1
        .try
        {
            IL_000b: ldloc.1
            IL_000c: callvirt instance string [mscorlib]System.IO.TextReader::ReadLine()
            IL_0011: stloc.2
            IL_0012: ldloc.2
            IL_0013: brfalse.s IL_0017
    
            IL_0015: ldloc.2
            IL_0016: stloc.0
    
            IL_0017: leave.s IL_0023
        }
        finally
        {
            IL_0019: ldloc.1
            IL_001a: brfalse.s IL_0022
    
            IL_001c: ldloc.1
            IL_001d: callvirt instance void [mscorlib]System.IDisposable::Dispose()
    
            IL_0022: endfinally
        }
    
        IL_0023: call bool Demonstrate.Program::get_ShouldStop()
        IL_0028: brfalse.s IL_0004
    
        IL_002a: ldloc.0
        IL_002b: ret
    }
    

    (Strictly, the two should really have resulted in identical code, but the fact is they didn't, and your approach was the slightly longer and slightly greater use of stack space).

    Because the C# compiler wasn't able to optimise away your having reader outside of the using, it's actually your approach that results in extra stack space being taken up for another copy of the reader.

    If you aren't familiar with CIL, compare how ILSpy tries to decompile these back to C# again:

    private static string LastLine1(NetworkStream networkStream)
    {
        string result = null;
        while (!Program.ShouldStop)
        {
            StreamReader streamReader2;
            StreamReader streamReader = streamReader2 = new StreamReader(networkStream);
            try
            {
                string text = streamReader.ReadLine();
                if (text != null)
                {
                    result = text;
                }
            }
            finally
            {
                if (streamReader2 != null)
                {
                    ((IDisposable)streamReader2).Dispose();
                }
            }
        }
        return result;
    }
    
    private static string LastLine2(NetworkStream networkStream)
    {
        string result = null;
        while (!Program.ShouldStop)
        {
            using (StreamReader streamReader = new StreamReader(networkStream))
            {
                string text = streamReader.ReadLine();
                if (text != null)
                {
                    result = text;
                }
            }
        }
        return result;
    }
    

    (It's also possible that you've reduced the chances of the null-check being optimised away when this is then turned into machine code that is actually run).

    it is much more less efficient & perfomant. However I dont know if the first usage of the using-statement calls the instance's Dispose()-function regularly.

    The using is calling Dispose() fine either way, however you are being slightly more wasteful and hence slightly less efficient and performant. Probably negligible, but the approach you avoid is certainly not "much more less efficient & performant" as you claim.


    In general, keep your scopes tight. The main reason is that a variable that's no longer in scope is a variable you can no longer do something wrong with or even have to think about, so you'll have cleaner code with fewer bugs and where the bugs are more easily found. A secondary reason is that there are a few cases, like this, where a wider scope results in very slightly more wasteful code.

    Now, putting assignments outside of a loop can indeed be more performant. If your code could work with:

    using(var reader = new StreamReader(networkStream))
      while(!ShouldStop)
      {
        // do stuff
      }
    

    Then that would save heap churn and most of all do less and therefore if it would work, it would be an improvement.

    Declarations however don't do anything, so it doesn't help to have them outside of loops, and sometimes slightly hinders.