英文原文:https://www.jacksondunstan.com/articles/2994
译者测试结论:
Test,Manual Time,Normal Time,LINQ Time
Where,55,402,93
Select,83,592,199
2024年2月份,Unity版本:2021.3.29,
目前LINQ已经比原文作者的时候有了非常明显的进步
除了手动创建结果数组,其他任何方式和LINQ相比都没有明显的优势
SQL 样式的 LINQ 查询是执行处理各种集合的各种任务的简洁、可读的方式。当然,所有这些便利都伴随着性能成本。你觉得有多糟糕?今天,我们将了解一些基本 LINQ 查询(Where、Select)与等效的非 LINQ 代码的成本。我们还将看到它们与手动编写的传统代码相比要慢多少,传统代码失去了所有灵活性。继续阅读以查看结果!
以下测试使用 System.Linq 命名空间中找到的 LINQ 扩展函数版本。它们看起来与 C# 关键字非常相似,但提供了更多的灵活性,使我们能够仅隔离我们想要的部分。这是一个简单的例子:
someCollection.Where(element => element == 6); // 所有为 6 的元素
someCollection.Select(element => element * 2); // 将每个元素加倍
我们将把它们与同样通用的手写函数进行比较:
// 相当于 Where()
private List<TInput> WhereNormal<TInput>(
IEnumerable<TInput> enumeration,
Func<TInput, bool> checker
)
{
var found = new List<TInput>();
foreach (var elem in enumeration)
{
if (checker(elem))
{
found.Add(elem);
}
}
return found;
}
// 相当于 Select()
private List<TResult> SelectNormal<TInput,TResult>(
IEnumerable<TInput> enumeration,
Func<TInput,TResult> transformer
)
{
var transformed = new List<TResult>();
foreach (var elem in enumeration)
{
transformed.Add(transformer(elem));
}
return transformed;
}
对于基线,我们将使用一些不太通用的“手动”函数。他们知道枚举是一个数组以及数组中元素的类型。它们直接包含选择器或转换器功能。基本上,这是针对特定问题编写特定代码的老式方法。这也不是一场公平的战斗。
// Where() 的手动版本
private List<int> WhereManual(int[] array)
{
var found = new List<int>();
for (int i = 0, len = array.Length; i < len; ++i)
{
var elem = array[i];
if (elem == 1)
{
found.Add(elem);
}
}
return found;
}
// Select() 的手动版本
private int[] SelectManual(int[] array)
{
var len = array.Length;
var transformed = new int[len];
for (var i = 0; i < len; ++i)
{
transformed[i] = array[i] * 2;
}
return transformed;
}
现在让我们将它们相互竞争,看看谁最快。我们将使用一个包含 1024 个元素的数组(全部为零),并在Where()中查找值1,并将Select中的每个元素加倍。这是测试程序:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using UnityEngine;
public static class StopwatchExtensions
{
public static long RunTests(
this Stopwatch stopwatch,
int numIterations,
Action testFunction
)
{
stopwatch.Reset();
stopwatch.Start();
for (var i = 0; i < numIterations; ++i)
{
testFunction();
}
return stopwatch.ElapsedMilliseconds;
}
}
public class TestScript : MonoBehaviour
{
private string report;
void Start()
{
var array = new int[1024];
var numIterations = 10000;
var stopwatch = new Stopwatch();
Func<int,bool> checker = val => val == 1;
var whereManualTime = stopwatch.RunTests(
numIterations,
() => WhereManual(array)
);
var whereNormalTime = stopwatch.RunTests(
numIterations,
() => WhereNormal<int>(array, checker)
);
var whereLINQTime = stopwatch.RunTests(
numIterations,
() => WhereLINQ<int>(array, checker)
);
Func<int,int> transformer = val => val * 2;
var selectManualTime = stopwatch.RunTests(
numIterations,
() => SelectManual(array)
);
var selectNormalTime = stopwatch.RunTests(
numIterations,
() => SelectNormal<int,int>(array, transformer)
);
var selectLINQTime = stopwatch.RunTests(
numIterations,
() => SelectLINQ<int,int>(array, transformer)
);
report =
"Test,Manual Time,Normal Time,LINQ Time\n" +
"Where," + whereManualTime + "," + whereNormalTime + "," + whereLINQTime + "\n" +
"Select," + selectManualTime + "," + selectNormalTime + "," + selectLINQTime + "\n";
}
void OnGUI()
{
GUI.TextArea(new Rect(0, 0, Screen.width, Screen.height), report);
}
private List<int> WhereManual(int[] array)
{
var found = new List<int>();
for (int i = 0, len = array.Length; i < len; ++i)
{
var elem = array[i];
if (elem == 1)
{
found.Add(elem);
}
}
return found;
}
private List<TInput> WhereNormal<TInput>(
IEnumerable<TInput> enumeration,
Func<TInput, bool> checker
)
{
var found = new List<TInput>();
foreach (var elem in enumeration)
{
if (checker(elem))
{
found.Add(elem);
}
}
return found;
}
private List<TInput> WhereLINQ<TInput>(
IEnumerable<TInput> enumeration,
Func<TInput, bool> checker
)
{
return enumeration.Where(checker).ToList();
}
private int[] SelectManual(int[] array)
{
var len = array.Length;
var transformed = new int[len];
for (var i = 0; i < len; ++i)
{
transformed[i] = array[i] * 2;
}
return transformed;
}
private List<TResult> SelectNormal<TInput,TResult>(
IEnumerable<TInput> enumeration,
Func<TInput,TResult> transformer
)
{
var transformed = new List<TResult>();
foreach (var elem in enumeration)
{
transformed.Add(transformer(elem));
}
return transformed;
}
private List<TResult> SelectLINQ<TInput,TResult>(
IEnumerable<TInput> enumeration,
Func<TInput,TResult> transformer
)
{
return enumeration.Select(transformer).ToList();
}
}
如果您想自己尝试测试,只需将上述代码粘贴到 Unity 项目的 Assets 目录中的 TestScript.cs 文件中,并将其附加到新的空项目中的主相机游戏对象。然后以 64 位处理器的非开发模式进行构建,并以 640×480 的窗口速度和最快的图形运行它。我在这台机器上这样运行:
- 2.3 GHz 英特尔酷睿 i7-3615QM
- Mac OS X 10.10.2
- Unity 5.0.0f4、Mac OS X 独立版、x86_64、非开发
- 640×480,最快,窗口化
并得到这些结果:
令人惊讶的是,至少对我来说,LINQ 并不比使用 foreach 循环的“正常”版本慢多少。考虑到需要编写、维护和理解更多的代码才能实现很少的性能,LINQ 查询似乎很划算。
然而,“手动”测试要快得多。这是可以预料的,因为他们通过了解问题的所有细节来“作弊”:元素类型、枚举类型以及每个元素要执行的函数。它们需要编写的代码与“正常”版本一样多,只是灵活性较差,速度却更快。通过删除通用代码并直接手动解决问题,可以获得 10 倍的性能增益。
因此,当涉及 LINQ 查询时,请注意它们比手动编写代码要慢一个数量级。如果您发现带有 LINQ 查询的函数会减慢您的应用程序的速度,请考虑使用“手动”方法重写 LINQ 查询。您可能会获得巨大的性能提升!
您是否遇到过 LINQ 特定的性能问题?知道 LINQ 的哪些用途特别慢吗?在评论中分享您的经验!
译者测试结论:
Test,Manual Time,Normal Time,LINQ Time
Where,55,402,93
Select,83,592,199
2024年2月份,Unity版本:2021.3.29,
目前LINQ已经比原文作者的时候有了非常明显的进步
除了手动创建结果数组,其他任何方式和LINQ相比都没有明显的优势