快速和改進的二維凸包演算法及其在O(n log h)中的實現(實現部分)
此篇接上一篇部落格http://blog.csdn.net/firstchange/article/details/78588669
- 實施選擇
陣列與列表
“List”類是一個C#集合,它使用一個數組作為其底層容器。使用“列表”而不是陣列應該有類似的效能。測試證實,直接管理陣列的效能提升很小。這個差別太小,很難用一個數組來證明丟失的清晰度。這兩個集合已被用於不同的實現,可以一起比較。
陣列與樹
所有的Ouellet(和Liu和Chen)實現都使用基於陣列的容器,除了Ouellet AVL版本。Ouellet AVL和Ouellet AVL v2使用AVL樹來儲存潛在候選,而不是基於陣列的容器。使用基於陣列的容器意味著手動的二分法管理。而樹本質上是在內部實行二分法。使用二分法得到適當的插入點確保了良好的效能,並且是留在O(n log h)中的主要關鍵。根據我的測試和生活中的一個實際用法,我懷疑在大多數情況下“h”應該保持在1000以下,這對使用陣列或樹木沒有任何影響。但是,如果“h”可能非常大(在我的測試中超過50萬分),那麼擁有一棵樹會更安全。除此以外,陣列的優點是(與樹結構相比):
資料連續在一起,實現更高的CPU快取命中率。 在大多數情況下,只有一次呼叫堆分配器才能獲得足夠大的陣列以容納所有的船體點。這是基於許多使用隨機點生成器作為輸入點源的測試,如在此處的基準測試中所述。實際執行保留1000分,以減少堆分配器請求。對於100萬點,結果幾乎總是保持在800點的赫爾點與所有正常的隨機生成器(而不是“弧”之一)。 可以使用索引,這使得直接訪問點。實際上,直接訪問可以在多執行緒演算法中實現O(n)效能,但是一些測試顯示比實際實現更慢的結果。效能下降似乎是由多執行緒鎖定機制引起的。在O(n)中檢視凸包,獲取更多關於我做的快速測試的資訊,這些資訊沒有像預期的那樣工作。另外,因為在大多數情況下“h”非常低,所以應用在“n”上的多執行緒機制的新增應該是非常低的,以便不會比與“h”有關的時間更重要。O(n)測試不包含在提供的程式碼中。
在樹結構上使用陣列的主要缺點是:
當“h”的數量變大,大於〜500 000(Ouellet C#實現)時,陣列容器解決方案變得昂貴,原因有二:
主要原因是我們每次插入或刪除候選人都要轉移積分。
另外,每次到達保留空間邊界時,我們都必須分配一個新陣列,並將所有資料複製到新陣列。
樹解(在使用AVL樹的程式碼中)更新。我的意思是使用AVL樹而不是陣列是最新的主要實現更改。AVL樹實現似乎是一個很好的選擇,以獲得直接依賴於其輸出大小的演算法,而不是更高。這將確保在所有情況下都能保持更穩定的表現。但在一般用例中不需要,因為在所有測試的情況下,“h”(船體點)比“n”(源點)小很多。C ++實現不使用樹的原因是我最近才意識到陣列的負面影響,當“h”變得太大,我錯過了用C ++編寫另一個實現的時間。
有2種最流行的平衡樹的樹管理演算法:AVL和“紅黑”樹。我選擇AVL在“紅黑”樹上有兩個原因:
懶惰,在我看來,在紅黑樹上實現AVL更容易。
根據我的測試,因為“h”對於“n”來說保持非常小,所以應該比在樹上插入更多的讀取,這應該偏愛AVL樹而不是紅黑樹以獲得最佳效能。
- O(n)中的凸包
我試圖在O(n)而不是O(n log h)中建立一個演算法。我想我可以藉助執行緒,陣列作為容器和良好的設計。至少需要2個執行緒才能達到目標。一個執行緒,快速篩選潛在的候選人,另一個插入潛在的候選人在結果凸包容器,一個數組。
執行緒1 - 每個點在O(1)中篩選候選。那個執行緒拒絕不好的候選人,把好的一個人放進一個堆疊。為了將近似的插入位置放入已經找到的候選凸包點的陣列中,演算法應該在第一點和最後點之間使用線性定位。這不應該給出插入的確切位置(弧線性化),但應該足以快速拒絕大多數點。這樣,就沒有更多的二分法,這就相當於去掉了“log h”。該執行緒在O(n)中完成它的工作。
執行緒2-嘗試插入由執行緒1過濾的潛在凸包點。這在O(〜h ++ log h)中完成。當接近n點和/或接近溶液時,插入應該隨著時間的推移而越來越少。這應該有足夠的時間來清空堆疊的點來嘗試插入。
如果執行緒1總是線上程2之前完成,或者非常接近(恆定時間),那麼我們可以說在O(n)中有一個凸包。
2個執行緒之間的首選容器是堆疊,因為最近評估的點具有更高的風險,成為更好的凸包候選。
事實上,這是工作,但工作的時間是Ouellet單執行緒版本的兩倍。請注意,在大多數情況下,執行緒1在可能的候選堆疊中沒有點數完成,這應該表示O(n)成功,至少在一般情況下是成功的。
此外,該演算法之間交替2個不變陣列副本,以保持連貫性,而不是減緩篩選候選人的執行緒。由於幾個估計的原因,這不是一個真正的成功:
在一般情況下,它比其他任何我的實現慢兩倍,可能是由於訪問共享的過濾候選堆疊所需的鎖定機制。雖然我使用的是“SpinLock”,而不是標準的“Lock”,這應該會更快,但是我覺得阻塞的情況太多了,導致了延遲。我應該找到一個沒有阻礙的方法來插入潛在的候選人。
它高度依賴於一個很好的隨機分佈點。這可能是非常糟糕的,否則因為過濾可能有非常糟糕的丟棄命中,至少我實現我的演算法的方式。如果有可能的話,我將不得不重做一切,以使其更有效率,更少依賴資料。
我確定了每象限的執行緒數量,而不是幫助其他象限完成他們的初始象限/作業完成任務。看到結果後,我決定停止夢想O(n)真實世界的用法。由於常規演算法中每一步的簡單性和速度,新增一個鎖定機制會使事情減慢太多。也許在很多年的時候,千萬分的時候會少點,而執行緒的數量會越來越高,那麼這也許會被認為是有用的。
我認為總之,緩慢問題來自一般在1000以下的小“h”,這是8-10的樹。做10次迭代是如此之快以至於它可以很好地與任何鎖定機制競爭。
作為一個方面說明:由於我缺乏知識,我不確定我是否有權說如果一個演算法需要很多執行緒來實現這個效能,那麼這個演算法是在“O(n)”中的呢?我也必須去尋找那個 所有關於在O(n)中找到赫爾點的東西都必須被證明,而且真的很有趣,但時間和金錢統治世界,而我也是其中的一部分。由於效能不佳,因為我認為我應該改進演算法,我寧願不發表該部分。
多執行緒
根據基準測試結果,很容易看出多執行緒版本是單執行緒版本的兩倍以上。由於Ouellet Hull演算法的性質,它確實使它成為一個很容易實現多執行緒的候選者,至少在3的2個第一步中。2個第一步依賴於“n”而第三個是僅依賴於“h”。3個步驟的,第一很容易被完全多執行緒的,所述第二可容易地在4個完全獨立的執行緒執行(不需要同步機構)和3 次的步驟將是有點難以多執行緒但它不受由“n “只能通過”h“。
儘管多執行緒的使用並不像演算法的複雜性(大O)那麼重要,但它可以幫助提高效能。在Ouellet演算法的情況下,新增多執行緒並保持執行緒完全獨立是非常容易的,這是額外的好處。
- 演算法基準
隨機生成器,基準測試有五種不同型別的點生成器:
硬體用於測試
- “速度測試”的結果
所提出的大部分結果都是針對O(n log h)中的演算法來更好地區分最快者的表現。大多數測試都是用2臺發電機完成的,“圓”和“扔掉”應該更接近實際使用。任何其他的演算法組合,隨機生成器都可以使用提供的程式碼輕鬆測試。如果您願意,也可以新增自己的生成器或演算法。
C / C ++中的實現結果是以本地語言編寫的,而不是C#程式碼,以防止結果轉換時間。這應該更好地瞭解每個演算法/實現所花費的實時性。
直接基於結果點的線性迴歸也被新增以便檢視趨勢以及是否是線性的。
關於“速度測試”結果的結論
O(n log h)的優點是顯而易見的。 單調鏈演算法是評估演算法實現中速度最慢的演算法 儘管MiConvexHull和Heap演算法都在O(n log n)中,但Heap非常慢。 速度取決於源點,它們的位置和順序。速度結果可能因點排列(隨機發生器)而異。 O(n log h)中的所有凸包演算法都比較快,但它們之間存在很好的差異。 Ouellet對劉和陳的優化帶來了更好的表現。 基於陣列的容器實現依賴於非常大的“h”。當“h”達到約50萬分時出現問題。 Chan比Ouellet更獨立於隨機生成器的型別。比較不同隨機生成器的結果時可以看出這種差異。 Ouellet AVL和Ouellet AVL v2比Chan更受“h”的影響。通過比較“電弧發生器”和其他發電機的結果可以看出。這應該是由堆分配器延遲和/或重新平衡樹造成的。 多執行緒是一個真正的優勢。 C ++比C#快很多。雖然ne是完全相同的演算法,但差異比預期的要高很多。我認為這可以歸因於大量的記憶體讀/寫,這可能在C ++中具有較少的間接性,也可能是由於在C#中為了安全而自動發生的陣列邊界檢查,而不是在C ++中。 C#單執行緒vs Chan根據生成器的型別而不同。對於“圓”點生成器,C#比C中的Chan要快。使用“Throw away”生成器時,Chan很容易獲勝。 Ouellet CPP比Chan的速度快4倍,兩者都是用相同的語言編寫的(C / CPP)。那是高於100000的點數。 不可變陣列拷貝受到許多點的影響比使用重疊拷貝時的速度要快很多(正常使用,比如列表)。 儘管Ouellet CPP在正常使用情況下明顯勝出,但是由於基於陣列的用於儲存船體點的“h”變得太大,所以它表現不佳。 當點數變大時,Ouellet AVL和Chan之間的區別正在減小。檢視“深度效能測試”的結果。 對於Ouellet來說,AVL樹似乎是確保在所有情況下都有良好效能的最佳容器。
“深度效能測試”的結果
這個測試主要顯示演算法速度的比較。結果分為兩部分:
第一張表顯示原始速度結果
第二個表顯示相同的資訊,相對於同一行上最快的演算法的比率
“深度效能測試”如何實現
對於每個點數(列:點數)
每次測試10次
建立一個隨機點數(ptsCount)
執行每個演算法計算其所花費的時間
對於每個演算法,根據10次測試做一個平均值(每個演算法都有一個更好的標準化平均值)
這種型別的測試的一個優點是它是在一定數量的點上完成的,但是對於每一個數量,平均值是由10個不同的隨機點組成的,每個演算法對相同的一組點進行測試。在10次測試中計算平均值有助於獲得更可靠的結果,更好地反映實際情況。它降低了演算法計算效能的變化。
還剩下什麼
還剩下很多東西:
用紅黑樹代替Avl樹測試Ouellet Avl並比較效能
在C ++中實現AVL版本以查詢語言優勢,並檢視與Ouellet ST的差異是否相同
在所有版本中新增一些或全部 優化
在C ++中執行多執行緒版本,就像在C#中完成的一樣,使用AVL樹
改進多執行緒版本,以便在每次傳遞中使用所有情況下的所有執行緒。
找到更好的實現選擇,並對O(n)中執行的演算法進行更多測試
通過使其更通用,使演算法適應3D和/或任何維度。這應該是可行的。作為一個想法,我們可以使用位值作為象限。
結合前面的任何一個來獲得最好的演算法實現