65.9K
CodeProject 正在变化。 阅读更多。
Home

LINQ 和性能

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (28投票s)

2022年11月1日

CPOL

11分钟阅读

viewsIcon

40909

downloadIcon

47

LINQ 是否是处理大量运行时相关环境数据的正确技术?

引言

自 Microsoft 在 .NET 3.5 中引入 LINQ 以来,已经过去了 15 年(2007 年)。这应该足以让它成熟为一个全方位的有用工具。

LINQ 的使用有几个方面

  • 延迟执行和惰性求值(更好地利用多线程系统)
  • 缩短代码(可读性和可重用性)
  • 所用技术的运行时开销(性能)

在本文中,我将只关注 LINQ 的一个方面——性能。

背景

LINQ 的出现,至少在我的环境中,让所有 C# 程序员都为之兴奋,并随后导致这项技术的大规模使用。

我想在这里问一下,当我们以非常清醒的眼光看待 LINQ 时——它是一种创新的组合,将 扩展方法匿名委托(lambda 表达式)与延迟执行(yield)相结合——它的营销光环还剩下多少?

为了回答性能方面的问题,我想使用 LINQ 过滤一个对象列表并对结果进行排序。

这种方法的第一处特殊性在于 LINQ 必须处理到最后一个元素。因此,我故意排除了使用 Any()First() 可以实现的运行时优势,并专注于必须处理所有元素的场景。

这种方法的第二处特殊性在于 LINQ 是对内存进行操作(而不是对数据库进行操作)。因此,我故意排除了访问数据库会引入的运行时影响——无论是(小的)内存数据库、经典的服务器数据库还是它们的混合体。因此,LINQ 的一个典型应用——数据库查询——在测试中并没有得到合理的体现。这是因为将数据库查询添加到测试中也会很容易导致结果失真。

  • LINQ 数据库查询的有效性更多地受数据库驱动程序的影响,而不是编程方法的影响。
  • 如果 LINQ 数据库查询是针对缓存数据库执行的,那么结果仍然来自内存。
  • 编程方法必须有多么愚蠢才能与 LINQ 相提并论?我预计存储过程(可参数化且已编译的 SQL 查询)将远优于 LINQ 查询。
更新 1:在评论部分,有许多评论讨论了不将 LINQ 用于数据库访问的自我约束。这些评论的范围从批评(这忽略了核心用例)到赞同(存储 SQL 过程无论如何都是数据库高效查询的更好方法)。通过我的方法,我只想且仅限于排除数据库访问会给测试带来的副作用。不多不少。

测试

当然,结果取决于底层运行时系统、操作系统和硬件的多线程能力——因此只有在同一环境中比较不同方法的有用性才有意义。出于这个原因,我准备了 4 个可以相互比较的测试用例。

  • 测试用例 1:LINQ with C++(有和无排序)
  • 测试用例 2:经典方法(for(...) 循环、if(...)sort(...))与 C++(有和无排序)
  • 测试用例 3:LINQ with C#(有和无排序)
  • 测试用例 4:经典方法(for(...) 循环、if(...)sort(...))与 C#(有和无排序)

测试用例将在两个不同的环境中执行……

Win10 测试

第一个测试环境是一台联想 T550,配备 i7-5600U @ 2.6GHz、Windows 10 x64、.NET 4.7.2 和 16GB 内存。

C++ 的测试结果是

  • C++ 代码已针对 x86 架构编译,符合 ISO C++ 14 标准,并使用 /O2 优化标志。
  • 我也测试了 x64 架构——但没有观察到差异。

C# 的测试结果是

openSUSE 测试

第二个测试环境是一台联想 T520,配备 i5-2540M @ 2.6GHz、openSUSE Leap 15.4 x64、.NET 6.0 和 12 GB 内存。

更新 1:当我首次发布测试时,.NET 6.0 是当前版本,而 .NET 7.0 即将发布。评论区有许多评论提出是否 .NET 7.0 的结果会更好。在文章的最后,我添加了涵盖此问题的测量结果。

C++ 的测试结果是

  • C++ 代码使用 GNU C++ 12-lp154 默认设置和 /O3 优化标志编译。

C# 的测试结果是

结果比较

为了更好地比较结果,我忽略了最差的测试运行(最长的运行时间),而是取剩余三次测试运行的中位数。这产生了以下汇总图

系统 硬件 code 尝试 排序 中位数 C++ vs. C# LINQ vs. 经典方法
1 Win 10 T550 C++ LINQ 11.5 毫秒 好 13% 差约 3 倍
2 Win 10 T550 C# LINQ 13.0 毫秒 差 13%
3 Win 10 T550 C++ 经典方法 2.5 毫秒 好 112% 好约 3 倍
4 Win 10 T550 C# 经典方法 5.3 毫秒 差 112%
5 Win 10 T550 C++ LINQ 21.2 毫秒 好 159% 不一致(C++
差约 2.5 倍,
C# 可比)
6 Win 10 T550 C# LINQ 55.0 毫秒 差 159%
7 Win 10 T550 C++ 经典方法 8.2 毫秒 好 554% 不一致(C++
好约 2.5 倍,
C# 可比)
8 Win 10 T550 C# 经典方法 53.7 毫秒 差 554%
9 openSUSE T520 C++ LINQ 6.0 毫秒 好 171% 差约 3.5 倍
10 openSUSE T520 C# LINQ 16.3 毫秒 差 171%
11 openSUSE T520 C++ 经典方法 1.2 毫秒 好 575% 好约 3.5 倍
12 openSUSE T520 C# 经典方法 8.1 毫秒 差 575%
13 openSUSE T520 C++ LINQ 10.5 毫秒 好 206% 不一致(C++
差约 2 倍,
C# 可比)
14 openSUSE T520 C# LINQ 32.2 毫秒 差 206%
15 openSUSE T520 C++ 经典方法 5.2 毫秒 好 496% 不一致(C++
好约 2 倍,
C# 可比)
16 openSUSE T520 C# 经典方法 31.0 毫秒 差 496%

对期望和结果的讨论

C++ vs. C#

  • 我期望 C++ 比 C# 有明显的优势。
  • 期望已满足。

C++ 的速度最高可达 C# 的约 5.5 倍——这对于应用程序来说不算戏剧性,但对于平台和服务来说可能是一个决定性的标准。显然,STL 和 C++ 编译器优化器在这里做得非常出色。

更新 1:评论区有一些评论似乎提及 C++ 与 C# 的比较不公平。

核心观点是:C++ 被设计用于接近硬件的开发(操作系统、平台、驱动程序……),而 C# 被设计用于快速且最少出错的开发(应用程序系统级别)。
总的来说,我同意这个观点,即使我对“设计”这个词有些疑问——毕竟,这是一次进化发展。

尽管如此,我通常同意这个论点,在某些情况下,我在应用程序系统级别也使用了 C++ 和 C# 的组合——因为即使使用 .NET Core 或 .NET 7.0,也可以轻松编写混合模式 DLL。

LINQ vs. 经典方法(for(...) 循环、if(...) 和 sort(...)

  • 我期望 LINQ 比经典方法有明显的劣势。
  • 期望已满足。但是,不如预期那样明显。

对于 C++ 而言,LINQ 始终比经典方法(for(...) 循环、if(...)sort(...))差,但对于 C# 并非如此。

我认为 LINQ 在 C# 中表现如此出色的原因是特殊的数据结构。虽然 C++ 中的 LINQ 使用 vector<Element> 作为源数据和结果,但 C# 中的 LINQ 使用 List<Element> 作为源数据,以及 System.Linq.Enumerable.WhereListIterator<Element>System.Linq.OrderedEnumerable<Element, int> 作为结果。第二个 datatype 提供了对排序标准的显式访问,也可以被视为不再可比或作弊。但我们还是温和一点。

Win10 vs. openSUSE

我不想对此做过多评论,但测试似乎在 Linux 上以更弱的硬件运行得更快。也许是因为 Microsoft Defender——它在我的 Windows 机器上总是运行着的。

扩展

C# 相对于 C++ 的结果相当令人满意,这让我感到惊讶(我曾预计至少有十倍的差距),也让我感到高兴(显然 15 年的发展确实对 C# 产生了积极影响)。

尽管如此,我想确保 C# 确实如此强大。因此,我简单地将数据量扩大了二十倍,从 640000 增加到 12800000

缩放后的 C++ 测试结果是

缩放后的 C# 测试结果是

这产生了以下汇总图

系统 硬件 code 尝试 排序 中位数 C++ vs. C# LINQ vs. 经典方法
1 openSUSE T520 C++ LINQ 108.9 毫秒 好 58% 差约 2.5 倍
2 openSUSE T520 C# LINQ 171.8 毫秒 差 58%
3 openSUSE T520 C++ 经典方法 35.9 毫秒 好 197% 好约 2.5 倍
4 openSUSE T520 C# 经典方法 106.7 毫秒 差 197%
5 openSUSE T520 C++ LINQ 250.9 毫秒 好 153% 不一致(C++
差约 2 倍,
C# 可比)
6 openSUSE T520 C# LINQ 636.4 毫秒 差 153%
7 openSUSE T520 C++ 经典方法 135.1 毫秒 好 383% 不一致(C++
好约 2 倍,
C# 可比)
8 openSUSE T520 C# 经典方法 652.1 毫秒 差 383%

讨论

先前关于较小数据集的所有陈述都得到了证实。

Using the Code

更新 1:以下代码(C++ 和 C#)代表了我测试的初始版本(第一次代码生成),不包括已集成的错误修复。评论区中的许多评论都提出了使代码更高效的建议。这些建议范围从更现代的表示法(例如属性)到对基准测试库的推荐。

首先,感谢大家! 我已经尝试/实现了其中许多建议。对于使 C++ 代码更高效的建议,事实证明 GNU C++ 编译器优化得非常高效,以至于代码更改没有可衡量的效果。对于使 C# 代码更高效的建议,非常好的评价可以从测量结果中读出改进,但 C# 编译器也优化得非常高效。

从基准测试库的建议中,我只采纳了提供清晰结果评估接口并以更紧凑、更清晰的方式呈现结果的想法。C++ 和 C# 的基准测试库差异很大,以至于会过度影响方法的比较,因此我决定使用一个自实现的解决方案,该方案在 C++ 和 C# 中都能完全相同地工作。

我很乐意提供增强的代码:
C++ 增强代码 下载 LINQ-SimulationTest.zip
C# 增强代码 下载 LINQ-Test.zip

C++ 测试代码(第一次代码生成)

代码在 Win10 和 openSUSE 上是完全相同的。

#include <iostream>
#include <iomanip>
#include <vector>
#include <chrono>
#include "boolinq.h"

using namespace std;
using namespace boolinq;

static const size_t VECTORSIZE = 640000;

class Element
{
private:
    int tag;
    int order;
    int data;

public:
    int getTag() const { return tag; }
    void setTag(int t) { tag = t; }
    int getOrder() const { return order; }
    void setOrder(int o) { order = o; }
    int getData() const { return data; }
    void setData(int d) { data = d; }

    inline bool operator<(const Element& b)
    {
        return this->order * VECTORSIZE + this->data < b.order* VECTORSIZE + b.data;
    }
};

void checkResults(vector<Element> resultVector)
{
    int d = 0;
    int o = 0;
    for (auto it = resultVector.begin(); it != resultVector.end(); it++)
    {
        if (d > it->getData() && o > it->getOrder())
            throw "Ordering failed";
        d = it->getData();
        o = it->getOrder();
    }
}

void testClasic(vector<Element> testVector, bool order, int run)
{
    auto startTime = chrono::high_resolution_clock::now();

    vector<Element> resultVector;

    for (auto it = testVector.begin(); it != testVector.end(); it++)
    {
        if (it->getTag() < 1)
        {
            resultVector.push_back(*it);
        }
    }
    if(order)
        sort(resultVector.begin(), resultVector.end());

    if (resultVector.size() != VECTORSIZE / 8)
        throw "Number of result elements not correct!";

    checkResults(resultVector);

    auto endTime = chrono::high_resolution_clock::now();

    auto ordering = (order ? "ordered" : "not ordered");
    double timeTaken = chrono::duration_cast<chrono::nanoseconds>
                          (endTime - startTime).count() / 1000000000.0;
    cout << "Classic (" << ordering << "), run " << run << " -> Time: "
         << setprecision(9) << timeTaken << " sec" << " To be: "
         << VECTORSIZE / 8 << " Result: " << resultVector.size() << endl;
}

void testLINQ(vector<Element> testVector, bool order, int run)
{
    auto startTime = chrono::high_resolution_clock::now();

    auto resultVector = (order ?
        from(testVector).where([](const Element& element)
                            { return element.getTag() < 1; })
                        .orderBy([](const Element& element)
                            { return element.getOrder() * VECTORSIZE +
                                     element.getData(); })
                        .toStdVector() :
        from(testVector).where([](const Element& element)
                            { return element.getTag() < 1; })
                        .toStdVector());
    if (resultVector.size() != VECTORSIZE / 8)
        throw "Number of result elements not correct!";

    checkResults(resultVector);

    auto endTime = chrono::high_resolution_clock::now();

    auto ordering = (order ? "ordered" : "not ordered");
    double timeTaken = chrono::duration_cast<chrono::nanoseconds>
                           (endTime - startTime).count() / 1000000000.0;
    cout << "LINQ (" << ordering << "),    run " << run << " -> Time: "
         << setprecision(9) << timeTaken << " sec" << " To be: "
         << VECTORSIZE / 8 << " Result: " << resultVector.size() << endl;
}

int main()
{
    vector<Element> testVector(VECTORSIZE);
    for (size_t index = 0; index < VECTORSIZE; index++)
    {
        testVector[index].setTag(index % 8);
        testVector[index].setOrder(index % 12);
        testVector[index].setData(index);
    }

    for (int run = 0; run < 4; run++)
    {
        testLINQ(testVector, false, run);
        testClasic(testVector, false, run);
    }

    for (int run = 0; run < 4; run++)
    {
        testLINQ(testVector, true, run);
        testClasic(testVector, true, run);
    }

    int i;
    cin >> i;

    return 0;
}

C# 测试代码(第一次代码生成)

代码在 Win10 和 openSUSE 上是完全相同的。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace LINQtest
{
    public static class Settings
    {
        public const int VECTORSIZE = 640000;
    }

    class Element : IComparable<Element>
    {
        private int tag;
        private int order;
        private int data;

        public Element(int t, int o, int d)
        {
            tag = t;
            order = o;
            data = d;
        }

        public int Tag { get { return tag; } set { tag = value; } }
        public int Order { get { return order; } set { order = value; } }
        public int Data { get { return data; } set { data = value; } }

        public int CompareTo(Element? b)
        {
            if (b == null)
                return 1;
            return (b.order * Settings.VECTORSIZE + b.data) -
                   (order * Settings.VECTORSIZE + data);
        }
    }

    class Program
    {
        static void checkResults(IEnumerable<Element> resultVector)
        {
            int d = 0;
            int o = 0;
            foreach (var element in resultVector)
            {
                if (d > element.Data && o > element.Order)
                    throw new Exception("Ordering failed");
                d = element.Data;
                o = element.Order;
            }
        }

        static void testClasic(List<Element> testVector, bool order, int run)
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();

            var resultVector = new List<Element>(testVector.Count / 8);

            foreach (var element in testVector)
            {
                if (element.Tag < 1)
                    resultVector.Add(element);
            }

            if (order)
                resultVector.Sort();

            if (resultVector.Count != Settings.VECTORSIZE / 8)
                throw new Exception("Number of result elements not correct!");

            checkResults(resultVector);

            sw.Stop();

            var ordering = (order ? "ordered" : "not ordered");
            Console.WriteLine(
                "Classic ({0}), run {1} -> Time: {2} sec To be: {3} Result: {4}",
                ordering, run, sw.Elapsed.ToString().Substring(7),
                Settings.VECTORSIZE / 8, resultVector.Count());
        }

        static void testLINQ(List<Element> testVector, bool order, int run)
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();

            var resultVector = (order ?
                    testVector.Where(element => element.Tag < 1)
                              .OrderBy(element => element.Order *
                                       Settings.VECTORSIZE + element.Data) :
                    testVector.Where(element => element.Tag < 1)).ToList();

            if (resultVector.Count() != Settings.VECTORSIZE / 8)
                throw new Exception("Number of result elements not correct!");

            checkResults(resultVector);

            sw.Stop();

            var ordering = (order ? "ordered" : "not ordered");
            Console.WriteLine(
                "LINQ ({0}),    run {1} -> Time: {2} sec To be: {3} Result: {4}",
                ordering, run, sw.Elapsed.ToString().Substring(7),
                Settings.VECTORSIZE / 8, resultVector.Count());
        }

        static void Main(string[] args)
        {
            var testVector = new List<Element>(Settings.VECTORSIZE);
            for (int index = 0; index < Settings.VECTORSIZE; index++)
            {
                testVector.Add(new Element(index % 8, index % 12, index));
            }

            for (int run = 0; run < 4; run++)
            {
                testLINQ(testVector, false, run);
                testClasic(testVector, false, run);
            }
            for (int run = 0; run < 4; run++)
            {
                testLINQ(testVector, true, run);
                testClasic(testVector, true, run);
            }

            var key = Console.ReadKey();
        }
    }
}

Win10 代码编译

我使用了 Visual Studio 2019 项目来编译 C++ 和 C#。

openSUSE 代码编译

要编译 C++ 代码,我创建了一个 Code::Blocks 20.3 项目并使用了 gcc-c++ 12

要编译 C# 代码,我使用了 NET6.0 SDK 和此 shell 脚本

#              |<-- The path to the *.scproj file                    
|<-- The target folder (the result will also be in the bin/Release folder).
dotnet publish . --configuration Release --framework net6.0 --output ./pub 
                 --self-contained false --runtime linux-x64 --verbosity quiet
更新 1:同时,我已从 .NET 6.0 升级到 .NET 7.0。转换测试所需做的就是将 shell 脚本和项目文件(*.csproj)中的 net6.0 更改为 net7.0

关注点

我想知道 LINQ 和经典方法(for(...) 循环、if(...)sort(...))在其各自的最佳实现中的差距有多大。

结果表明,对于 C++,经典方法是最具性能的,而对于 C#,则推荐使用 LINQ。这似乎有充分的理由。虽然 C++ 的 LINQ 只意味着额外的开销,但 C# 的 LINQ 可以通过特殊的数据结构(System.Linq.Enumerable.WhereListIterator<Element>System.Linq.OrderedEnumerable<Element, int>)来弥补这种额外的开销。

更新 1:同时,我已从 .NET 6.0 升级到 .NET 7.0。下表显示了在 openSUSE 上的测量结果(在 Win10 上,测量结果的散布几乎不允许做出可靠的陈述,这一点已经显示出来)。

这些测量结果是通过增强的代码、更紧凑清晰的结果输出(新的干净基准测试接口)以及将运行次数增加到 9 次评估运行来获得的。

我重复了测试 3 次——并且无法检测到 .NET 7.0 的任何加速。
Platform environment: Unix
Runtime environment:  .NETCoreApp,Version=<code>v6.0</code>

| Measurement set       |     N |       Mean |      Error |     StdDev | Ratio |
| --------------------- | ----- | ---------- | ---------- | ---------- | ----- |
| LINQ (not ordered)    |     9 |   0.197 s  |   0.037 s  |   0.020 s  |  1.00 |
| Classic (not ordered) |     9 |   0.106 s  |   0.000 s  |   0.000 s  |  0.54 |
| LINQ (ordered)        |     9 |   0.658 s  |   0.041 s  |   0.019 s  |  3.35 |
| Classic (ordered)     |     9 |   0.686 s  |   0.037 s  |   0.020 s  |  3.49 |

| Measurement set       |     N |       Mean |      Error |     StdDev | Ratio |
| --------------------- | ----- | ---------- | ---------- | ---------- | ----- |
| LINQ (not ordered)    |     9 |   0.197 s  |   0.038 s  |   0.020 s  |  1.00 |
| Classic (not ordered) |     9 |   0.106 s  |   0.000 s  |   0.000 s  |  0.54 |
| LINQ (ordered)        |     9 |   0.659 s  |   0.041 s  |   0.019 s  |  3.35 |
| Classic (ordered)     |     9 |   0.690 s  |   0.036 s  |   0.019 s  |  3.51 |

| Measurement set       |     N |       Mean |      Error |     StdDev | Ratio |
| --------------------- | ----- | ---------- | ---------- | ---------- | ----- |
| LINQ (not ordered)    |     9 |   0.196 s  |   0.035 s  |   0.019 s  |  1.00 |
| Classic (not ordered) |     9 |   0.106 s  |   0.001 s  |   0.001 s  |  0.54 |
| LINQ (ordered)        |     9 |   0.659 s  |   0.039 s  |   0.018 s  |  3.36 |
| Classic (ordered)     |     9 |   0.691 s  |   0.035 s  |   0.019 s  |  3.53 |

Platform environment: Unix
Runtime environment:  .NETCoreApp,Version=<code>v7.0</code>

| Measurement set       |     N |       Mean |      Error |     StdDev | Ratio |
| --------------------- | ----- | ---------- | ---------- | ---------- | ----- |
| LINQ (not ordered)    |     9 |   0.196 s  |   0.038 s  |   0.020 s  |  1.00 |
| Classic (not ordered) |     9 |   0.105 s  |   0.001 s  |   0.000 s  |  0.54 |
| LINQ (ordered)        |     9 |   0.661 s  |   0.050 s  |   0.025 s  |  3.38 |
| Classic (ordered)     |     9 |   0.764 s  |   0.035 s  |   0.019 s  |  3.91 |

| Measurement set       |     N |       Mean |      Error |     StdDev | Ratio |
| --------------------- | ----- | ---------- | ---------- | ---------- | ----- |
| LINQ (not ordered)    |     9 |   0.198 s  |   0.038 s  |   0.020 s  |  1.00 |
| Classic (not ordered) |     9 |   0.108 s  |   0.000 s  |   0.000 s  |  0.55 |
| LINQ (ordered)        |     9 |   0.660 s  |   0.042 s  |   0.020 s  |  3.33 |
| Classic (ordered)     |     9 |   0.778 s  |   0.036 s  |   0.018 s  |  3.93 |

| Measurement set       |     N |       Mean |      Error |     StdDev | Ratio |
| --------------------- | ----- | ---------- | ---------- | ---------- | ----- |
| LINQ (not ordered)    |     9 |   0.197 s  |   0.038 s  |   0.020 s  |  1.00 |
| Classic (not ordered) |     9 |   0.103 s  |   0.001 s  |   0.001 s  |  0.52 |
| LINQ (ordered)        |     9 |   0.657 s  |   0.042 s  |   0.019 s  |  3.33 |
| Classic (ordered)     |     9 |   0.771 s  |   0.036 s  |   0.019 s  |  3.91 |

历史

  • 2022 年 11 月 1 日:初始版本
  • 2022 年 11 月 6 日:更正:C++ 的排序未被禁用。因此,C++ 的所有测量值都需要重新进行。
  • 2022 年 11 月 9 日:更正:C# 的排序是对输入数据而不是结果数据进行排序,C# 的所有测量值都需要重新进行。
  • 2022 年 11 月 16 日:更新 1 - C++ 和 C# 的增强代码,.NET 7.0
© . All rights reserved.