Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove AsQueryable() from left join code sample #43807

Merged
merged 1 commit into from
Dec 2, 2024

Conversation

roji
Copy link
Member

@roji roji commented Dec 1, 2024

The LINQ join operations doc page has a section on left outer join, showing the following snippet for method syntax:

var query = students.GroupJoin(departments, student => student.DepartmentID, department => department.ID,
    (student, departmentList) => new { student, subgroup = departmentList.AsQueryable() })
    .SelectMany(joinedSet => joinedSet.subgroup.DefaultIfEmpty(), (student, department) => new
    {
        student.student.FirstName,
        student.student.LastName,
        Department = department.Name
    });

The AsQueryable() in the sample isn't needed, and is the source of some pretty dramatic perf overhead - this PR removes that (and reformats the code a bit for better readability). Note dotnet/runtime#110292, which is a proposal for adding a dedicated LeftJoin operator in .NET 10, which would replace the GroupJoin/SelectMany construct documented here.

BenchmarkDotNet v0.14.0, macOS Sequoia 15.1.1 (24B91) [Darwin 24.1.0]
Apple M2 Max, 1 CPU, 12 logical and 12 physical cores
.NET SDK 9.0.100
  [Host]     : .NET 9.0.0 (9.0.24.52809), Arm64 RyuJIT AdvSIMD
  DefaultJob : .NET 9.0.0 (9.0.24.52809), Arm64 RyuJIT AdvSIMD
Method InnersPerOuter OuterCount Mean Error StdDev Ratio RatioSD Gen0 Gen1 Gen2 Allocated Alloc Ratio
LeftJoin 1 1 97.82 ns 0.292 ns 0.244 ns 0.001 0.00 0.0573 - - 480 B 0.07
GroupJoin_SelectMany 1 1 67,935.22 ns 440.050 ns 411.623 ns 1.000 0.01 0.7324 0.3662 - 6735 B 1.00
LeftJoin 1 10 490.78 ns 1.131 ns 1.003 ns 0.001 0.00 0.2031 - - 1704 B 0.03
GroupJoin_SelectMany 1 10 674,969.93 ns 4,314.797 ns 3,824.955 ns 1.000 0.01 6.8359 2.9297 - 63527 B 1.00
LeftJoin 1 100 4,791.48 ns 28.291 ns 22.088 ns 0.001 0.00 1.7090 0.0534 - 14344 B 0.02
GroupJoin_SelectMany 1 100 6,824,534.62 ns 36,758.120 ns 32,585.116 ns 1.000 0.01 70.3125 31.2500 - 631926 B 1.00
LeftJoin 1 1000 47,687.53 ns 124.164 ns 116.144 ns 0.001 0.00 16.2964 3.4790 - 136728 B 0.02
GroupJoin_SelectMany 1 1000 67,837,071.04 ns 451,666.644 ns 400,390.716 ns 1.000 0.01 750.0000 375.0000 - 6313457 B 1.00
LeftJoin 10 1 293.74 ns 0.499 ns 0.442 ns 0.004 0.00 0.1316 - - 1104 B 0.15
GroupJoin_SelectMany 10 1 68,395.18 ns 594.304 ns 555.913 ns 1.000 0.01 0.8545 0.3662 - 7504 B 1.00
LeftJoin 10 10 2,598.17 ns 17.950 ns 16.791 ns 0.004 0.00 0.9460 0.0076 - 7944 B 0.11
GroupJoin_SelectMany 10 10 678,496.09 ns 7,117.783 ns 6,657.979 ns 1.000 0.01 7.8125 3.9063 - 69776 B 1.00
LeftJoin 10 100 30,906.21 ns 30.591 ns 27.118 ns 0.005 0.00 9.1553 0.7324 - 76744 B 0.11
GroupJoin_SelectMany 10 100 6,844,961.17 ns 27,512.584 ns 22,974.254 ns 1.000 0.00 78.1250 39.0625 - 694356 B 1.00
LeftJoin 10 1000 377,067.26 ns 1,734.244 ns 1,622.213 ns 0.006 0.00 90.8203 30.2734 - 760728 B 0.11
GroupJoin_SelectMany 10 1000 68,429,902.38 ns 579,735.477 ns 542,284.925 ns 1.000 0.01 714.2857 285.7143 - 6934177 B 1.00
LeftJoin 100 1 1,849.73 ns 4.218 ns 3.739 ns 0.03 0.00 0.6981 0.0019 - 5848 B 0.48
GroupJoin_SelectMany 100 1 68,215.49 ns 611.444 ns 510.583 ns 1.00 0.01 1.3428 0.6104 - 12104 B 1.00
LeftJoin 100 10 18,371.97 ns 367.054 ns 306.507 ns 0.03 0.00 6.5918 0.2747 - 55384 B 0.47
GroupJoin_SelectMany 100 10 684,697.64 ns 6,704.490 ns 5,598.553 ns 1.00 0.01 13.6719 6.8359 - 117230 B 1.00
LeftJoin 100 100 202,379.73 ns 3,950.849 ns 4,227.364 ns 0.03 0.00 65.6738 15.8691 - 551144 B 0.47
GroupJoin_SelectMany 100 100 6,863,089.50 ns 31,329.779 ns 26,161.785 ns 1.00 0.01 125.0000 62.5000 - 1168687 B 1.00
LeftJoin 100 1000 2,600,306.48 ns 4,152.430 ns 3,467.467 ns 0.04 0.00 656.2500 316.4063 - 5504731 B 0.47
GroupJoin_SelectMany 100 1000 71,509,367.47 ns 558,147.996 ns 522,091.982 ns 1.00 0.01 1285.7143 714.2857 - 11679688 B 1.00
LeftJoin 1000 1 17,042.46 ns 322.777 ns 301.926 ns 0.20 0.00 5.8594 0.1221 - 49056 B 0.89
GroupJoin_SelectMany 1000 1 86,478.90 ns 914.553 ns 855.473 ns 1.00 0.01 6.5918 2.1973 - 55318 B 1.00
LeftJoin 1000 10 170,578.63 ns 329.190 ns 291.819 ns 0.20 0.00 58.1055 11.9629 - 487464 B 0.89
GroupJoin_SelectMany 1000 10 853,239.40 ns 2,109.237 ns 1,761.309 ns 1.00 0.00 64.4531 25.3906 - 549354 B 1.00
LeftJoin 1000 100 1,937,489.58 ns 5,986.665 ns 4,999.136 ns 0.22 0.00 582.0313 251.9531 - 4871945 B 0.89
GroupJoin_SelectMany 1000 100 8,938,502.84 ns 50,181.279 ns 46,939.600 ns 1.00 0.01 656.2500 312.5000 - 5490176 B 1.00
LeftJoin 1000 1000 40,889,193.74 ns 241,213.807 ns 213,829.757 ns 0.36 0.01 5900.0000 1700.0000 100.0000 48712871 B 0.89
GroupJoin_SelectMany 1000 1000 112,797,755.31 ns 1,643,320.304 ns 2,653,656.189 ns 1.00 0.03 6333.3333 1666.6667 - 54892131 B 1.00
Benchmark code
[MemoryDiagnoser]
public class AsQueryableBenchmarks
{
    [Params(1, 10, 100, 1000, Priority = 1)]
    public int InnersPerOuter { get; set; }

    [Params(1, 10, 100, 1000, Priority = 2)]
    public int OuterCount { get; set; }

    private Outer[] _outers = null!;
    private Inner[] _inners = null!;

    class Outer
    {
        public int Id { get; set; }
        public string? OuterPayload { get; set; }
    }

    class Inner
    {
        public int Id { get; set; }
        public int OuterId { get; set; }
        public string? InnerPayload { get; set; }
    }

    private const int RandomSeed = 42;

    [GlobalSetup]
    public void Setup()
    {
        _outers = new Outer[OuterCount];
        _inners = new Inner[OuterCount * InnersPerOuter];

        var remainingInners = new List<Inner>(OuterCount * InnersPerOuter);
        for (var outerId = 0; outerId < OuterCount; outerId++)
        {
            _outers[outerId] = new Outer { Id = outerId, OuterPayload = $"Outer{outerId}" };

            for (var j = 0; j < InnersPerOuter; j++)
            {
                var innerId = outerId * j + j;
                remainingInners.Add(new Inner { Id = innerId, OuterId = outerId, InnerPayload = $"Inner{innerId}" });
            }
        }

        var random = new Random(RandomSeed);

        for (var i = 0; i < _inners.Length; i++)
        {
            var j = random.Next(0, remainingInners.Count);
            _inners[i] = remainingInners[j];
            remainingInners.RemoveAt(j);
        }

        Debug.Assert(remainingInners.Count == 0);
    }

    [Benchmark(Baseline = true)]
    public int GroupJoin_SelectMany_with_AsQueryable()
        => _outers
            .GroupJoin(_inners, o => o.Id, i => i.OuterId, (o, inners) => new { Outer = o, Inners = inners.AsQueryable() })
            .SelectMany(
                joinedSet => joinedSet.Inners.DefaultIfEmpty(),
                (o, i) => new
                {
                    o.Outer.OuterPayload,
                    i?.InnerPayload
                })
            .Count();

    [Benchmark]
    public int GroupJoin_SelectMany_without_AsQueryable()
        => _outers
            .GroupJoin(_inners, o => o.Id, i => i.OuterId, (o, inners) => new { Outer = o, Inners = inners })
            .SelectMany(
                joinedSet => joinedSet.Inners.DefaultIfEmpty(),
                (o, i) => new
                {
                    o.Outer.OuterPayload,
                    i?.InnerPayload
                })
            .Count();
}

/cc @jeffhandley @dotnet/area-system-linq @dotnet/efteam

Copy link
Member

@BillWagner BillWagner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for making this fix.

I'll :shipit: now.

@BillWagner BillWagner merged commit 8187638 into dotnet:main Dec 2, 2024
14 checks passed
@roji roji deleted the LeftJoinAsQueryable branch December 2, 2024 15:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants