Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

non executing linq causing memory allocation C#

While analyzing the .NET memory allocation of my code with the Visual Studio 2013 performance wizard I noticed a certain function allocating a lot of bytes (since it is called in a large loop). But looking at the function highlighted in the profiling report I didn't understand why it was allocating any memory at all.

To better understand what happened I isolated the code causing the allocations. This was similar to the LinqAllocationTester class below.

Once I commented out the LINQ code in that function, which was never executed anyway in the tested code path, no memory was allocated anymore. The NonLinqAllocationTester class imitates this behavior. Replacing the LINQ code with a normal loop also let to no memory allocations.

If I run the .NET memory allocation test on the test code below it shows that the LinqAllocationTester causes 100.000 allocations (1 per call), while the NonLinqAllocationTester has none. Note that useLinq is always false, so the LINQ code itself is never actually being executed.

Function Name                    | Inclusive   | Exclusive   | Inclusive | Exclusive
                                 | Allocations | Allocations | Bytes     | Bytes
-------------------------------------------------------------------------------------
LinqAllocationTester.Test(int32) |     100.000 |     100.000 | 1.200.000 | 1.200.000
Program.Main(string[])           |     100.000 |           0 | 1.200.000 |         0

So why does the non executing LINQ code cause memory allocations? And is there a way to prevent this besides avoiding LINQ functions?

class Program {
    static void Main(string[] args) {
        List<int> values = new List<int>() { 1, 2, 3, 4 };
        LinqAllocationTester linqTester = new LinqAllocationTester(false, values);
        NonLinqAllocationTester nonLinqTester = new NonLinqAllocationTester(false, values);

        for (int i = 0; i < 100000; i++) {
            linqTester.MaxDifference(i);
        }

        for (int i = 0; i < 100000; i++) {
            nonLinqTester.MaxDifference(i);
        }
    }
}

internal class LinqAllocationTester {
    private bool useLinq;
    private List<int> values;

    public LinqAllocationTester(bool useLinq, List<int> values) {
        this.useLinq = useLinq;
        this.values = values;
    }

    public int MaxDifference(int value) {
        if (useLinq) {
            return values.Max(x => Math.Abs(value - x));
        } else {
            int maxDifference = int.MinValue;
            foreach (int value2 in values) {
                maxDifference = Math.Max(maxDifference, Math.Abs(value - value2));
            }
            return maxDifference;
        }
    }
}

internal class NonLinqAllocationTester {
    private bool useLinq;
    private List<int> values;

    public NonLinqAllocationTester(bool useLinq, List<int> values) {
        this.useLinq = useLinq;
        this.values = values;
    }

    public int MaxDifference(int value) {
        if (useLinq) {
            return 0;
        } else {
            int maxDifference = int.MinValue;
            foreach (int value2 in values) {
                maxDifference = Math.Max(maxDifference, Math.Abs(value - value2));
            }
            return maxDifference;
        }
    }
}
like image 317
Bas Weitjens Avatar asked Jan 28 '15 15:01

Bas Weitjens


1 Answers

You can take a peek at the generated IL to see that the DisplayClass for the LINQ expression will be initialized in the beginning of the method outside the first if-branch. That is because it is generating the closure for the lambda expression at the beginning of the method (where the value first appears).

IL:

IL_0000: ldnull
IL_0001: stloc.2
IL_0002: newobj instance void ConsoleApplication2.LinqAllocationTester/'<>c__DisplayClass2'::.ctor()
IL_0007: stloc.3
IL_0008: ldloc.3
IL_0009: ldarg.1
IL_000a: stfld int32 ConsoleApplication2.LinqAllocationTester/'<>c__DisplayClass2'::'value'
IL_000f: nop
IL_0010: ldarg.0
IL_0011: ldfld bool ConsoleApplication2.LinqAllocationTester::useLinq
IL_0016: ldc.i4.0
IL_0017: ceq
IL_0019: stloc.s CS$4$0001
IL_001b: ldloc.s CS$4$0001
IL_001d: brtrue.s IL_0042

If you copy your value to a narrower scoped variable like this:

if (useLinq)
{
    int value2 = value;
    return values.Max(x => Math.Abs(value2 - x));
}

the extra allocations should not happen anymore.

like image 146
GaussZ Avatar answered Oct 21 '22 11:10

GaussZ