Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does "fixed" get cleaned up properly if an Exception is thrown?

Tags:

c#

.net

unsafe

I'd assume that fixed is implemented similar to using/try..finally in that if the block terminates early (via return or throwing an Exception), the pointer gets properly cleaned up ("unfixed" so that the GC can do its work again).

However, I'm not seeing such a guarantee in the fixed documentation, so I'm wondering if there is some form of official guarantee somewhere, or if I should introduce a try..catch in every fixed block.

unsafe void FooUnsafe()
{
    var str = "Foo";
    try
    {
        fixed (char* pStr = str)
        {
            Console.WriteLine("First Char: " + (*pStr));
            throw new Exception("Test");
        }
    }
    catch (Exception ex) {
        Console.WriteLine($"Exception when working with {str}: {ex.Message}");      
    }
}
like image 213
Michael Stum Avatar asked Mar 06 '23 16:03

Michael Stum


2 Answers

From the documentation:

After the code in the statement is executed, any pinned variables are unpinned and subject to garbage collection.

fixed Statement (C# Reference)

like image 91
JuanR Avatar answered Mar 30 '23 00:03

JuanR


As commented by FCin

Pointer cannot live longer than the resource that it points to because C# protects it from happening because of Dangling Pointer. So if you point to a local variable, then it will definitely be disposed once the variable goes out of scope. In this situation, once the FooUnsafe returns.

Also noted by JuanR

fixed Statement (C# Reference)

After the code in the statement is executed, any pinned variables are unpinned and subject to garbage collection.

However, lets try and prove it with a simple example, and some snippets of information around the Interwebs

private static unsafe void Main()
{
   Console.WriteLine($"Total Memory: {GC.GetTotalMemory(false)}");

   var arr = new int[100000];

   Console.WriteLine($"Total Memory after new : {GC.GetTotalMemory(false)}");

   try
   {

      fixed (int* p = arr)
      {
         *p = 1;
         throw new Exception("rah");
      }

   }
   catch 
   {
   }

   Console.WriteLine($"Generation: {GC.GetGeneration(arr)}, Total Memory: {GC.GetTotalMemory(false)}");

   arr = null;
   GC.Collect();
   GC.WaitForPendingFinalizers();
   Console.WriteLine("Total Memory: {0}", GC.GetTotalMemory(false));
   Console.Read();
}

Results

Total Memory: 29948
Total Memory after new: 438172
Generation: 2, Total Memory: 438172
Total Memory: 29824

You will notice in IL the finally and ldnull

.try
{

   // [23 14 - 23 26]
   IL_0043: ldloc.0      // arr
   IL_0044: dup          
   IL_0045: stloc.2      // V_2
   IL_0046: brfalse.s    IL_004d
   IL_0048: ldloc.2      // V_2
   IL_0049: ldlen        
   IL_004a: conv.i4      
   IL_004b: brtrue.s     IL_0052
   IL_004d: ldc.i4.0     
   IL_004e: conv.u       
   IL_004f: stloc.1      // p
   IL_0050: br.s         IL_005b
   IL_0052: ldloc.2      // V_2
   IL_0053: ldc.i4.0     
   IL_0054: ldelema      [mscorlib]System.Int32
   IL_0059: conv.u       
   IL_005a: stloc.1      // p

   ...

} // end of .try
finally
{

   IL_006a: ldnull       
   IL_006b: stloc.2      // V_2
   IL_006c: endfinally   
} // end of finally

An interesting note, is you wont always see the finally as the compiler will optimize it out in certain situations

LocalRewriter_FixedStatement.cs in the Roslyn source

// In principle, the cleanup code (i.e. nulling out the pinned variables) is always
// in a finally block.  However, we can optimize finally away (keeping the cleanup
// code) in cases where both of the following are true:
//   1) there are no branches out of the fixed statement; and
//   2) the fixed statement is not in a try block (syntactic or synthesized).
if (IsInTryBlock(node) || HasGotoOut(rewrittenBody))
{

i.e even if it lives in a method like this

private static unsafe void test(int[] arr)
{
   fixed (int* p = arr)
   {
      *p = 1;
   }
}

You will notice

.method private hidebysig static void 
   test(
   int32[] arr
   ) cil managed 
{
   .maxstack 2
   .locals init (
   [0] int32* p,
   [1] int32[] pinned V_1
   )

   ...

   IL_001e: ldnull       
   IL_001f: stloc.1      // V_1

   // [54 7 - 54 8]
   IL_0020: ret          

} // end of method MyGCCollectClass::test

Some background

Standard ECMA-335 Common Language Infrastructure (CLI)

II.7.1.2 pinned The signature encoding for pinned shall appear only in signatures that describe local variables (§II.15.4.1.3). While a method with a pinned local variable is executing, the VES shall not relocate the object to which the local refers. That is, if the implementation of the CLI uses a garbage collector that moves objects, the collector shall not move objects that are referenced by an active pinned local variable.

[Rationale: If unmanaged pointers are used to dereference managed objects, these objects shall be pinned. This happens, for example, when a managed object is passed to a method designed to operate with unmanaged data. end rationale]

VES = Virtual Execution System CLI = Common Language Infrastructure CTS = Common Type System

Lastly, The JITer and the CLR aside, most of the ground work for pinning is done by the GC

In effect the GC has to get out of the way and leave the pinned local variable alone for the life-time of the method. Normally the GC is concerned about which objects are live or dead so that it knows what it has to clean up. But with pinned objects it has to go one step further, not only must it not clean up the object, but it must not move it around. Generally the GC likes to relocate objects around during the Compact Phase to make memory allocations cheap, but pinning prevents that as the object is being accessed via a pointer and therefore its memory address has to remain the same.

Obviously your main concern here is the Fragmentation Problem, and you are worried that the GC wont be able to clean it up.

enter image description here

However as seen by the example (and you can play with it your self), as soon as the ary goes out of scope and the fixed is finalized the GC will eventually free it completely.


Note : I am not a reputable source, and i could find no Official Confirmation however i thought these snippets of information i found might be of interest all the same

like image 37
TheGeneral Avatar answered Mar 30 '23 00:03

TheGeneral