[C#] LINQ 的CPU性能

文章比较了2021.3.29版本Unity中LINQ查询(Where和Select)与手动编写和传统代码的性能,结果显示除手动数组操作外,LINQ查询速度接近于传统方法,而手动方法显著更快,提示在追求性能时要考虑使用手动编写代码。
摘要由CSDN通过智能技术生成

英文原文: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相比都没有明显的优势

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值