2015-08-11 18 views
6

CPU zamanının yığınının (>% 95) bir kısmının N ~ 200 siparişi için kötü (ve kaçınılmaz) O (N^2) etkileşiminin hesaplanması için harcanan bazı bilimsel C++ kodlarını paralel hale getirmeye (OpenMP) çalışıyorum farklı parçacıklar. Bu hesaplama 1e10 zaman adımı için tekrarlanır. OpenMP ile çeşitli ek yapılandırmalar denedim, her biri ek çekirdekler eklendikçe zayıf ölçekleme ile bazı kenar boşlukları (en azından büyüklük derecesi) ile seri koddan daha yavaş.OpenMP kodu, seri bellek veya iş parçacığı başındaki darboğazdan daha mı yavaş?

Aşağıda, ilgili kodun temsili bir veri hiyerarşisi Tree->Branch->Leaf ile ilgili bir çizimi görülmektedir. Her bir Leaf nesnesi, diğer şeylerin yanı sıra mevcut ve önceki üç zaman adımı için kendi konumunu ve hızlarını depolar. Her Branch, Leaf nesnesinin bir koleksiyonunu saklar ve her Tree, Branch nesnesinin bir koleksiyonunu depolar. Bu veri yapısı, her zaman adımında (aylarca mükemmel hale getirilmiş) gerçekleştirilmesi gereken karmaşık ancak daha az CPU-yoğun hesaplamalar için çok iyi çalışır.

#include <omp.h> 

#pragma omp parallel num_threads(16) // also tried 2, 4 etc - little difference - hoping that placing this line here spawns the thread pool at the onset rather than at every step 
{ 
while(i < t){ 
    #pragma omp master 
    { 
     /* do other calculations on single core, output etc. */ 
     Tree.PreProcessing() 
     /* PreProcessing can drastically change data for certain conditions, but only at 3 or 4 of the 1e10 time steps */ 
     Tree.Output() 
    } 
    #pragma omp barrier 
    #pragma omp for schedule(static) nowait 
    for(int k=0; k < size; k++){ 
     /* do O(N^2) calc that requires position of all other leaves */ 
     Tree.CalculateInteraction(Branch[k]) 
    } 
    /* return to single core to finish time step */ 
    #pragma omp master 
    { 
     /* iterate forwards */ 
     Tree.PropagatePositions() 
     i++ 
    } 
    #pragma omp barrier 
} 

Çok kısaca, CPU-domuz fonksiyonu yapar:

void Tree::CalculateInteraction(Leaf* A){ 
// for all branches B in tree{ 
     // for all leaves Q in B{ 
      if(condition between A and Q){skip} 
      else{ 
       // find displacement D of A and Q 
       // find displacement L of A and "A-1" 
       // take cross product of the two displacements 
       // add the cross-product to the velocity of leaf A 
       for(int j(0); j!=3; j++){ 
        A->Vel[j] += constant * (D_cross_L)[j]; 
       } 

Sorum performansının bu sakat OpenMP iplik yönetimi havai egemen nedeniyle, ya da ister bir durum olup olmadığıdır Paralellik düşünülmeden tasarlanmış bir veri hiyerarşisi mi?

Her adımın, seriye göre paralel olarak önemli ölçüde daha uzun olması için zamanlandığına dikkat etmeliyim; bu, bazı başlangıç ​​ek yükü sorunu değildir; iki versiyon, yaklaşık 10 saat süren ve en sonunda 30 saat sürebilen seri hesaplara (genellikle 2 kat hızlanmanın çok faydalı olacağı) hesaplamalar için test edilmiştir. Ayrıca, -fopenmp -march=native -m64 -mfpmath=sse -Ofast -funroll-loops ile g ++ 5.2.0 kullanıyorum bilmek değer olabilir.

OpenMP'ye yeni geldim, bu yüzden herhangi bir ipucu büyük bir memnuniyetle karşılanacaktır, lütfen açıklığa kavuşturulması gereken bir şey varsa lütfen bana bildirin.

+0

Ağaç yapısı nasıl tam olarak düzenlenir? bitişik bir dizideki veya bağlantılı bir listede bulunan alt düğümler (diğer bir deyişle ana düğümler yalnızca ilk alt düğümün ve her alt düğümün kız kardeşine göstericidir)? Paralel octtrees (N-body simülasyonları ve SPH için) ile çok çalışıyorum, ama her zaman mükemmel ölçekleme buluyorum. Ayrıca, N (N^2) için neye ihtiyacınız olduğunu merak ediyorum - bu genellikle O (NlnN) veya O (N) içinde ağaç veya FMM ile yaklaşık olarak tahmin edilir. – Walter

+0

@Walter Yapraklar, şube boyunca önceki ve sonraki yapraklara iki kez bağlı bir listede saklanır (bellekte 'herhangi bir yer' bulunabilir). Bir ağaç, bu özel tür hesaplamalar için standart bir seçenektir (süper akışkan helyumun vorteks filaman modeli, bakınız örneğin [bu gözden geçirme] (http://arxiv.org/abs/1305.2753)) fakat bu proje üzerindeki zaman kısıtlamaları nedeniyle Tek bir uzun hesaplamalar kümesini çalıştırmak için daha hızlı bir düzeltme arıyorum. – Matthew

+0

@Walter Sanırım bitişik diziler kullanıyorsunuz? Ama tam olarak kaç tane altuzun olduğunu biliyorsun. Bir listeyi bilmiyorsanız ve kullanmanız gerekiyorsa, bellek erişimi gerçekten paralel ölçeklemeyi bozabilir. Bir iş parçacığı ayırıcısının yardımcı olacağını merak ediyorum. – emsr

cevap

2

Orijinal kaynağa bağlantı sağladığınız için teşekkür ederiz! İki platformda bazı istatistikleri derleyebildim ve elde ettim: icpc 15.0 ve g ++ 4.9.0 ile Xeon E5-2670; ve Core i7-4770, g ++ 4.8.4 ile.

Xeon'da, her iki icpc ve g ++, iş parçacığı sayısıyla ölçeklenen kod üretti. Ben g ++ 4.8.4 ile, gözlenen çirkin ölçekleme davranışını görmek, çekirdek i7 üzerinde

Xeon E5-2670/icpc 15.0 
threads time ipc 
--------------------- 
1   17.5 2.17 
2   13.0 1.53 
4   6.81 1.53 
8   3.81 1.52 

Xeon E5-2670/g++ 4.9.0 
threads time ipc 
--------------------- 
1   13.2 1.75 
2   9.38 1.28 
4   5.09 1.27 
8   3.07 1.25 

: I dağılımının içinde run.in dosyası elde kısaltılmış (3e-7 saniye) simülasyon ran:

Core i7-4770/g++ 4.8.4 
threads time ipc 
--------------------- 
1   8.48 2.41 
2   11.5 0.97 
4   12.6 0.73 

İlk gözlem, ölçeklemeye etki eden platforma özgü bir şey olduğu yönündedir.

Ben point.h ve velnl.cpp dosyalarında bir göz vardı ve birçok geçicilere dahil 3 boyutlu vektör verileri depolamak için vector<double> değişkenleri kullanarak fark ettim. Bunların hepsi yığına erişecek ve potansiyel bir çekişme kaynağı olacaktır. Intel'in open-off uygulaması, yığın çekişmesini önlemek için thread-local yığınları kullanıyor ve g ++ 4.9.4 de değil, belki de g ++ 4.9 da var mı?

Bu 3 boyutlu vektörler için projeyi (halfflat/vfmcppar github üzerinde) işaretledim ve bu dosyaları std::array<double,3> kullanmak üzere değiştirdim; Bu ölçeklendirme geri yükler ve aynı zamanda çok daha hızlı koşmak süreleri vermiştir:

Core i7-4770/g++ 4.8.4 
std::array implementation 
threads time ipc 
--------------------- 
1   1.40 1.54 
2   0.84 1.35 
4   0.60 1.11 

Ben iyi uzunlukta simülasyon bu testler değil, bu yüzden bazı ölçekleme de i/o havai kurmak için kaçırılan ve mümkündür.

Paketleme noktası, paylaşılan herhangi bir kaynağın yığın da dahil olmak üzere ölçeklenebilirliği engelleyebileceği yönündedir.

+0

Vay, bu harika, tüm bu çabaya gittiğiniz için teşekkür ederim. Önceki seferlerim 4670K ve 4770 idi, ancak g ++ 5.1.0 ile. Sanırım hala bir yerde ipc için bir lisansım var ... Hafta boyunca uzaktayım ama değişikliklerinizi test etmeyi dört gözle bekliyorum. – Matthew

1

Performans ölçme araçları (Linux perf gibi), önbellek performansı veya çekişme hakkında bazı bilgiler verebilir; Optimizasyonda ilk adım ölçümdür!

Bu benim tahminim, bu, hız güncellemesinin uygulanması ile birlikte bir veri düzeni problemidir: herhangi bir zamanda her bir iş parçacığı, (aslında) rastgele bir yaprak ile ilişkili verileri yüklemeye çalışmaktadır. önbellek thrashing için tarif. Bir yaprak ile ilgili veriler ne kadar büyük ve hafızada bitişik olarak düzenlenmişler? Eğer bu bir önbellek meselesidir (ölçüm yapın!). Eğer N^2 probleminin çözülmesiyle çözülebilir: - diğer tüm yaprakların katkısı olan hız delta biriktirmek yerine, yığınlar halinde biriktirilebilirler. N yapraklarını bu hesaplamanın amacı için K partiküllerine ayırmayı göz önünde bulundurun, burada her yaprak verisi yığınının yarısına (diyelim) uyuyor. Daha sonra, grupların K^2 çiftini (A, B) tekrarlayarak, etkileşim adımlarını gerçekleştirin, yani, B kümesindeki tüm yaprakların A yığınındaki yapraklara katkısını hesaplayın, ki bu paralel olarak yapılmalıdır. Önbellek bozmadan A'daki yapraklar üzerinde. Yaprakların bellek olarak kesikli bir şekilde düzenlendiğinden emin olmak için daha fazla kazanç elde edilebilir.

+0

Teşekkürler, aslında gprof'un kısa bir süre gelmesi ve intel'ın vtune'sinin çekirdeğimi desteklememesi nedeniyle uygun bir profesör istedim. Yapraklar, ön işlem sırasında yürürlüğe giren değişiklikler nedeniyle bellekte zorunlu değildir. İlk öneriyi denediğime emin olacağım ve belki de bellekte daha kullanışlı bir düzenleme sağlamak için ön işlemeyi değiştirmeyi düşüneceğim. – Matthew

+0

Her bir yaprak için hızı bir kerede bir kez güncellemek için sorunu çözdükten sonra, dört çekirdek üzerinde çalışan önbellek, "perf" tarafından bildirilen önbelleklerin, tüm önbellek referanslarının ~% 0.164'ine, döngü başına sıkılmış 1.49 yönergeleriyle karşılık gelir. Seri kodun önbellek referanslarının yaklaşık% 48'i eksiktir (ancak 400 kez daha az önbellek referansı ile) ve döngü başına 1.83 komutunu kronize ederek hesaplamayı dörtte bir oranında tamamlar. – Matthew

+0

Geri bildirim için teşekkürler! Uyguladığınız döşemenin tarifine göre biraz şaşırdım: Diğer yaprakların üzerinde birikip biriktirmeden, yaprak başına sadece bir güncelleme yapmayı nasıl ayarlıyorsunuz? Bununla birlikte, genel performans cephesinde, seri ve paralel versiyonlar arasında herhangi bir diğer büyük tutarsızlık tespit edildi mi? – halfflat

2

Sorunun nedeni bağlantılı listeler düğümlerin kullanımınız için büyük olasılıkla sahte paylaşım olduğunu.Bu bellek düzeniyle, ağacın başka bir düğüme (halfflat tarafından da belirtildiği gibi) her yürüdüğünüzde yalnızca önbellek kayıpları sorunu yaşamayacaksınız.

Daha ciddi bir sorun, farklı parçacıklardan erişilen ve değiştirilen ağaç düğümlerinin belleğe gerçekten yakın olması olabilir. Bir önbellek satırı paylaşıyorlarsa, bu yanlış paylaşım (veya önbellek ping-pongu) farklı iş parçacıkları arasında paylaşılan önbellek satırlarının tekrar tekrar senkronize edilmesine neden olur.

Her iki soruna da çözüm, bağlantılı veri yapılarından kaçınmaktır. Neredeyse her zaman düşük verimliliğin sebebidir. Sizin durumunuzda, çözüm öncelikle, en az veri içeren (yalnızca ağacı tanımlamak için gerekli olan) bağlantılı bir liste ağacı oluşturmak ve daha sonra bağlantılı listeler kullanmayan ve daha fazla veri içerebilen başka bir ağaca eşlemek içindir. Yaptığım şey budur ve ağaç geçişi oldukça hızlıdır (ağaç yürüyüşü gerçekten hızlı olamaz, çünkü ana-kıza erişim aynı anda bitişik olamayacağından önbellek kayıpları bitişik kardeş düğümlerle bile kaçınılmazdır). Yeni ağaç ağacına parçacıkları eski ağaç sırasına eklerseniz ağaç yapısında önemli bir artış (faktör> 2) elde edilebilir (bu önbellek kayıplarını önler).

+0

Bu çok yararlı, teşekkürler. Halfflat'ın cevabı hakkındaki yorumda belirtildiği gibi, paralel durumda ve seri kodda nispeten az sayıda önbellek hatası görüyorum. – Matthew

+0

Açıklamak gerekirse, yalnızca hesaplamada gerekli olan konumları içeren bir ağacın her zaman adımında oluşturulduğunu/güncellendiğini ve daha sonra etkileşim işlevi tarafından kullanılacağını mı öneriyorsunuz? Her bir yaprak için hesaplamanın kendi ve komşu komşularının pozisyonlarını atlaması gerektiği problemi vardır, bu yüzden muhtemelen yanlış paylaşım sorunu ortaya çıkmaktadır. Sahte paylaşım hakkında biraz daha okuyacağım şimdi bunun için bir ismim var, yardım için teşekkürler. – Matthew

+0

Yani topoloji ağacının a) işaretçisi üst virgülle birlikte b) çocukların bağlantılı listesinin ilk düğümüne işaretçi c) veri ağacındaki düğümlere bağırsaklarla gösterici? S1: Verileri size veri ağacına itmenin bazen işaretçilerin yeniden oluşturulmasını ve geçersiz kılınmasını zorlayabileceğinden endişe ediyor musunuz? S2: Buna bir çözüm bulursanız, veri ağacının neden bir ağaç olması gerekir? Bu bir vektör olabilir mi? – emsr

1

Performanstan bağımsız olabilir, ancak yazılan kod artık garip bir paralelleştirme yapısına sahiptir.

I (, omp for nowait da bariyer bulunmamaktadır omp master bariyer sahip değildir) parallel içinde while döngü engelleri sahip olmadığı için bu, doğru sonuçları üretebilir şüphe.Ana dişli Tree.PreProcessing() tamamlanmadan önce

Sonuç olarak

, (1) iplikleri Bazı ipliklerin aslında omp for tek ön işlem adımında ana işleri kez önce herhangi bir sayıda gerçekleştirebilir, omp for döngü başlayabilir; (2) master, omp for'u diğer iş parçacıklarını bitirmeden önce Tree.PropagatePositions() çalıştırabilir; (3) farklı iş parçacığı farklı zaman adımları çalıştırabilir; (4) teorik olarak, ana iplik, hatta bir iplik bile paralel bölgeye girmeden önce while ilminin tüm aşamalarını tamamlayabilir ve böylece omp for ilmek döngüsünün bazı yinelemeleri hiç bir zaman gerçekleştirilemez.

Yoksa bir şey mi özlüyorum?

+0

Haklı olabilirsiniz, ana bloğun sonunda zımni bir bariyer olduğunu, ancak belgelere daha yakından bakmanın eklenmesi gerektiğini düşündük. Bunu söyledikten sonra, belki de bu derleyici doğrudur, çünkü bu kod doğru sonuçları üretir (ve önerileriniz gerçekleşirse çok büyük ölçüde yanlış olurlar). Test etmeye değecek bir şey varsa, bu konuya karışan herkes için karışıklığı ortadan kaldırmak için sorumu düzenlerim. – Matthew

+1

İç döngüde basitçe 'omp parallel for' yerine 'omp parallel' fonksiyonuna sahip olmaktan ziyade, kazanılacak çok az şey var. (Github'da çatalda kullandığım şey budur.) – halfflat

İlgili konular