manofstick / Cistern.ValueLinq

MIT License
23 stars 0 forks source link

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);
        }
Method Length ContainerType Mean Error StdDev Median Ratio RatioSD Gen 0 Gen 1 Gen 2 Allocated
CisternValueLinq_ValueFuncs 1 Array 109.014 ns 0.0963 ns 0.0804 ns 109.001 ns 12.33 0.02 - - - -
CisternValueLinq 1 Array 92.106 ns 0.3890 ns 0.3448 ns 91.936 ns 10.42 0.05 - - - -
CisternValueLinq_struct_nothing_up_my_sleve 1 Array 337.995 ns 0.7011 ns 0.6215 ns 337.797 ns 38.24 0.11 0.0286 - - 120 B
Handcoded 1 Array 8.838 ns 0.0154 ns 0.0128 ns 8.835 ns 1.00 0.00 - - - -
Linq 1 Array 179.817 ns 0.4103 ns 0.3838 ns 179.715 ns 20.34 0.04 0.0248 - - 104 B
CisternValueLinq_ValueFuncs 100 Array 467.568 ns 0.6051 ns 0.5364 ns 467.620 ns 2.42 0.00 - - - -
CisternValueLinq 100 Array 1,256.297 ns 0.5981 ns 0.4995 ns 1,256.220 ns 6.49 0.01 - - - -
CisternValueLinq_struct_nothing_up_my_sleve 100 Array 1,677.269 ns 9.3588 ns 8.7542 ns 1,672.959 ns 8.67 0.05 0.0286 - - 120 B
Handcoded 100 Array 193.526 ns 0.3986 ns 0.3328 ns 193.607 ns 1.00 0.00 - - - -
Linq 100 Array 2,107.560 ns 15.9628 ns 14.9316 ns 2,107.047 ns 10.90 0.07 0.0229 - - 104 B
CisternValueLinq_ValueFuncs 1000000 Array 3,928,467.738 ns 78,228.5717 ns 201,932.9239 ns 3,799,725.391 ns 0.63 0.02 - - - -
CisternValueLinq 1000000 Array 14,837,932.500 ns 38,233.4876 ns 35,763.6280 ns 14,843,329.688 ns 2.24 0.01 - - - -
CisternValueLinq_struct_nothing_up_my_sleve 1000000 Array 16,760,601.339 ns 143,574.7964 ns 127,275.3173 ns 16,745,145.312 ns 2.53 0.02 - - - 120 B
Handcoded 1000000 Array 6,623,467.344 ns 32,653.3972 ns 30,544.0080 ns 6,618,369.531 ns 1.00 0.00 - - - 5 B
Linq 1000000 Array 20,094,558.259 ns 242,776.7486 ns 215,215.2640 ns 20,076,793.750 ns 3.04 0.03 - - - 104 B