我在http://AdventOfCode.com(第4天挑战赛)中获得了一些乐趣
他们的一个问题涉及获取每个数字1,2,3,...,将其视为字符串并计算该字符串的MD5哈希值。该问题需要第1个(最小的)数字,其MD5哈希值(十六进制)以6个零开始。
我用一个简单的for
解决了它,但花了大约35秒(在2012 i5 Macbook上的Win10 VM中运行)。
看到CPU利用率相当低,我尝试了我脑海中最简单的优化 - TPL,更确切地说是Parallel.For
。
令我惊讶的是,(第一个)结果在42秒后被检索到,因此比单线程更糟糕。正如预期的那样,CPU利用率正在上升。
这是我的C#代码。为单线程与TPL注释一行或另一行。
using System;
using System.Text;
using System.Threading;
using System.Security.Cryptography;
using System.Diagnostics;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
Day4.P2();
}
}
class Day4
{
//not thread-safe, make one instance per thread
static ThreadLocal<MD5> md5Hash = new ThreadLocal<MD5>(MD5.Create);
public static int P2()
{
string input = "yzbqklnj";
var sw = Stopwatch.StartNew();
Action<int> checkAction = i =>
{
var hashBytes = md5Hash.Value.ComputeHash(Encoding.UTF8.GetBytes(input + i));
if ( hashBytes[0] == 0 && hashBytes[1] == 0 && (hashBytes[2]) == 0 )
{
Console.WriteLine(i + ": " + sw.Elapsed);
}
};
//for (var i = 0;; i++) { checkAction(i); }
Parallel.For(1, int.MaxValue, checkAction);
return 0;
}
}
我的问题是:为什么并行版本不是很优越?
如何在线程之间对数据进行分区?
PS。在实际的Windows机器上运行时结果是相似的,但是(预期)第一个结果是不最小(即问题的正确结果)。
答案 0 :(得分:1)
为什么并行版本不是很优越?
因为没有理由这样做。没有保证处理顺序。情况可能是你的线程都忙着处理没有前6个字符0
的数字,而顺序版本的线程比第一个正确的数字更快。< / p>
它是如何[即TPL]在其线程中划分数据?
MSDN上没有提到确切的方法,但关键原则是负载平衡。在Data Parallelism (Task Parallel Library)上引用MSDN页面:
在幕后,任务计划程序根据任务划分任务 系统资源和工作量。如果可能,调度程序 如果是,则在多个线程和处理器之间重新分配工作 工作量变得不平衡。
最后,并行版本的答案是错误的,但是,我得到的并行与顺序的数字与你所说的大不相同。我得到了:
此外,稍后,并行版本分别以21.7秒(9962624),22.06秒(541160794),23.59秒(541640646)给出了下一个数字。
我没有什么革命性的可以在这里得出结论,只是为了重申这一点
答案 1 :(得分:1)
行为是可以预期的,因为所有线程只是以某种方式划分范围1..int.MaxValue
。这是一个巨大的范围,所以几乎所有的线程都可以处理大量的数字。一个线程可以做有用的工作并从头开始,但即使这样也不能保证,因此结果是不可预测的。我测量了你的程序的这个时间(纠正结果的时间):
original serial: 00:00:28.27
original parallel: 00:00:24.53
您可以手动编码分块,但有一件事要尝试,将序列定义为有序。
int result = Enumerable.Range(1, int.MaxValue)
//.AsParallel()
//.AsOrdered()
.Where(i =>
{
var hashBytes = md5Hash.Value.ComputeHash(Encoding.UTF8.GetBytes(input + i));
return (hashBytes[0] == 0 && hashBytes[1] == 0 && hashBytes[2] == 0);
}).First();
Console.WriteLine(result + ": " + sw.Elapsed);
我首先注释掉两行以使其成为串行。
enumerable serial: 00:00:26.68
ordered parallel: 00:01:53.41
这真是一个惊喜。虽然第一个数字实际上很快找到(可以在大约9.2秒内以Where
条件打印到控制台),但事实证明引擎在每个线程返回至少一个值(或用完之前)不会合并结果顺序,大概)。所以大多数时候我们都在等待最慢的线程找到它的值。但是将Console.WriteLine
返回到Where
条件会返回订单问题。虽然结果有保证,但处理顺序不是。
最后,分块并不那么难
const int chunkSize = 100000;
int result = int.MaxValue;
object foundLock = new object();
for (int chunk = 1; chunk < int.MaxValue; chunk += chunkSize)
{
Parallel.For(chunk, chunk + chunkSize, (i) =>
{
var hashBytes = md5Hash.Value.ComputeHash(Encoding.UTF8.GetBytes(input + i));
if (hashBytes[0] == 0 && hashBytes[1] == 0 && hashBytes[2] == 0)
{
lock (foundLock)
{
result = Math.Min(result, i);
}
}
});
if (result < int.MaxValue)
{
Console.WriteLine(result + ": " + sw.Elapsed);
break;
}
}
结果时间
chunked parallel: 00:00:08.85
答案 2 :(得分:0)
如何在线程之间对数据进行分区?
这是关键问题。这没有指定。它可能首先巧合地检查坏数字。也许你得到了一个错误的答案,因为你可能不一定得到最小的i
,只有任何一个。
我会这样做:按顺序处理大小数量,例如10000个。一旦找到匹配,就会中止所有比具有匹配的块大的块的处理。您可能会发现一些更小的匹配,并且必须选择最小的匹配。
我不太确定如何使用TPL最好地实现这一点。 Parallel.ForEach
循环可以中止,但我不知道它们是否可以可靠地订购。可能。
答案 3 :(得分:0)
随着操作的增长,并行化是有意义的,如果每个操作要完成的工作非常小,并行化时可能会变慢,因为增加线程和在它们之间切换的额外成本可能高于使用多个CPU的节省
要测试这个,如果您执行以下操作,您会得到类似的结果: 将int.MaxValue更改为int.MaxValue / 1000并使用for j = 1到1000将lambda的INSIDE包装起来,以确保一个单元的工作量增加1000倍,从而减少安排较少任务的时间,并在每个任务中“更多”时间
根据你到达的结果,我们可以得出一些结论。
答案 4 :(得分:0)
尝试在每次迭代时调用Console.WriteLine(...)
。你可能会看到至少和我在机器上看到的一样有趣......
2: 00:00:00.0030730
1: 00:00:00.0031281
1073741824: 00:00:00.0033078
1073741825: 00:00:00.0080216
1073741826: 00:00:00.0080340
1073741827: 00:00:00.0080457
...
1073745189: 00:00:00.0663925
1073745190: 00:00:00.0664038
1073745191: 00:00:00.0664155
85: 00:00:00.0489811
86: 00:00:00.0666171
87: 00:00:00.0666364
...
40451: 00:00:01.1846214
1073753653: 00:00:01.1846293
40452: 00:00:01.1846365
1073753654: 00:00:01.1846440
40453: 00:00:01.1846527
1073753655: 00:00:01.1846633
40454: 00:00:01.1846750
... etc.
循环计数器可以非常快速地增加,但由于更多涉及的计算无法跟上,您可能会看到各种各样的值以看似任意的顺序进行测试。
按照usr的建议进行块测试(块不需要大于系统中的核心数)是利用多线程处理的一个想法,但请记住,你打破了逻辑这样做可以保证算法的正确性。以指定顺序运行测试不是多线程解决方案可以保证的。