对多个属性求和的更有效方法?



到目前为止,我有一个解决方案:

struct MyStruct {
int propA;
int propB;
int propC;
}
/*Extension Method*/
function MyStruct Total(this List<MyStruct> toSum) {
var sum = new MyStruct();
toSum.ForEach(x => {
sum.propA += x.propA;
sum.propB += x.propB;
sum.propC += x.propC;
});
return sum;
}

这将循环访问列表一次,将每个属性相加。我觉得有一种更优雅、更简单的解决方案,它甚至可能性能更高,但目前还没有想到更好的方法。我不认为我正在做的事情有什么问题,但我想改进它,我希望有人能够帮助我找到更好的方法来做到这一点。

在我的机器中,你的代码已经可以对每秒大约 70M 的结构进行总和,如果你问我,这已经相当不错了。但是,匿名委托是有代价的,因为它们不能内联。因此,用普通的 vanilla 循环替换List.ForEach会使您的代码速度提高 2 倍(每秒 160M 结构(。

public static MyStruct Total(this List<MyStruct> source)
{
var sum = new MyStruct();
var count = source.Count;
for (int i = 0; i < count; i++)
{
var x = source[i];
sum.PropA += x.PropA;
sum.PropB += x.PropB;
sum.PropC += x.PropC;
}
return sum;
}

使用并行性可将执行速度再提高 1.5 倍。现在,您每秒对 240M 个结构求和,但您使用的是机器的 3 个内核而不是一个。

public static MyStruct Total(this List<MyStruct> source)
{
var locker = new object();
var sum = new MyStruct();
var options = new ParallelOptions() { MaxDegreeOfParallelism = 3 };
Parallel.ForEach(Partitioner.Create(0, source.Count), options,
localInit: () => new MyStruct(), body: (range, state, local) =>
{
for (int i = range.Item1; i < range.Item2; i++)
{
var x = source[i];
local.PropA += x.PropA;
local.PropB += x.PropB;
local.PropC += x.PropC;
}
return local;
}, localFinally: (localSum) =>
{
lock (locker)
{
sum.PropA += localSum.PropA;
sum.PropB += localSum.PropB;
sum.PropC += localSum.PropC;
}
});
return sum;
}

通过使用Spans、Vector和棘手的内存对齐,您可以仅使用一个内核来实现并行代码的性能。这需要 .NET Core,并且还需要将数组而不是列表作为参数传递:

public static MyStruct Total(this MyStruct[] source)
{
Debug.Assert(Marshal.SizeOf(typeof(MyStruct)) == 12);
var span = new ReadOnlySpan<MyStruct>(source);
var intSpan = MemoryMarshal.Cast<MyStruct, int>(span);
var sum = new Vector<int>(0);
for (int i = 0; i < span.Length - 1; i++)
{
var vector = new Vector<int>(intSpan.Slice(i * 3, 4));
sum += vector;
}
// The last one must be added separately
sum += new Vector<int>(new int[] {
source[^1].PropA, source[^1].PropB, source[^1].PropC, 0 });
return new MyStruct() { PropA = sum[0], PropB = sum[1], PropC = sum[2] };
}

在这里,Vector结构没有得到充分利用,因为四个可用int插槽中只有三个被使用(第四个被丢弃(。在输入此代码路径之前,还应检查属性Vector.IsHardwareAccelerated,因为如果系统不支持单指令多数据 (SIMD( 指令,性能会很差。

作为使用forforeach的替代解决方案,它们可能是性能最佳的,您可以使用Aggregate扩展方法使用 LINQ 解决方案。

public static MyStruct Total(this IEnumerable<MyStruct> toSum)
=> toSum?.Aggregate(new MyStruct(), (acc, elem) =>
{
acc.propA += elem.propA;
acc.propB += elem.propB;
acc.propC += elem.propC;
return acc;
}) ?? new MyStruct();

这会将每个元素折叠在累加器上并返回填充的累加器。如果集合为 null 或为空,您将返回 MyStruct 的空实例。

您是否认为这更优雅可能是一个品味问题。

最新更新