【发布时间】:2012-01-22 08:41:42
【问题描述】:
这两者有什么区别?
最好的比较方法是什么?
plinq 总是更好?
我们什么时候使用 plinq ?
【问题讨论】:
-
我认为可以相当安全地假设如果使用 PLINQ 总是更好,那么 LINQ 就不会存在。结果:使用 PLINQ 并不总是更好。
这两者有什么区别?
最好的比较方法是什么?
plinq 总是更好?
我们什么时候使用 plinq ?
【问题讨论】:
Linq 是一组共同解决类似问题的技术 - 在所有这些技术中,您都有一个数据源(xml 文件或文件、数据库内容、内存中的对象集合)并且您想要检索部分或全部这些数据并以某种方式对其采取行动。 Linq 致力于解决这组问题的共性,例如:
var brithdays = from user in users where
user.dob.Date == DateTime.Today && user.ReceiveMails
select new{user.Firstname, user.Lastname, user.Email};
foreach(bdUser in birthdays)
SendBirthdayMail(bdUser.Firstname, bdUser.Lastname, bdUser.Email);
以及等价物(通过传统 C# 语法显式使用 Linq 相关的类和方法):
var birthdays = users
.Where(user => user.dob.Date == DateTime.Today)
.Select(user => new{user.Firstname, user.Lastname, user.Email});
foreach(bdUser in birthdays)
SendBirthdayMail(bdUser.Firstname, bdUser.Lastname, bdUser.Email);
这两个代码示例都可以工作,无论它是要转换为数据库调用、解析 xml 文档还是搜索对象数组。
唯一的区别是users 是什么类型的对象。如果它是列表、数组或其他可枚举集合,它将是 linq-to-objects,如果它是 System.Data.Linq.Table,它将是 linq to sql。前者会导致内存中的操作,后者会导致 SQL 查询,然后尽可能晚地反序列化为内存中的对象。
如果它是 ParallelQuery - 通过在内存中可枚举集合上调用 .AsParallel 生成 - 那么查询将在内存中执行,并行化(大部分时间)以便由多个线程执行 -理想情况下,让每个核心都忙于推进工作。
显然这里的想法是更快。当它运行良好时,它会起作用。
但也有一些缺点。
首先,进行并行化总是有一些开销,即使在最终无法并行化的情况下也是如此。如果没有对数据进行足够的工作,这种开销将超过任何潜在收益。
其次,并行处理的好处取决于可用的内核。如果查询最终不会阻塞 4 核机器上的资源,理论上你会得到 4 倍的加速(4 个超线程可能会给你更多甚至更少,但可能不是 8 倍,因为 hyper-线程将 CPU 的某些部分加倍并没有明显增加两倍)。在单核上使用相同的查询,或者处理器亲和性意味着只有一个核可用(例如,处于“网络花园”模式的网络服务器),那么就没有加速。如果资源被阻塞,仍然会有收益,但收益取决于机器。
第三,如果有任何共享资源(可能正在输出一个集合结果)以非线程安全的方式使用,它可能会出现错误结果、崩溃等严重错误。
第四,如果以线程安全的方式使用共享资源,并且线程安全来自锁定,则可能存在足够多的争用成为瓶颈,从而抵消并行化带来的所有好处。
第五,如果您有一台四核机器在四个不同的线程上运行或多或少相同的算法(可能由于四个客户端而在客户端-服务器情况下,或者在一组类似任务的桌面情况下)在这个过程中更高),那么他们已经充分利用了这些核心。将算法中的工作拆分以便在所有四个内核上处理意味着您已经从每个使用一个内核的四个线程转移到了争夺四个内核的 16 个线程。充其量是一样的,并且可能的开销会使情况变得更糟。
它可以在很多情况下仍然是一个重大胜利,但上面应该清楚地表明它不会总是。
【讨论】:
我还想知道何时使用 PLINQ 而非 LINQ,所以我进行了一些测试。
总结: 在决定使用 LINQ 还是 PLINQ 运行查询时,有两个问题需要回答。
运行查询涉及多少次迭代(集合中有多少对象)?
一次迭代涉及多少工作?
除非 PLINQ 性能更高,否则请使用 LINQ。如果查询集合涉及太多迭代和/或每次迭代涉及太多工作,则 PLINQ 可能比 LINQ 性能更高。
但随后出现了两个难题:
我的建议是测试您的查询。使用 LINQ 测试一次,使用 PLINQ 测试一次,然后比较两个结果。
测试 1:通过增加集合中对象的数量来增加查询中的迭代次数。
初始化 PLINQ 的开销大约需要 20 毫秒。如果不利用 PLINQ 的优势,这是浪费时间,因为 LINQ 有 0 毫秒的开销。
每次迭代所涉及的工作对于每个测试始终是相同的。工作量很少。
工作定义:将int(集合中的对象)乘以10。
在迭代 100 万个对象且每次迭代涉及最少的工作时,PLINQ 比 LINQ 更快。尽管在专业环境中,我从未查询甚至初始化内存中包含 1000 万个对象的集合,因此 PLINQ 恰好优于 LINQ 的情况不太可能发生。
╔═══════════╦═══════════╦════════════╗
║ # Objects ║ LINQ (ms) ║ PLINQ (ms) ║
╠═══════════╬═══════════╬════════════╣
║ 1 ║ 1 ║ 20 ║
║ 10 ║ 0 ║ 18 ║
║ 100 ║ 0 ║ 20 ║
║ 1k ║ 0 ║ 23 ║
║ 10k ║ 1 ║ 17 ║
║ 100k ║ 4 ║ 37 ║
║ 1m ║ 36 ║ 76 ║
║ 10m ║ 392 ║ 285 ║
║ 100m ║ 3834 ║ 2596 ║
╚═══════════╩═══════════╩════════════╝
测试 2:增加迭代涉及的工作量
我将集合中的对象数设置为始终为 10,因此查询涉及的迭代次数较少。对于每个测试,我都增加了处理每次迭代所涉及的工作。
工作定义:将int(集合中的对象)乘以10。
增加工作的定义:增加迭代次数将int乘以10。
PLINQ 查询集合的速度更快,因为当工作迭代中的迭代次数增加到 1000 万时,工作显着增加,我得出结论,当单个迭代涉及这么多工作时,PLINQ 优于 LINQ。
本表中的“#Iterations”表示一个工作迭代中的迭代次数。请参阅下面的测试 2 代码。
╔══════════════╦═══════════╦════════════╗
║ # Iterations ║ LINQ (ms) ║ PLINQ (ms) ║
╠══════════════╬═══════════╬════════════╣
║ 1 ║ 1 ║ 22 ║
║ 10 ║ 1 ║ 32 ║
║ 100 ║ 0 ║ 25 ║
║ 1k ║ 1 ║ 18 ║
║ 10k ║ 0 ║ 21 ║
║ 100k ║ 3 ║ 30 ║
║ 1m ║ 27 ║ 52 ║
║ 10m ║ 263 ║ 107 ║
║ 100m ║ 2624 ║ 728 ║
║ 1b ║ 26300 ║ 6774 ║
╚══════════════╩═══════════╩════════════╝
测试 1 代码:
class Program
{
private static IEnumerable<int> _numbers;
static void Main(string[] args)
{
const int numberOfObjectsInCollection = 1000000000;
_numbers = Enumerable.Range(0, numberOfObjectsInCollection);
var watch = new Stopwatch();
watch.Start();
var parallelTask = Task.Run(() => ParallelTask());
parallelTask.Wait();
watch.Stop();
Console.WriteLine($"Parallel: {watch.ElapsedMilliseconds}ms");
watch.Reset();
watch.Start();
var sequentialTask = Task.Run(() => SequentialTask());
sequentialTask.Wait();
watch.Stop();
Console.WriteLine($"Sequential: {watch.ElapsedMilliseconds}ms");
Console.ReadKey();
}
private static void ParallelTask()
{
_numbers
.AsParallel()
.Select(x => DoWork(x))
.ToArray();
}
private static void SequentialTask()
{
_numbers
.Select(x => DoWork(x))
.ToArray();
}
private static int DoWork(int @int)
{
return @int * 10;
}
}
测试 2 代码:
class Program
{
private static IEnumerable<int> _numbers;
static void Main(string[] args)
{
_numbers = Enumerable.Range(0, 10);
var watch = new Stopwatch();
watch.Start();
var parallelTask = Task.Run(() => ParallelTask());
parallelTask.Wait();
watch.Stop();
Console.WriteLine($"Parallel: {watch.ElapsedMilliseconds}ms");
watch.Reset();
watch.Start();
var sequentialTask = Task.Run(() => SequentialTask());
sequentialTask.Wait();
watch.Stop();
Console.WriteLine($"Sequential: {watch.ElapsedMilliseconds}ms");
Console.ReadKey();
}
private static void ParallelTask()
{
_numbers
.AsParallel()
.Select(x => DoWork(x))
.ToArray();
}
private static void SequentialTask()
{
_numbers
.Select(x => DoWork(x))
.ToArray();
}
private static int DoWork(int @int)
{
const int numberOfIterations = 1000000000;
for (int i = 0; i < numberOfIterations; i++)
{
@int = @int * 10;
}
return @int;
}
}
【讨论】:
PLinq 是 Linq 的并行版本。一些查询可以在多个线程上执行,然后 PLinq 会提高性能。
但是,其他查询不能并行执行,否则会给出错误的结果。因此,您应该为每个查询决定何时使用 PLinq,并确保性能确实有所提高。
MSDN 有很多关于它的文档。
【讨论】:
考虑在使用PLINQ 时避免使用anonymous types,因为根据Threading in C#, by Joe Albahari:
匿名类型(作为类,因此是引用类型)会产生基于堆的分配和后续垃圾回收的成本。
(...)
基于堆栈的分配是高度可并行化的(因为每个线程都有自己的堆栈),而所有线程必须竞争同一个堆 - 由单个内存管理器和垃圾收集器管理。
【讨论】:
PLINQ 可以通过更有效地使用主机上的所有可用内核来显着提高 LINQ to Objects 查询的速度。这种提高的性能为桌面带来了高性能计算能力。
【讨论】: