Home

Awesome

Cistern.ValueLinq

In the tradition of Kevin Montrose's LinqAF, a version of Linq that has minimal allocations. (Another rendition on this there is @reegeek's StructLinq).

NOTE THAT THIS A WIP; NOT ALL OPERATORS ARE SUPPORTED (ALTHOUGH THEY ALL "WORK" BECAUSE THEY ARE PATCHED TO SYSTEM.LINQ)

So given that there area already a couple of version of a value based linq, what does this one bring to the table?

Anyway, here is simple example which shows:

        [Benchmark(Baseline = true)]
        public double Handcoded()
        {
            return _ints switch
            {
                int[] asArray => AsArray(asArray),
                _ => throw new NotSupportedException(),
            };

            static int AsArray(int[] asArray)
            {
                var max = int.MinValue;
                foreach (var n in asArray)
                {
                    var x = n / 2;
                    if ((x & 1) == 0)
                    {
                        if (x > max)
                            max = x;
                    }
                }
                return max;
            }
        }

OK; Pretty simple, the linq transform of this is:

        [Benchmark]
        public int Linq() =>
            _ints
            .Select(x => x / 2)
            .Where(x => (x & 1) == 0)
            .Max();

And, the Cistern.ValueLinq looks exactly the same:

        [Benchmark]
        public double CisternValueLinq_normal() =>
            _ints
            .Select(x => x / 2)
            .Where(x => (x & 1) == 0)
            .Max();

OK, and here is an example of the 'ugly' where we use value-type lambdas to perform the actions. These are pure functions. They could copy some state around, but lead to some perverse outcomes.

        struct HalveAnInt : IFunc<int, int> { public int Invoke(int t) => t / 2; } 
        struct FilterEvenInts : IFunc<int, bool> { public bool Invoke(int t) => (t & 1) == 0; }

And then we can use them in my new Linq as follows

        [Benchmark]
        public int CisternValueLinq_ValueFuncs() =>
            _ints
            .Select(new HalveAnInt(), default(int)) // ug, sugar please + better type inference...
            .Where(new FilterEvenInts()) // ug, sugar please
            .Max();

Notice that we have to supply the output type for the where (via default(int)) due to the way that c# handles type inference of generic arguments with constraints.

Now, the following doesn't exist, but I'm imagining you could have an additional syntax something like the following, where a >=> would create a value type IFunc used in the above example, and allow a layer of type-inference to do some magic. But hey, one can but dream:

        [Benchmark]
        public double CisternValueLinq_struct_in_dream_land() =>
            _ints
            .Select(t >=> t / 2)
            .Where(t >=> (t & 1) == 0)
            .Max();

And finally a "nothing up my sleave" version that seemly switches between the value type representation and the IEnumerable<> for an example of the trivial interop with existing code. It also shows that segments created in different parts can all be just put together with an Aggregating function, and hence all the magic that ties together everything at runtime.

        [Benchmark]
        public double CisternValueLinq_struct_nothing_up_my_sleve()
        {
            IEnumerable<int> collection = GetCollection();
            IEnumerable<int> andSelect = AddSelect(collection);
            IEnumerable<int> withWhere = AddWhere(andSelect);

            var result = andSelect.Max();

            return result;

            IEnumerable<int> GetCollection() => _ints;
            IEnumerable<int> AddSelect(IEnumerable<int> stuff) => stuff.Select(x => x / 2);
            IEnumerable<int> AddWhere(IEnumerable<int> stuff) => stuff.Where(x => (x & 1) == 0);
        }
MethodLengthContainerTypeMeanErrorStdDevMedianRatioRatioSDGen 0Gen 1Gen 2Allocated
CisternValueLinq_ValueFuncs1Array109.014 ns0.0963 ns0.0804 ns109.001 ns12.330.02----
CisternValueLinq1Array92.106 ns0.3890 ns0.3448 ns91.936 ns10.420.05----
CisternValueLinq_struct_nothing_up_my_sleve1Array337.995 ns0.7011 ns0.6215 ns337.797 ns38.240.110.0286--120 B
Handcoded1Array8.838 ns0.0154 ns0.0128 ns8.835 ns1.000.00----
Linq1Array179.817 ns0.4103 ns0.3838 ns179.715 ns20.340.040.0248--104 B
CisternValueLinq_ValueFuncs100Array467.568 ns0.6051 ns0.5364 ns467.620 ns2.420.00----
CisternValueLinq100Array1,256.297 ns0.5981 ns0.4995 ns1,256.220 ns6.490.01----
CisternValueLinq_struct_nothing_up_my_sleve100Array1,677.269 ns9.3588 ns8.7542 ns1,672.959 ns8.670.050.0286--120 B
Handcoded100Array193.526 ns0.3986 ns0.3328 ns193.607 ns1.000.00----
Linq100Array2,107.560 ns15.9628 ns14.9316 ns2,107.047 ns10.900.070.0229--104 B
CisternValueLinq_ValueFuncs1000000Array3,928,467.738 ns78,228.5717 ns201,932.9239 ns3,799,725.391 ns0.630.02----
CisternValueLinq1000000Array14,837,932.500 ns38,233.4876 ns35,763.6280 ns14,843,329.688 ns2.240.01----
CisternValueLinq_struct_nothing_up_my_sleve1000000Array16,760,601.339 ns143,574.7964 ns127,275.3173 ns16,745,145.312 ns2.530.02---120 B
Handcoded1000000Array6,623,467.344 ns32,653.3972 ns30,544.0080 ns6,618,369.531 ns1.000.00---5 B
Linq1000000Array20,094,558.259 ns242,776.7486 ns215,215.2640 ns20,076,793.750 ns3.040.03---104 B