Linq性能优化:我应该先使用`where`还是`select`?

15

我在内存中有一个大的List,这个List来自一个包含大约20个属性的类。

我想基于其中一个属性来过滤这个列表,对于我只需要这个属性的任务,我只需要这个属性的列表。因此我的查询结果如下:

data.Select(x => x.field).Where(x => x == "desired value").ToList()

使用 Select 还是使用 Where 会给我带来更好的性能表现?

data.Where(x => x.field == "desired value").Select(x => x.field).ToList()

请告诉我这是否与我所保存的数据类型或字段类型有关。请注意,我还需要这些对象进行其他任务,因此无法在加载到内存之前首先对它们进行过滤。


在我看来,SELECT/WHERE 比 WHERE/SELECT 更好一些,因为在这个两步骤的过程中,第一步(SELECT)比 WHERE 更具限制性,所以第二步将有一个更小的数据集进行筛选。这只是一个观察结果,我认为除非你的列表变得非常庞大,否则结果不会有太大的区别。 - Shannon Holsinger
理论上,人们会认为 Where 根据条件返回的记录比 Select 返回的记录少。而 Select 首先返回所有记录,然后对其进行条件过滤。因此,使用 Where 应该会提高性能。不过,你可以尝试在一个包含 10000 条以上记录的巨大测试数据库中测试你的查询。好问题! - Keyur PATEL
看吧?这就是我想太多的结果!@Keyur PATEL 的想法似乎比我更有道理。 - Shannon Holsinger
1
当你有两匹马并想知道哪一匹更快时,你需要比赛它们。对于这两个问题,你应该采取相同的方法。请参阅 https://ericlippert.com/2012/12/17/performance-rant/。 - Enigmativity
当我说实际代码时,我指的是用于此查询的实际代码(不是为了这里简化的代码),以及用于构造您进行查询的列表的实际代码(即实际查询数据库以创建您现在正在查询的列表的代码)。 - Ronan Thibaudau
显示剩余7条评论
2个回答

15

使用Where先过滤集合再进行Select操作的方法性能更佳,因为它在仅对过滤后的值执行Select时。从数学上讲,使用Where先进行筛选需要N + N'个操作,在此处N'是符合Where条件的集合项数量。因此,它最少需要N + 0 = N个操作(如果没有任何项通过此Where条件),最多需要N + N = 2 * N个操作(如果所有项均满足条件)。

同时,使用Select优先的方法始终需要2 * N个操作,因为它会遍历所有对象以获取属性,然后遍历所有对象以过滤它们。

基准测试证明

我已经完成了基准测试来证明我的答案。

结果:

Condition value: 50
Where -> Select: 88 ms, 10500319 hits
Select -> Where: 137 ms, 20000000 hits

Condition value: 500
Where -> Select: 187 ms, 14999212 hits
Select -> Where: 238 ms, 20000000 hits

Condition value: 950
Where -> Select: 186 ms, 19500126 hits
Select -> Where: 402 ms, 20000000 hits

如果您多次运行基准测试,那么您会发现Where -> Select方法的性能会不时地变化,而Select -> Where方法总是需要2N次操作。

IDEOne演示:

https://ideone.com/jwZJLt

代码:

class Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

class Program
{
    static void Main()
    {
        var random = new Random();
        List<Point> points = Enumerable.Range(0, 10000000).Select(x => new Point { X = random.Next(1000), Y = random.Next(1000) }).ToList();

        int conditionValue = 250;
        Console.WriteLine($"Condition value: {conditionValue}");

        Stopwatch sw = new Stopwatch();
        sw.Start();

        int hitCount1 = 0;
        var points1 = points.Where(x =>
        {
            hitCount1++;
            return x.X < conditionValue;
        }).Select(x =>
        {
            hitCount1++;
            return x.Y;
        }).ToArray();

        sw.Stop();
        Console.WriteLine($"Where -> Select: {sw.ElapsedMilliseconds} ms, {hitCount1} hits");

        sw.Restart();

        int hitCount2 = 0;
        var points2 = points.Select(x =>
        {
            hitCount2++;
            return x.Y;
        }).Where(x =>
        {
            hitCount2++;
            return x < conditionValue;
        }).ToArray();

        sw.Stop();
        Console.WriteLine($"Select -> Where: {sw.ElapsedMilliseconds} ms, {hitCount2} hits");

        Console.ReadLine();
    }
}

相关问题

这些问题可能对您也很有趣。它们与 SelectWhere 无关,但涉及到 LINQ 的性能顺序:

LINQ 函数的顺序有影响吗?
LINQ 扩展方法的顺序不会影响性能?


1
在你选择的Point只有两个属性且轻量级的情况下,这可能是正确的,但如果我们处理大型对象,则可能会有所不同。 - Hari Prasad
@HariPrasad 我不确定一个对象的大小如何影响执行时间。你能解释一下吗? - Yeldar Kurmangaliyev
@YeldarKurmangaliyev:我添加了一个不同对象尺寸的测试。你能看一下吗?可能是我漏看了某些东西。 - displayName
请注意,您的950值示例是基准测试无法正常工作的完美示例,在两种情况下都会获得大约相同数量的命中次数,因此函数调用量也大致相同,并且其中一个函数所需时间不到一半,清楚地表明了基准测试甚至对于对象的linq也失败了。要对此进行基准测试,需要一些预热代码以及将测试外部化并跳过前几次运行,每种情况下至少显示最小/最大/平均持续时间。 - Ronan Thibaudau

5
答案取决于您的集合状态。
  • 如果大多数实体通过Where测试,请先应用Select
  • 如果较少的实体通过Where测试,请先应用Where

更新:

@YeldarKurmangaliyev 给出了具体的示例和基准测试的答案。我运行了类似的代码来验证他的说法,我们的结果完全相反,因为我使用的测试对象不像他用于运行测试的简单的Point类型那么简单。

这段代码看起来非常像他的代码,只是我将类名从Point更改为EnumerableClass

以下是我用于构成EnumerableClass类的类:

public class EnumerableClass
{
    public int X { get; set; }
    public int Y { get; set; }
    public String A { get; set; }
    public String B { get; set; }
    public String C { get; set; }
    public String D { get; set; }
    public String E { get; set; }
    public Frame F { get; set; }
    public Gatorade Gatorade { get; set; }
    public Home Home { get; set; }
}

public class Home
{
    private Home(int rooms, double bathrooms, Stove stove, InternetConnection internetConnection)
    {
        Rooms = rooms;
        Bathrooms = (decimal) bathrooms;
        StoveType = stove;
        Internet = internetConnection;
    }

    public int Rooms { get; set; }
    public decimal Bathrooms { get; set; }
    public Stove StoveType { get; set; }
    public InternetConnection Internet { get; set; }

    public static Home GetUnitOfHome()
    {
        return new Home(5, 2.5, Stove.Gas, InternetConnection.Att);
    }
}

public enum InternetConnection
{
    Comcast = 0,
    Verizon = 1,
    Att = 2,
    Google = 3
}

public enum Stove
{
    Gas = 0,
    Electric = 1,
    Induction = 2
}

public class Gatorade
{
    private Gatorade(int volume, Color liquidColor, int bottleSize)
    {
        Volume = volume;
        LiquidColor = liquidColor;
        BottleSize = bottleSize;
    }

    public int Volume { get; set; }
    public Color LiquidColor { get; set; }
    public int BottleSize { get; set; }

    public static Gatorade GetGatoradeBottle()
    {
        return new Gatorade(100, Color.Orange, 150);
    }
}

public class Frame
{
    public int X { get; set; }
    public int Y { get; set; }

    private Frame(int x, int y)
    {
        X = x;
        Y = y;
    }

    public static Frame GetFrame()
    {
        return new Frame(5, 10);
    }
}

FrameGatoradeHome都有一个静态方法,用于返回它们类型的实例。

以下是主程序:

public static class Program
{
    const string Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    private static readonly Random Random = new Random();

    private static string RandomString(int length)
    {
        return new string(Enumerable.Repeat(Chars, length)
            .Select(s => s[Random.Next(s.Length)]).ToArray());
    }

    private static void Main()
    {
        var random = new Random();
        var largeCollection =
            Enumerable.Range(0, 1000000)
                .Select(
                    x =>
                        new EnumerableClass
                        {
                            A = RandomString(500),
                            B = RandomString(1000),
                            C = RandomString(100),
                            D = RandomString(256),
                            E = RandomString(1024),
                            F = Frame.GetFrame(),
                            Gatorade = Gatorade.GetGatoradeBottle(),
                            Home = Home.GetUnitOfHome(),
                            X = random.Next(1000),
                            Y = random.Next(1000)
                        })
                .ToList();

        const int conditionValue = 250;
        Console.WriteLine(@"Condition value: {0}", conditionValue);

        var sw = new Stopwatch();
        sw.Start();
        var firstWhere = largeCollection
            .Where(x => x.Y < conditionValue)
            .Select(x => x.Y)
            .ToArray();
        sw.Stop();
        Console.WriteLine(@"Where -> Select: {0} ms", sw.ElapsedMilliseconds);

        sw.Restart();
        var firstSelect = largeCollection
            .Select(x => x.Y)
            .Where(y => y < conditionValue)
            .ToArray();
        sw.Stop();
        Console.WriteLine(@"Select -> Where: {0} ms", sw.ElapsedMilliseconds);
        Console.ReadLine();

        Console.WriteLine();
        Console.WriteLine(@"First Where's first item: {0}", firstWhere.FirstOrDefault());
        Console.WriteLine(@"First Select's first item: {0}", firstSelect.FirstOrDefault());
        Console.WriteLine();
        Console.ReadLine();
    }
}

结果:

我运行了多次测试并发现,在集合大小为1000000时,

.Select().Where() .Where().Select().

表现更好。


以下是需要翻译的内容:

这是第一个测试结果,我强制将每个EnumerableClass对象的Y值设置为5,因此每个项目都通过了Where

Condition value: 250
Where -> Select: 149 ms
Select -> Where: 115 ms

First Where's first item: 5
First Select's first item: 5

以下是第二个测试结果,我强制每个EnumerableClass对象的Y值为251,因此没有任何项目通过了Where:
Condition value: 250
Where -> Select: 110 ms
Select -> Where: 100 ms

First Where's first item: 0
First Select's first item: 0

显然,结果非常依赖于集合的状态:

  • 在@YeldarKurmangaliyev的测试中,.Where().Select()表现更好; 和,
  • 在我的测试中,.Select().Where()表现更好。

我一遍又一遍地提到的集合的状态包括:

  • 每个项目的大小;
  • 集合中的总项目数; 和,
  • 可能通过Where子句的项目数。

回应评论:

此外,@Enigmativity说,在知道Where的结果以便确定先放置Where还是先放置之前,这是一个进退两难的局面。理论上,他是正确的,而且不出所料,在计算机科学的另一个领域Scheduling中也存在这种情况。

最佳调度算法是最短作业优先,我们首先安排执行时间最短的作业。但是,有人怎么知道一个特定作业需要多长时间才能完成呢?嗯,答案是:

在准确估计运行时间的专门环境中使用最短作业优先。

因此,正如我在一开始时所说的(这也是我的回答的第一个、较短版本),这个问题的正确答案将取决于集合的当前状态

一般来说,

  • 如果您的对象在合理的大小范围内; 并且,
  • 您正在从每个对象中选择一个非常小的块; 并且,
  • 您的集合大小也不仅仅是几千个,

那么本答案开头提到的指导方针将对您有用。


你能解释一下你的推理吗? - Enigmativity
@Akbari:我只是猜测对于Linq-to-SQL来说,这不会有什么区别,因为数据库也会优化查询。 - displayName
@Akbari:请阅读Enig和我的评论,我已经提到了当集合可以被跟踪时你可以做什么。 - displayName
@displayName - 我认为这个答案需要一些解释。现在它是随意的,几乎没有科学依据。 - Enigmativity
1
@Akbari:我已经更新了答案,涵盖了你和其他人提出的所有问题。如果还有不完整的地方,请告诉我,我会尽力补充。 - displayName
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接