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}");
}
}
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)
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With