C#多线程/优化数组迭代以获得有趣的值(最小值、最大值、总和、平均值)
Posted
技术标签:
【中文标题】C#多线程/优化数组迭代以获得有趣的值(最小值、最大值、总和、平均值)【英文标题】:C# Multithread / Optimize iteration over array for interesting values (Min, Max, Sum, Average) 【发布时间】:2015-04-13 11:19:07 【问题描述】:我需要优化这个经常被调用的函数:
private static InterstingDataValues CalculateFor(IData data)
InterstingDataValues dataValues = new InterstingDataValues(null);
float[] pixels = data.ReadAsFloatBuffer();
if (pixels == null)
return null;
float value1 = pixels[0];
if (float.IsNaN(value1))
return null;
dataValues.HighestIntensityInData = float.MinValue;
dataValues.LowestIntensityInData = float.MaxValue;
for (int i = 0; i < pixels.Length; ++i)
float pixelf = pixels[i];
if (float.IsNaN(pixelf))
pixelf = 0;
dataValues.SumIntensity += (uint)pixelf;
dataValues.HighestIntensityInData = Math.Max(dataValues.HighestIntensityInData, pixelf);
dataValues.LowestIntensityInData = Math.Min(dataValues.LowestIntensityInData, pixelf);
dataValues.AverageIntensity = dataValues.SumIntensity / (uint)pixels.Count();
if (double.IsNaN(dataValues.HighestIntensityInData))
dataValues.HighestIntensityInData = float.MaxValue;
if (double.IsNaN(dataValues.LowestIntensityInData))
dataValues.LowestIntensityInData = 0;
return dataValues;
我注意到 C# 有类似的内置函数
pixels.Max()
pixels.Min()
pixels.Sum()
pixels.Average()
我认为这是很好的优化。但是我的感觉是单独调用这些会比一起调用效率低得多。
我目前的想法是将数组块发送到单独的线程以获得最小/最大/总和。然后当我得到块的结果时,我可以运行 min,max,sum 对块的结果。
但我感觉 C# 将通过 Parallel.For 有一些内置的方式来执行此操作,但我得到了 worried at answers to this
由于“互锁”这个词,我需要做更多的研究,但是我想知道我是否走在正确的轨道上。
谢谢,克里斯
【问题讨论】:
您是否看到该答案中的第二段代码,显示了正确的操作方式? Nonononononono :D 首先,找出代码实际花费时间的地方。你的猜测可能是错误的——使用分析器。其次,我可以看到的唯一可以提高性能的简单事情是最大值和最小值 - 无论值是否更改,您都总是分配。这可能意味着性能略有不同。但是,个人资料。这些天猜测对你没有多大帮助:) 这应该很容易并行化,只是避免共享状态 - 让每个并行执行计算它们的部分,并在最后聚合它。它可能会有所帮助:) @LasseV.Karlsen,是的,我做到了,但我仍然对联锁这个词感到担心。 (x) => Interlocked.Add(ref sum, x)。但如果我没记错的话,他正在做我通过那种方法提出的建议? @Luaan 我确实做了一些分析,我尝试了缓存,这确实有很大帮助。但是数据在变化,所以缓存会经常失效。我应该更清楚。但这是最好优化的区域。 我怀疑您是否可以通过 Parallel for (Sum/Min/Max) 以任何方式对其进行优化 - 而是将像素阵列分成几部分并使用您必须并行计算这些部分的方法最后合并它们 【参考方案1】:data.ReadAsFloatBuffer() 似乎是唯一的冗余调用,消除它应该是您的首要任务。您应该在循环中查找数据,而不是将其复制到固定的连续数组中。
【讨论】:
【参考方案2】:如果这确实是您需要的部分,那么并行化循环真的很容易:
public void Main()
int[] array = new int[] 12, 15, 18, 64, 3, 68, 32 ;
object sync = new object();
var results = new List<Result>();
Parallel.ForEach
(
array,
() => default(Result),
(item, s, input) => input.Add(item),
input => lock (sync) results.Add(input);
);
var aggregatedResult = results.Aggregate((acc, item) => acc.Add(item));
aggregatedResult.Dump();
public struct Result
public readonly int Sum;
public readonly int? Min;
public readonly int? Max;
public Result(int sum, int min, int max)
Sum = sum;
Min = min;
Max = max;
public Result Add(int item)
return
new Result
(
Sum + item,
Min.HasValue && Min.Value < item ? Min.Value : item,
Max.HasValue && Max.Value > item ? Max.Value : item
);
public Result Add(Result partialResult)
return
new Result
(
Sum + partialResult.Sum,
Min.HasValue && Min.Value < partialResult.Min
? Min.Value : partialResult.Min.GetValueOrDefault(0),
Max.HasValue && Max.Value > partialResult.Max
? Max.Value : partialResult.Max.GetValueOrDefault(0)
);
我并不是说这是最好的方法(我特别不喜欢那里的lock
,我确信有更好的方法),但它非常简单。请注意,除了最终数据的聚合之外的所有内容都是并行化的 - 与在数组中的数十万个项目中执行相同操作相比,将几个结构聚合在一起将相当便宜。
还要注意如何使用可空类型来处理诸如“没有价值”之类的边缘情况 - default(float?)
比使用 NaN
好得多。尽量使用最好的类型来描述您的场景。
另外,我不确定ForEach
将数组拆分成多个部分有多聪明——改用For
可能会更好。不过,原则保持不变。
【讨论】:
【参考方案3】:发布者可能会迟到,但可能对其他人有用。当问一个愚蠢的问题时,我在这里得到了 Corey 的精彩回答:Parallel.For() with Interlocked.CompareExchange(): poorer performance and slightly different results to serial version
Corey 提请我注意 Enumerable.Aggregate
函数 (https://msdn.microsoft.com/en-us/library/system.linq.enumerable.aggregate(v=vs.110).aspx),使用该函数可以并行完成此类任务而无需锁定。据我了解,您定义了一个累加器类来保存来自同一线程的临时数据,然后定义一个函数来合并来自并行线程的部分数据。最好用一个例子来说明。下面的扩展方法在IList<double>
中搜索最小值和最大值:
public static class Statistics
internal class ExtremumAccumulator
internal double Min;
internal double Max;
/// <summary>
/// An aggregate parallel query to return the minimum and the maximum of <paramref name="data"/> together, faster than two successive parallel queries to minimum and maximum.
/// </summary>
/// <param name="data">The list whose extrema we are to find.</param>
/// <returns>A <see cref="Tupledouble, double"/> instance whose <see cref="Tupledouble, double.Item1"/> represents the minimum and whose <see cref="Tupledouble, double.Item2"/> contains the maximum of <paramref name="data"/>.</returns>
public static Tuple<double, double> Extrema(this IList<double> data)
ParallelQuery<double> query = data.AsParallel();
return query.Aggregate(
// Initialise accumulator:
() => new ExtremumAccumulator() Min = Double.MaxValue, Max = Double.MinValue ,
// Aggregate calculations:
(accumulator, item) => if (item < accumulator.Min) accumulator.Min = item; if (item > accumulator.Max) accumulator.Max = item; return accumulator; ,
// Combine accumulators:
(accumulator1, accumulator2) => new ExtremumAccumulator() Min = Math.Min(accumulator1.Min, accumulator2.Min), Max = Math.Max(accumulator1.Max, accumulator2.Max) ,
// Get result:
accumulator => new Tuple<double, double>(accumulator.Min, accumulator.Max)
);
【讨论】:
以上是关于C#多线程/优化数组迭代以获得有趣的值(最小值、最大值、总和、平均值)的主要内容,如果未能解决你的问题,请参考以下文章