Bagaimana cara menjalankan perhitungan berat secara Paralel dengan benar menggunakan C#?

Hasil

Tujuannya adalah untuk menghitung semua kemungkinan bentuk poliform dari sejumlah kotak tertentu. Karena ini adalah perhitungan yang sangat berat untuk jumlah yang lebih besar, saya ingin menggunakan banyak inti yang dimiliki komputer saya.

Masalah

Saya membuat masalah lebih mudah untuk dijelaskan dan diuji dengan membuat skenario berikut:

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

Dalam program terakhir saya, langkah 2 jauh lebih luas dan berat secara komputasi, oleh karena itu saya lebih memilih untuk membagi tugas dua dalam banyak nilai yang ingin saya periksa berdasarkan nilai pada langkah 1.

Apa yang Saya Coba

Saya membuat aplikasi winforms dengan C# Core dengan 5 tombol mencoba berbagai variasi paralelisme yang saya temukan di sini di Stackoverflow dan tempat lain di internet: masukkan deskripsi gambar di sini

Berikut kodenya (kelihatannya banyak, tapi hanya 5 variasi dari ide yang sama), semuanya memberikan hitungan untuk memeriksa apakah menghasilkan hasil yang sama + berapa lama waktu yang dibutuhkan:

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

Hasil Sejauh Ini

Sejauh ini semua metode di atas menghasilkan durasi yang sama antara 1s - 1,5s. Sebenarnya, terkadang eksekusi serial normal tampak jauh lebih cepat. Bagaimana ini mungkin? Saya berharap dengan 8 core (16 core virtual) pembagian tugas akan menghasilkan kecepatan keseluruhan yang lebih cepat?

Bantuan apa pun sangat dihargai!

Masa depan

Setelah mempelajari lebih lanjut tentang cara menerapkan paralelisme dengan benar, saya berharap juga menjalankan keseluruhan perhitungan di thread lain/Async agar GUI tetap responsif.

Sunting:

Tanggapan ke @Pac0: Ini implementasi saya atas saran Anda. Tampaknya tidak ada banyak perbedaan: masukkan deskripsi gambar di sini

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 sumber
comment
Bisakah Anda mencoba metode yang sama untuk Repeat = 100000000 dan melihat hasilnya?   -  person Sowmyadhar Gourishetty    schedule 20.08.2020
comment
Dugaan saya adalah karena Anda selalu menggunakan pendekatan yang sama dengan tas bersamaan untuk menyimpan hasil dengan cepat, Anda menyebabkan banyak penguncian, sehingga kehilangan manfaat menggunakan banyak utas. Tapi saya mungkin salah karena saya tidak menggunakan concurrentbag untuk penyimpanan besar. Saya akan lebih memilih pendekatan Peta/Pengurangan : melakukan perhitungan untuk 2, 3, 5, dan 7 secara mandiri, lalu menggabungkan semua hasilnya.   -  person Pac0    schedule 20.08.2020
comment
jadi pada dasarnya, buat HashSet per perhitungan paralel, lalu setelah semuanya selesai, lakukan penyatuan hashset (yang akan menangani duplikat).   -  person Pac0    schedule 20.08.2020
comment
Tugas/async umumnya hanya untuk kode terikat I/O, sedangkan kode Anda tidak.   -  person Neil    schedule 20.08.2020
comment
@SowmyadharGourishetty, Berikut hasil 100.000.000x: i.imgur.com/luahcSv.png seperti yang Anda lihat ada beberapa perbedaan dan metode 2 tampaknya hanya 20% lebih cepat meskipun ada 4 thread terpisah.   -  person Lemth    schedule 20.08.2020
comment
@Neil ini juga cocok untuk pekerjaan berat yang terikat CPU, ketika Anda memiliki thread utama (seperti GUI) yang tidak ingin Anda bekukan.   -  person Pac0    schedule 20.08.2020
comment
@Pac0 Saya mengedit pertanyaan utama saya untuk menunjukkan implementasi saran Anda.   -  person Lemth    schedule 20.08.2020
comment
Tampaknya Anda beroperasi dengan asumsi bahwa leher botol adalah putaran for: for (int i = 0; i < Repeat; i++). Bagaimana Anda memastikan bahwa masalahnya ada, dan bukan pada kode yang menghapus duplikat?   -  person Joshua Robinson    schedule 20.08.2020
comment
@JoshuaRobinson, Anda benar. Dalam contoh ini hambatan terbesar adalah penghapusan duplikat. Namun ini berbeda dalam implementasi sebenarnya dari kode saya. Namun saya akan menerima saran untuk membuat penghapusan duplikat lebih efisien.   -  person Lemth    schedule 20.08.2020


Jawaban (2)


Seperti yang disebutkan dalam komentar, penggunaan Anda atas satu koleksi menyebabkan penguncian yang signifikan. Secara komputasi, solusi berbasis tugas sekitar 50% lebih cepat (lihat di bawah di mana kami tidak mengelola keluaran gabungan). Ini mengelola koleksi yang menyebabkan beberapa pengikatan. Bergantung pada cara penanganannya, eksekusi bisa 3 kali lebih lambat dibandingkan eksekusi serial.

Perjuangan dengan konkurensi selalu menyeimbangkan beban dengan hambatan.

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
Saya harus menambahkan, saya mencoba menggunakan umpan balik berbasis IProgress untuk menambahkan item ke hashset secara langsung, tapi itu yang terburuk dari semuanya. Mengulangi hasil dan UnionWith pada hashset tampaknya menjadi yang terbaik, tetapi masih lambat. - person yugami; 20.08.2020

Saya ingin memposting ini sebagai awnser karena seseorang bernama Yugami memposting sesuatu yang berbeda dari yang saya coba dan responnya bermanfaat dan bagus, tetapi telah dihapus.

Jadi saya memposting upaya saya untuk membuat ulang kode mereka di bangku pengujian saya:

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

Berikut adalah hasil serial dan dua solusi terbaik dengan 100.000.000 percobaan. Di sini saya akhirnya melihat beberapa perbaikan dalam melakukan langkah 2 secara paralel, tetapi sekarang hambatan terbesar adalah menghapus duplikat/memfilter semuanya menjadi satu HashSet... masukkan deskripsi gambar di sini

Jadi menurut saya ini menyelesaikan pertanyaan awal yang harus saya tingkatkan pada langkah 2. Sekarang saya akan melanjutkan pencarian saya untuk meningkatkan pada Langkah 3; menghapus duplikat.

person Lemth    schedule 20.08.2020
comment
Maaf solusi saya tidak lengkap, tetapi baru menyadarinya setelah kejadian. Saya sudah membersihkannya tetapi masih ada masalah tergantung bagaimana Anda ingin menangani datanya. - person yugami; 20.08.2020
comment
Sekadar catatan, dalam metode button9_Calculation Anda tahu bahwa output akan berisi Repeat item, jadi Anda bisa membuatnya dengan List<int> output = new List<int>(Repeat). Seharusnya kinerjanya sedikit lebih baik dengan menghilangkan kebutuhan untuk mengubah ukuran daftar selama eksekusi metode. - person Joshua Robinson; 20.08.2020