讓我們考慮一個簡單的編程挑戰:對大數組中的所有元素求和。現在可以通過使用並行性來輕鬆優化這一點,特別是對於具有數千或數百萬個元素的巨大陣列,還有理由認為,並行處理時間應該與常規時間除以CPU核心數一樣多。事實證明,這一壯舉並不容易實現。我將向您展示幾種並行執行此操作的方法,它們如何改善或降低性能以及 ...
讓我們考慮一個簡單的編程挑戰:對大數組中的所有元素求和。現在可以通過使用並行性來輕鬆優化這一點,特別是對於具有數千或數百萬個元素的巨大陣列,還有理由認為,並行處理時間應該與常規時間除以CPU核心數一樣多。事實證明,這一壯舉並不容易實現。我將向您展示幾種並行執行此操作的方法,它們如何改善或降低性能以及以某種方式影響性能的所有細節。
簡單的迴圈方法
private const int ITEMS = 500000; private int[] arr = null; public ArrayC() { arr = new int[ITEMS]; var rnd = new Random(); for (int i = 0; i < ITEMS; i++) { arr[i] = rnd.Next(1000); } } public long ForLocalArr() { long total = 0; for (int i = 0; i < ITEMS; i++) { total += int.Parse(arr[i].ToString()); } return total; } public long ForeachLocalArr() { long total = 0; foreach (var item in arr) { total += int.Parse(item.ToString()); } return total; }
只需要迭代迴圈就可以計算出結果,超級簡單,這裡沒有用直接相加求出結果,原因是直接求出結果,發現每次基本的運行都比並行快,但是實際上,並行處理沒有那麼簡單,所以這裡的加法就簡單的處理下total += int.Parse(arr[i].ToString())。現在,讓我們嘗試用並行性來打敗數組迭代吧。
首次嘗試
private object _lock = new object(); public long ThreadPoolWithLock() { long total = 0; int threads = 8; var partSize = ITEMS / threads; Task[] tasks = new Task[threads]; for (int iThread = 0; iThread < threads; iThread++) { var localThread = iThread; tasks[localThread] = Task.Run(() => { for (int j = localThread * partSize; j < (localThread + 1) * partSize; j++) { lock (_lock) { total += arr[j]; } } }); } Task.WaitAll(tasks); return total; }
請註意,您必須使用localThread變數來“保存”該iThread時間點的值。否則,它將是一個隨著for迴圈前進而變化的捕獲變數。當數據最後打的時候並行已經比普通的快了,但是發現快的不多,說明還可以優化
再次優化
public long ThreadPoolWithLock2() { long total = 0; int threads = 8; var partSize = ITEMS / threads; Task[] tasks = new Task[threads]; for (int iThread = 0; iThread < threads; iThread++) { var localThread = iThread; tasks[localThread] = Task.Run(() => { long temp = 0; for (int j = localThread * partSize; j < (localThread + 1) * partSize; j++) { temp += int.Parse(arr[j].ToString()); } lock (_lock) { total += temp; } }); } Task.WaitAll(tasks); return total; }
增加設置臨時變數,減少lock次數,發現運行效果已經有質的提高,提高了幾倍。忽然想起,有個Parallel.For的方法,研究性能是否可以更快。
Parallel.For優化
public long ParallelForWithLock() { long total = 0; int parts = 8; int partSize = ITEMS / parts; var parallel = Parallel.For(0, parts, new ParallelOptions(), (iter) => { long temp = 0; for (int j = iter * partSize; j < (iter + 1) * partSize; j++) { temp += int.Parse(arr[j].ToString()); } lock (_lock) { total += temp; } }); return total; }
運行結果比普通迭代快,但是沒有ThreadPool快,但是覺得Parallel.For還可以繼續優化,也許可以更快
Parallel.For繼續優化
public long ParallelForWithLock2() { long total = 0; int parts = 8; int partSize = ITEMS / parts; var parallel = Parallel.For(0, parts, localInit: () => 0L, // Initializes the "localTotal" body: (iter, state, localTotal) => { for (int j = iter * partSize; j < (iter + 1) * partSize; j++) { localTotal += int.Parse(arr[j].ToString()); } return localTotal; }, localFinally: (localTotal) => { total += localTotal; }); return total; }
運行效果已經很快,和ThreadPool優化過的差不多,有些時候更快
結論和總結
並行化優化肯定可以提高性能,但是這取決於很多因素,每個案例都應該進行測量和檢查。
當各種線程需要通過某種鎖定機制相互依賴時,性能會顯著降低。