Summing every element in a byte array

Viewed 1660

Now, I'm new to threading and async / sync programming and all that stuff. So, I've been practicing and saw this problem on youtube. The problem was to sum every content of a byte array. It was from the channel called Jamie King. He did this with threads. I've decided to do this with task. I made it asynchronous and it was slower than the synchronous one. The difference between the two was 360 milliseconds! I wonder if any of you could do it faster in an asynchronous way. If so, please post it! Here's mine:

    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;
    }

Note that bytes.SubArray is an extension method. I have one question. Is asynchronous programming slower than synchronous programming? Please point out my mistakes.

Thanks for your time!

3 Answers

You need to use WhenAll() and return all of the tasks at the end:

    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());

    }

When you use the WhenAll() method, you combine all of the Task results, thus the tasks would run in parallel, saving you a lot of necessary time.

You can read more about it in docs.microsoft.com.

asynchronous is not explicitly slower - but runs in the background (Such as waits for connection to a website to be established) - so that the main thread is not stopped for the time it waits for something to happen.

The fastest way to do this is probably going to be to hand-roll a Parallel.ForEach() loop.

Plinq may not even give you a speedup in comparison to a single-threaded approach, and it certainly won't be as fast as Parallel.ForEach().

Here's some sample timing code. When you try this, make sure it's a RELEASE build and that you don't run it under the debugger (which will turn off the JIT optimiser, even if it's a RELEASE build):

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}");
        }
    }
}

The results I get with an x64 build on my octo-core PC are:

  • Single-threaded Linq took 00:00:03.1160235 to calculate sum as 63748717461
  • Single-threaded took 00:00:00.7596687 to calculate sum as 63748717461
  • Plinq took 00:00:01.0305913 to calculate sum as 63748717461
  • Parallel.ForEach with partioner took 00:00:00.0839141 to calculate sum as 63748717461

The results I get with an x86 build are:

  • Single-threaded Linq took 00:00:02.6964067 to calculate sum as 63748717461
  • Single-threaded took 00:00:00.8200462 to calculate sum as 63748717461
  • Plinq took 00:00:01.1251899 to calculate sum as 63748717461
  • Parallel.ForEach with partioner took 00:00:00.1084805 to calculate sum as 63748717461

As you can see, the Parallel.ForEach() with the x64 build is fastest (probably because it's calculating a long total, rather than because of the larger address space).

The Plinq is around three times faster than the Linq non-threaded solution.

The Parallel.ForEach() with a partitioner is more than 30 times faster.

But notably, the non-linq single-threaded code is faster than the Plinq code. In this case, using Plinq is pointless; it makes things slower!

This tells us that the speedup isn't just from multithreading - it's also related to the overhead of Linq and Plinq in comparison to hand-rolling the loop.

Generally speaking, you should only use Plinq when the processing of each element take a relatively long time (and adding a byte to a running total take a very short time).

The advantage of Plinq over Parallel.ForEach() with a partitioner is that it is much simpler to write - however, if it winds up being slower than a simple foreach loop then its utility is questionable. So timing things before choosing a solution is very important!

Related