จะทำการคำนวณจำนวนมากในแบบขนานโดยใช้ C # ได้อย่างไร

เป้าหมาย

เป้าหมายคือการคำนวณรูปร่างโพลีฟอร์มที่เป็นไปได้ทั้งหมดของจำนวนกำลังสองที่กำหนด เนื่องจากนี่เป็นการคำนวณที่หนักมากสำหรับจำนวนที่มากขึ้น ฉันจึงต้องการใช้หลายคอร์ที่คอมพิวเตอร์ของฉันมี

ปัญหา

ฉันทำให้ปัญหาง่ายต่อการอธิบายและทดสอบโดยการสร้างสถานการณ์ต่อไปนี้:

1) for each value of 2, 3, 5, and 7:
2) find all multiples (up to a certain value) and add them to the same List
3) remove all duplicates from said list

ในโปรแกรมสุดท้ายของฉันขั้นตอนที่ 2 นั้นกว้างใหญ่และหนักกว่ามาก ดังนั้นฉันจึงต้องการแบ่งงานที่สองออกเป็นค่าต่างๆ มากมายเท่าใดก็ได้ที่ฉันต้องการตรวจสอบตามค่าของขั้นตอนที่ 1

สิ่งที่ฉันพยายาม

ฉันสร้างแอป winforms ด้วย C# Core พร้อมปุ่ม 5 ปุ่ม ลองใช้ความคล้ายคลึงกันในรูปแบบต่างๆ ที่ฉันพบที่นี่บน Stackoverflow และที่อื่นๆ บนอินเทอร์เน็ต: ป้อนคำอธิบายรูปภาพที่นี่

นี่คือโค้ด (ซึ่งดูเหมือนมาก แต่เป็นเพียงแนวคิดเดียวกัน 5 รูปแบบ) พวกเขาทั้งหมดให้การนับเพื่อตรวจสอบว่าได้ผลลัพธ์เดียวกันหรือไม่ + ใช้เวลานานแค่ไหน:

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Security.Permissions;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Parallelism
{
    public partial class Form1 : Form
    {
        private readonly int Repeat = 10000000; 

        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            var watch = System.Diagnostics.Stopwatch.StartNew();
            List<int> output = new List<int>();
            foreach (int x in new int[] { 2, 3, 5, 7 })
            {
                for (int i = 0; i < Repeat; i++)
                {
                    output.Add(x * i);
                }
            }
            output = output.Distinct().ToList();
            watch.Stop();
            (sender as Button).Text += $", c:{output.Count} - {watch.ElapsedMilliseconds}ms";
        }

        private void button2_Click(object sender, EventArgs e)
        {
            var watch = System.Diagnostics.Stopwatch.StartNew();
            ConcurrentBag<int> output = new ConcurrentBag<int>();
            Task task = Task.WhenAll(
              Task.Run(() => button2_Calculation(2, output)),
              Task.Run(() => button2_Calculation(3, output)),
              Task.Run(() => button2_Calculation(5, output)),
              Task.Run(() => button2_Calculation(7, output))
            );
            task.Wait();
            HashSet<int> output2 = new HashSet<int>(output);
            watch.Stop();
            (sender as Button).Text += $", c:{output2.Count} - {watch.ElapsedMilliseconds}ms";
        }
        private void button2_Calculation(int x, ConcurrentBag<int> output)
        {
            for (int i = 0; i < Repeat; i++)
            {
                output.Add(x * i);
            }
        }

        private void button3_Click(object sender, EventArgs e)
        {
            var watch = System.Diagnostics.Stopwatch.StartNew();
            List<int> output = new List<int>();
            foreach (int x in (new int[] { 2, 3, 5, 7 }).AsParallel())
            {
                for (int i = 0; i < Repeat; i++)
                {
                    output.Add(x * i);
                }
            }
            output = output.Distinct().ToList();
            watch.Stop();
            (sender as Button).Text += $", c:{output.Count} - {watch.ElapsedMilliseconds}ms";
        }

        private void button4_Click(object sender, EventArgs e)
        {
            var watch = System.Diagnostics.Stopwatch.StartNew();
            ConcurrentBag<int> output = new ConcurrentBag<int>();
            Dictionary<int, Task> runningTasks = new Dictionary<int, Task>();
            foreach (int x in new int[] { 2, 3, 5, 7 })
            {
                int value = x;
                runningTasks.Add(x, Task.Factory.StartNew(() => button2_Calculation(value, output)));
            }
            foreach (Task t in runningTasks.Select(c => c.Value))
                t.Wait();
            HashSet<int> output2 = new HashSet<int>(output);
            watch.Stop();
            (sender as Button).Text += $", c:{output2.Count} - {watch.ElapsedMilliseconds}ms";
        }

        private void button5_Click(object sender, EventArgs e)
        {
            var watch = System.Diagnostics.Stopwatch.StartNew();
            ConcurrentBag<int> output = new ConcurrentBag<int>();
            Parallel.ForEach(new int[] { 2, 3, 5, 7 }, x => button5_Calculation(x, output));
            HashSet<int> output2 = new HashSet<int>(output);
            watch.Stop();
            (sender as Button).Text += $", c:{output2.Count} - {watch.ElapsedMilliseconds}ms";
        }
        private void button5_Calculation(int x, ConcurrentBag<int> output)
        {
            for (int i = 0; i < Repeat; i++)
                output.Add(x * i);
        }
    }
}

ผลลัพธ์จนถึงตอนนี้

จนถึงขณะนี้วิธีการข้างต้นทั้งหมดส่งผลให้มีระยะเวลาใกล้เคียงกันระหว่าง 1 วินาที - 1.5 วินาที จริงๆ แล้ว บางครั้งการประมวลผลแบบอนุกรมปกติดูเหมือนจะเร็วกว่ามาก สิ่งนี้เป็นไปได้อย่างไร? ฉันคาดหวังว่าด้วย 8 คอร์ (16 คอร์เสมือน) ที่การแยกงานจะส่งผลให้ความเร็วโดยรวมเร็วขึ้น

ความช่วยเหลือใด ๆ ที่ชื่นชมอย่างมาก!

อนาคต

หลังจากเรียนรู้เพิ่มเติมเกี่ยวกับวิธีการใช้งานการทำงานแบบขนานอย่างเหมาะสม ฉันคาดหวังว่าจะดำเนินการคำนวณทั้งหมดบนเธรดอื่น / Async เพื่อให้ GUI ยังคงตอบสนอง

แก้ไข:

ตอบกลับ @ Pac0: นี่คือการดำเนินการตามคำแนะนำของคุณ ดูเหมือนจะไม่มีความแตกต่างมากนัก: ป้อนคำอธิบายรูปภาพที่นี่

private void button6_Click(object sender, EventArgs e)
        {
            var watch = System.Diagnostics.Stopwatch.StartNew();
            ConcurrentBag<HashSet<int>> bag = new ConcurrentBag<HashSet<int>>();
            var output = Parallel.ForEach(new int[] { 2, 3, 5, 7 }, x =>
            {
                HashSet<int> temp = new HashSet<int>();
                for (int i = 0; i < Repeat; i++)
                    temp.Add(x * i);
                bag.Add(temp);
            });
            HashSet<int> output2 = new HashSet<int>();
            foreach (var hash in bag)
                output2.UnionWith(hash);
            watch.Stop();
            (sender as Button).Text += $", c:{output2.Count} - {watch.ElapsedMilliseconds}ms";
        }

person Lemth    schedule 20.08.2020    source แหล่งที่มา
comment
คุณสามารถลองใช้วิธีเดียวกันนี้กับ Repeat = 100000000 แล้วดูผลลัพธ์ได้หรือไม่   -  person Sowmyadhar Gourishetty    schedule 20.08.2020
comment
ฉันเดาว่าเนื่องจากคุณมักจะใช้วิธีการเดียวกันกับ concurrentbag เพื่อจัดเก็บผลลัพธ์ได้ทันที คุณทำให้เกิดการล็อคจำนวนมาก ดังนั้นจึงสูญเสียประโยชน์ของการใช้เธรดจำนวนมาก แต่ฉันอาจจะคิดผิดเพราะฉันไม่ได้ใช้กระเป๋าคู่กันสำหรับการจัดเก็บขนาดใหญ่ ฉันจะใช้แนวทาง Map/Reduce ให้มากกว่านี้ โดยคำนวณ 2, 3, 5 และ 7 อย่างอิสระ จากนั้นจึงรวมผลลัพธ์ทั้งหมดเข้าด้วยกัน   -  person Pac0    schedule 20.08.2020
comment
โดยพื้นฐานแล้ว ให้สร้าง HashSet ต่อการคำนวณแบบขนาน จากนั้นเมื่อทั้งหมดเสร็จสิ้น ให้ทำ Union of the hashset (ซึ่งจะดูแลรายการซ้ำ)   -  person Pac0    schedule 20.08.2020
comment
โดยทั่วไป Tasks/async นั้นใช้สำหรับโค้ดที่ผูกกับ I/O เท่านั้น ซึ่งไม่ใช่ของคุณ   -  person Neil    schedule 20.08.2020
comment
@SowmyadharGourishetty นี่คือผลลัพธ์สำหรับ 100.000.000x: i.imgur.com/luahcSv.png อย่างที่คุณเห็นมีความแตกต่างบางประการและวิธีที่ 2 ดูเหมือนว่าจะเร็วขึ้นเพียง 20% แม้ว่าจะมี 4 เธรดแยกกันก็ตาม   -  person Lemth    schedule 20.08.2020
comment
@Neil ยังเหมาะอย่างยิ่งสำหรับงานหนักที่ CPU ผูกไว้เมื่อคุณมีเธรดหลัก (เช่น GUI) ที่คุณไม่ต้องการหยุด   -  person Pac0    schedule 20.08.2020
comment
@ Pac0 ฉันแก้ไขคำถามหลักของฉันเพื่อแสดงการดำเนินการตามคำแนะนำของคุณ   -  person Lemth    schedule 20.08.2020
comment
ดูเหมือนว่าคุณจะดำเนินการภายใต้สมมติฐานที่ว่าคอขวดคือ for ลูป: for (int i = 0; i < Repeat; i++) คุณยืนยันได้อย่างไรว่ามีปัญหาเกิดขึ้น และไม่ใช่กับโค้ดที่จะลบรายการที่ซ้ำกัน   -  person Joshua Robinson    schedule 20.08.2020
comment
@JoshuaRobinson คุณพูดถูก ในตัวอย่างนี้ ปัญหาคอขวดที่ใหญ่ที่สุดคือการลบรายการที่ซ้ำกัน อย่างไรก็ตาม สิ่งนี้แตกต่างในการใช้งานจริงของโค้ดของฉัน แต่ฉันจะรับข้อเสนอแนะในการทำให้การลบซ้ำมีประสิทธิภาพมากขึ้น   -  person Lemth    schedule 20.08.2020


คำตอบ (2)


ตามความคิดเห็นที่กล่าวถึงการใช้คอลเลกชันเดียวของคุณทำให้เกิดการล็อคที่สำคัญ การคำนวณตามโซลูชันตามงานจะเร็วขึ้นประมาณ 50% (ดูด้านล่างในกรณีที่เราไม่ได้จัดการเอาต์พุตแบบรวม) มันเป็นการจัดการคอลเลกชันที่ทำให้เกิดความผูกพัน ขึ้นอยู่กับวิธีการจัดการ อาจช้ากว่าการประมวลผลแบบอนุกรมได้ถึง 3 เท่า

การต่อสู้กับภาวะเห็นพ้องต้องกันมักจะทำให้ภาระกับคอขวดสมดุลอยู่เสมอ

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ConsoleApp5
{
    class Program
    {
        static int Repeat = 100000000;
        static int[] worklist = new int[] { 2, 3, 5, 7 };

        static void Main(string[] args)
        {
            var watch = System.Diagnostics.Stopwatch.StartNew();

            Console.WriteLine("Hello World! Launching Threads");
            Task launcher = Task.Run(()=>LaunchThreads());
            launcher.Wait();
            Console.WriteLine("Hello World! Threads Complete");

            watch.Stop();
            Console.WriteLine($"Threads took: {watch.ElapsedMilliseconds}");

            watch = System.Diagnostics.Stopwatch.StartNew();
            Console.WriteLine("Serial Execution Starting");
            foreach (int i in worklist)
            {
                DoWork(i);
            }
            watch.Stop();
            Console.WriteLine($"Serial Execution took: {watch.ElapsedMilliseconds}");
        }
        static async void LaunchThreads()
        {
            //Dictionary<int, List<int>> mywork = new Dictionary<int, List<int>>();
            HashSet<int> output = new HashSet<int>();

            var worktasks = new List<Task<List<int>>>();

            foreach (int i in worklist)
            {
                worktasks.Add(Task.Run(() => DoWork(i)));
            }

            await Task.WhenAll(worktasks);
        }
        static List<int> DoWork(int x)
        {
            Console.WriteLine($"Thread Worker: {x}");
            List<int> output = new List<int>();
            for (int i = 0; i < Repeat; i++)
            {
                output.Add(x * i);
            }

            Console.WriteLine($"Thread Worker: {x} - Exiting");
            return output;
        }
    }
}
person yugami    schedule 20.08.2020
comment
ฉันควรเพิ่ม ฉันลองใช้ข้อเสนอแนะตาม IProgress เพื่อเพิ่มรายการลงในแฮชเซ็ตโดยตรง แต่นั่นเป็นกลุ่มที่แย่ที่สุด การวนซ้ำผลลัพธ์และ UnionWith บนแฮชเซ็ตดูเหมือนจะดีที่สุด แต่ก็ยังช้าอยู่ - person yugami; 20.08.2020

ฉันต้องการโพสต์สิ่งนี้ในฐานะ awnser เนื่องจากมีคนชื่อ Yugami โพสต์สิ่งที่แตกต่างจากที่ฉันเคยลองมาและเป็นการตอบรับที่เป็นประโยชน์และดีแต่ถูกลบไปแล้ว

ดังนั้นฉันจึงโพสต์ความพยายามของฉันในการสร้างโค้ดขึ้นใหม่บนม้านั่งทดสอบของฉัน:

private async void button9_Click(object sender, EventArgs e)
        {
            var watch = System.Diagnostics.Stopwatch.StartNew();
            HashSet<int> output = new HashSet<int>();
            var worktasks = new List<Task<List<int>>>();
            foreach (int i in new int[] { 2, 3, 5, 7 })
                worktasks.Add(Task.Run(() => button9_Calculation(i)));

            await Task.WhenAll(worktasks);
            foreach (Task<List<int>> tsk in worktasks)
                foreach (int i in tsk.Result)
                    output.Add(i);
            watch.Stop();
            (sender as Button).Text += $", c:{output.Count} - {watch.ElapsedMilliseconds}ms";
        }
        private List<int> button9_Calculation(int x)
        {
            List<int> output = new List<int>();
            for (int i = 0; i < Repeat; i++)
                output.Add(x * i);

            return output;
        }

นี่คือผลลัพธ์ของซีเรียลและสองโซลูชันที่ดีที่สุดที่มีการลอง 100,000,000 ครั้ง ในที่สุดฉันก็เห็นการปรับปรุงบางอย่างในการทำขั้นตอนที่ 2 แบบขนาน แต่ตอนนี้ปัญหาคอขวดที่ใหญ่ที่สุดคือการลบรายการที่ซ้ำกัน / กรองมันทั้งหมดให้เหลือ HashSet เดียว... ป้อนคำอธิบายรูปภาพที่นี่

ดังนั้นฉันคิดว่านี่จะช่วยแก้ปัญหาคำถามเบื้องต้นที่ฉันต้องปรับปรุงขั้นตอนที่ 2 ได้ ตอนนี้ฉันจะค้นหาต่อไปเพื่อปรับปรุงในขั้นตอนที่ 3 การลบรายการที่ซ้ำกัน

person Lemth    schedule 20.08.2020
comment
ขออภัย โซลูชันของฉันไม่สมบูรณ์ แต่สังเกตเห็นหลังจากข้อเท็จจริงเท่านั้น ฉันทำความสะอาดแล้ว แต่ยังมีปัญหาอยู่ ขึ้นอยู่กับว่าคุณต้องการจัดการกับข้อมูลอย่างไร - person yugami; 20.08.2020
comment
โปรดทราบว่าในเมธอด button9_Calculation คุณรู้ว่า output จะมี Repeat รายการ ดังนั้นคุณจึงสร้างมันขึ้นมาด้วย List<int> output = new List<int>(Repeat) ได้ ควรทำงานได้ดีขึ้นเล็กน้อยโดยไม่จำเป็นต้องปรับขนาดรายการระหว่างการดำเนินการตามวิธีการ - person Joshua Robinson; 20.08.2020