对一个字节数组中的每个元素求和

4
现在,我对线程和异步/同步编程等技术都很陌生。因此,我一直在练习,并在YouTube上看到了这个问题。这个问题是要对一个字节数组的每个内容进行求和。它来自名为Jamie King的频道。他使用了线程对此进行了解决。我决定使用任务来解决此问题并将其变成异步,但比同步更慢。两者之间的差异是360毫秒!我想知道是否有人可以用异步方式更快地完成它。如果是,请发布!这是我的代码:
    static Random Random = new Random(999);

    static byte[] byteArr = new byte[100_000_000];
    static byte TaskCount = (byte)Environment.ProcessorCount;
    static int readingLength;

    static void Main(string[] args)
    {
        for (int i = 0; i < byteArr.Length; i++)
        {
            byteArr[i] = (byte)Random.Next(11);
        }

        SumAsync(byteArr);
    }

    static async void SumAsync(byte[] bytes)
    {
        readingLength = bytes.Length / TaskCount;
        int sum = 0;
        Console.WriteLine("Running...");

        Stopwatch watch = new Stopwatch();

        watch.Start();
        for (int i = 0; i < TaskCount; i++)
        {
            Task<int> task = SumPortion(bytes.SubArray(i * readingLength, readingLength));
            int result = await task;
            sum += result;
        }

        watch.Stop();

        Console.WriteLine("Done! Time took: {0}, Result: {1}", watch.ElapsedMilliseconds, sum);

    }

    static async Task<int> SumPortion(byte[] bytes)
    {
        Task<int> task = Task.Run(() =>
        {
            int sum = 0;
            foreach (byte b in bytes)
            {
                sum += b;
            }
            return sum;
        });

        int result = await task;

        return result;
    }

请注意,bytes.SubArray是一个扩展方法。我有一个问题。异步编程比同步编程慢吗? 请指出我的错误。
谢谢你的时间!

1
你尝试过使用 Parallel.For 吗?虽然不是异步的,但是它仍然是多线程的。 https://learn.microsoft.com/zh-cn/dotnet/standard/parallel-programming/how-to-write-a-simple-parallel-for-loop - Nanhydrin
3
尝试使用int count = byteArr.AsParallel().Sum(x => x); - Matthew Watson
这非常相关:https://dev59.com/0WUo5IYBdhLWcg3w7S-T#15665968 async 不是关于并行计算的。它是关于非阻塞主线程的。对于您的情况,启动新线程的成本很高。 - trailmax
你的 SubArray() 扩展方法是什么样子的?我觉得有点可疑。 - Matthew Watson
@Nanhydrin 不,我还没有涉足其中。 - Marc2001
@MatthewWatson 我已经用其他数组进行了测试,它运行良好。 - Marc2001
3个回答

3
您需要使用 WhenAll() 并在结尾返回所有任务:
    static async void SumAsync(byte[] bytes)
    {
        readingLength = bytes.Length / TaskCount;
        int sum = 0;
        Console.WriteLine("Running...");

        Stopwatch watch = new Stopwatch();

        watch.Start();
        var results = new Task[TaskCount];
        for (int i = 0; i < TaskCount; i++)
        {
            Task<int> task = SumPortion(bytes.SubArray(i * readingLength, readingLength));
            results[i] = task
        }
        int[] result = await Task.WhenAll(results);
        watch.Stop();

        Console.WriteLine("Done! Time took: {0}, Result: {1}", watch.ElapsedMilliseconds, result.Sum());

    }

当您使用WhenAll()方法时,您将组合所有的Task结果,因此这些任务将并行运行,节省了大量必要的时间。

您可以在learn.microsoft.com上了解更多信息。


@MisterMagoo 添加了解释 :) - Barr J
1
这真的帮了我很多。它从360毫秒的差异提高到106毫秒。非常感谢! - Marc2001

2

异步不是明显的慢 - 但在后台运行(例如等待连接到网站建立)- 这样主线程就不会因为等待某些事情发生而停止。


0

最快的方法可能是手动编写一个Parallel.ForEach()循环。

Plinq甚至可能无法比单线程方法提供加速,而且肯定不会像Parallel.ForEach()那样快。

以下是一些时间代码示例。在尝试此操作时,请确保它是发布版本,并且不要在调试器下运行它(即使它是发布版本,调试器也会关闭JIT优化器):

using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Demo
{
    static class Program
    {
        static void Main()
        {
            // Create some random bytes (using a seed to ensure it's the same bytes each time).

            var rng = new Random(12345);
            byte[] byteArr = new byte[500_000_000];
            rng.NextBytes(byteArr);

            // Time single-threaded Linq.

            var sw = Stopwatch.StartNew();
            long sum = byteArr.Sum(x => (long)x);
            Console.WriteLine($"Single-threaded Linq took {sw.Elapsed} to calculate sum as {sum}");

            // Time single-threaded loop;

            sw.Restart();
            sum = 0;

            foreach (var n in byteArr)
                sum += n;

            Console.WriteLine($"Single-threaded took {sw.Elapsed} to calculate sum as {sum}");

            // Time Plinq

            sw.Restart();
            sum = byteArr.AsParallel().Sum(x => (long)x);
            Console.WriteLine($"Plinq took {sw.Elapsed} to calculate sum as {sum}");

            // Time Parallel.ForEach() with partitioner.

            sw.Restart();
            sum = 0;

            Parallel.ForEach
            (
                Partitioner.Create(0, byteArr.Length),

                () => 0L,

                (subRange, loopState, threadLocalState) =>
                {
                    for (int i = subRange.Item1; i < subRange.Item2; i++)
                        threadLocalState += byteArr[i];

                    return threadLocalState;
                },

                finalThreadLocalState =>
                {
                    Interlocked.Add(ref sum, finalThreadLocalState);
                }
            );

            Console.WriteLine($"Parallel.ForEach with partioner took {sw.Elapsed} to calculate sum as {sum}");
        }
    }
}

在我的八核PC上使用x64构建得到的结果是:

  • 单线程Linq花费00:00:03.1160235计算总和为63748717461
  • 单线程花费00:00:00.7596687计算总和为63748717461
  • Plinq花费00:00:01.0305913计算总和为63748717461
  • Parallel.ForEach与分区器一起花费00:00:00.0839141计算总和为63748717461

在x86构建中得到的结果是:

  • 单线程Linq花费00:00:02.6964067计算总和为63748717461
  • 单线程花费00:00:00.8200462计算总和为63748717461
  • Plinq花费00:00:01.1251899计算总和为63748717461
  • Parallel.ForEach与分区器一起花费00:00:00.1084805计算总和为63748717461

正如您所看到的,使用x64构建的Parallel.ForEach()是最快的(可能是因为它正在计算一个long总数,而不是因为更大的地址空间)。

Plinq比非线程化的Linq解决方案快三倍左右。

使用分区器的Parallel.ForEach()速度超过30倍。

但值得注意的是,非Linq单线程代码比Plinq代码更快。在这种情况下,使用Plinq是没有意义的;它会使事情变慢!

这告诉我们,加速不仅来自多线程 - 它还与LinqPlinq的开销有关,相对于手动循环。

一般来说,只有在每个元素的处理时间相对较长(并且将一个字节添加到运行总数需要非常短的时间)时,才应该使用Plinq

使用 Plinq 而不是带分区器的 Parallel.ForEach() 的优势在于编写简单 - 然而,如果它比简单的 foreach 循环更慢,则其实用性值得怀疑。因此,在选择解决方案之前计时非常重要!

这太棒了!你向我介绍了很多全新的东西。非常感谢! - Marc2001

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接