Başvurum neden ömrünün% 24'ünü sıfır kontrolü yaparak geçiriyor?


104

Performans açısından kritik bir ikili karar ağacım var ve bu soruyu tek bir kod satırına odaklamak istiyorum. İkili ağaç yineleyicinin kodu, ona karşı performans analizi çalıştırmanın sonuçlarıyla birlikte aşağıdadır.

        public ScTreeNode GetNodeForState(int rootIndex, float[] inputs)
        {
0.2%        ScTreeNode node = RootNodes[rootIndex].TreeNode;

24.6%       while (node.BranchData != null)
            {
0.2%            BranchNodeData b = node.BranchData;
0.5%            node = b.Child2;
12.8%           if (inputs[b.SplitInputIndex] <= b.SplitValue)
0.8%                node = b.Child1;
            }

0.4%        return node;
        }

BranchData bir özellik değil, bir alandır. Bunu satır içi olmama riskini önlemek için yaptım.

BranchNodeData sınıfı aşağıdaki gibidir:

public sealed class BranchNodeData
{
    /// <summary>
    /// The index of the data item in the input array on which we need to split
    /// </summary>
    internal int SplitInputIndex = 0;

    /// <summary>
    /// The value that we should split on
    /// </summary>
    internal float SplitValue = 0;

    /// <summary>
    /// The nodes children
    /// </summary>
    internal ScTreeNode Child1;
    internal ScTreeNode Child2;
}

Gördüğünüz gibi while döngüsü / null kontrolü, performansa büyük bir darbe vuruyor. Ağaç çok büyük, bu yüzden bir yaprak aramanın biraz zaman almasını beklerdim, ancak o satırda harcanan orantısız zamanı anlamak istiyorum.

Denedim:

  • Boş çekini süre ile ayırmak - isabet olan Boşluk çekidir.
  • Nesneye bir boole alanı eklemek ve buna karşı kontrol etmek, hiçbir fark yaratmadı. Neyin karşılaştırıldığı önemli değil, sorun olan karşılaştırma.

Bu bir şube tahmin sorunu mu? Öyleyse bu konuda ne yapabilirim? Eğer birşey?

CIL'i anlıyormuş gibi yapmayacağım , ancak bunu herkesin anlaması için göndereceğim , böylece ondan bazı bilgiler almaya çalışabilirler.

.method public hidebysig
instance class OptimalTreeSearch.ScTreeNode GetNodeForState (
    int32 rootIndex,
    float32[] inputs
) cil managed
{
    // Method begins at RVA 0x2dc8
    // Code size 67 (0x43)
    .maxstack 2
    .locals init (
        [0] class OptimalTreeSearch.ScTreeNode node,
        [1] class OptimalTreeSearch.BranchNodeData b
    )

    IL_0000: ldarg.0
    IL_0001: ldfld class [mscorlib]System.Collections.Generic.List`1<class OptimalTreeSearch.ScRootNode> OptimalTreeSearch.ScSearchTree::RootNodes
    IL_0006: ldarg.1
    IL_0007: callvirt instance !0 class [mscorlib]System.Collections.Generic.List`1<class OptimalTreeSearch.ScRootNode>::get_Item(int32)
    IL_000c: ldfld class OptimalTreeSearch.ScTreeNode OptimalTreeSearch.ScRootNode::TreeNode
    IL_0011: stloc.0
    IL_0012: br.s IL_0039
    // loop start (head: IL_0039)
        IL_0014: ldloc.0
        IL_0015: ldfld class OptimalTreeSearch.BranchNodeData OptimalTreeSearch.ScTreeNode::BranchData
        IL_001a: stloc.1
        IL_001b: ldloc.1
        IL_001c: ldfld class OptimalTreeSearch.ScTreeNode OptimalTreeSearch.BranchNodeData::Child2
        IL_0021: stloc.0
        IL_0022: ldarg.2
        IL_0023: ldloc.1
        IL_0024: ldfld int32 OptimalTreeSearch.BranchNodeData::SplitInputIndex
        IL_0029: ldelem.r4
        IL_002a: ldloc.1
        IL_002b: ldfld float32 OptimalTreeSearch.BranchNodeData::SplitValue
        IL_0030: bgt.un.s IL_0039

        IL_0032: ldloc.1
        IL_0033: ldfld class OptimalTreeSearch.ScTreeNode OptimalTreeSearch.BranchNodeData::Child1
        IL_0038: stloc.0

        IL_0039: ldloc.0
        IL_003a: ldfld class OptimalTreeSearch.BranchNodeData OptimalTreeSearch.ScTreeNode::BranchData
        IL_003f: brtrue.s IL_0014
    // end loop

    IL_0041: ldloc.0
    IL_0042: ret
} // end of method ScSearchTree::GetNodeForState

Düzenleme: Dal tahmin testi yapmaya karar verdim, süre içinde ise bir özdeş ekledim, yani

while (node.BranchData != null)

ve

if (node.BranchData != null)

onun içinde. Daha sonra buna karşı performans analizi çalıştırdım ve her zaman doğru olan ikinci karşılaştırmayı yürütmek için olduğu gibi ilk karşılaştırmayı yürütmek altı kat daha uzun sürdü. Yani bu gerçekten bir dal tahmini sorunu gibi görünüyor - ve bu konuda yapabileceğim hiçbir şey olmadığını tahmin ediyorum ?!

Başka Bir Düzenleme

Yukarıdaki sonuç, node.BranchData'nın while denetimi için RAM'den yüklenmesi gerekmesi durumunda da ortaya çıkar - daha sonra if ifadesi için önbelleğe alınır.


Bu, benzer bir konudaki üçüncü sorum. Bu sefer tek bir kod satırına odaklanıyorum. Bu konudaki diğer sorularım:


3
Lütfen BranchNodemülkün uygulamasını gösterin . Lütfen değiştirmeyi deneyin node.BranchData != null ReferenceEquals(node.BranchData, null). hiç fark yapar mı?
Daniel Hilgarth

4
% 24'ün while ifadesi için olmadığından ve while ifadesinin parçası olan koşul ifadesi olmadığından emin misin
Rune FS

2
Başka testi: için deneyin bu gibi while döngüsü yeniden yazma: while(true) { /* current body */ if(node.BranchData == null) return node; }. Herhangi bir şeyi değiştirir mi?
Daniel Hilgarth

2
Küçük bir optimizasyon şu şekilde olacaktır: while(true) { BranchNodeData b = node.BranchData; if(ReferenceEquals(b, null)) return node; node = b.Child2; if (inputs[b.SplitInputIndex] <= b.SplitValue) node = b.Child1; }Bu node. BranchDatayalnızca bir kez alınır .
Daniel Hilgarth

2
Lütfen en fazla zaman tüketen iki satırın toplam çalıştırılma sayısını ekleyin.
Daniel Hilgarth

Yanıtlar:


180

Ağaç çok büyük

Bir işlemcinin şimdiye kadar yaptığı en pahalı şey talimatları yürütmek değil, belleğe erişmektir. Modern yürütülmesi çekirdek CPU olan birçok kat daha hızlı bellek veri yolu daha. İle ilgili bir problem, bir mesafede olup, ayrıca bir elektrik sinyali imkanına sahiptir, daha o bozulmadan telin diğer ucu teslim edilen sinyal almak için alır. Bu sorunun tek çaresi, onu daha yavaş hale getirmektir. Makinenizdeki CPU'yu RAM'e bağlayan kablolarla ilgili büyük bir problem, kasayı açıp kabloları görebilirsiniz .

İşlemcilerin bu sorun için bir karşı önlemi vardır , RAM'de baytların bir kopyasını depolayan önbellekleri , arabellekleri kullanırlar . Önemli olan , tipik olarak veriler için 16 kilobayt ve talimatlar için 16 kilobayt olan L1 önbelleğidir . Küçük, yürütme motoruna yakın olmasına izin veriyor. L1 önbelleğinden bayt okumak genellikle 2 veya 3 CPU döngüsü sürer. Sırada, daha büyük ve daha yavaş L2 önbelleği var. Lüks işlemcilerin de daha büyük ve daha yavaş bir L3 önbelleği vardır. Süreç teknolojisi geliştikçe, bu tamponlar daha az yer kaplar ve çekirdeğe yaklaştıkça otomatik olarak daha hızlı hale gelir; bu, daha yeni işlemcilerin neden daha iyi olduğunun ve giderek artan sayıda transistör kullanmayı nasıl başardıklarının büyük bir nedeni.

Ancak bu önbellekler mükemmel bir çözüm değil. Veriler, önbelleklerden birinde mevcut değilse, işlemci hala bir bellek erişimini geciktirecektir. Çok yavaş bellek veriyolu veriyi sağlayana kadar devam edemez. Tek bir komutla yüzlerce CPU döngüsünü kaybetmek mümkündür.

Ağaç yapıları bir sorundur, önbellek dostu değildir . Düğümleri adres alanı boyunca dağılma eğilimindedir. Belleğe erişmenin en hızlı yolu, sıralı adreslerden okumaktır. L1 önbelleği için depolama birimi 64 bayttır. Veya başka bir deyişle, işlemci bir bayt okuduğunda , sonraki 63 bayt önbellekte bulunacağından çok hızlıdır.

Bu da bir diziyi açık ara en verimli veri yapısı yapar. Ayrıca .NET List <> sınıfının bir liste olmaması nedeni, depolama için bir dizi kullanır. Aynı, Dictionary gibi diğer koleksiyon türleri için de yapısal olarak uzaktan bir diziye benzemeyen, ancak dizilerle dahili olarak uygulanan.

Bu nedenle, while () deyiminiz, BranchData alanına erişmek için bir göstericiye başvuruda bulunmadığı için büyük olasılıkla CPU durmalarından muzdariptir. Sonraki ifade çok ucuzdur çünkü while () ifadesi, değeri bellekten almak gibi ağır bir yük halini almıştır. Yerel değişkeni atamak ucuzdur, bir işlemci yazma işlemleri için bir arabellek kullanır.

Aksi takdirde çözülmesi gereken basit bir problem değil, ağacınızı diziler halinde düzleştirmek çok pratik olmayacaktır. En azından değil, çünkü ağacın düğümlerinin hangi sırayla ziyaret edileceğini genellikle tahmin edemezsiniz. Kırmızı-siyah bir ağaç yardımcı olabilir, sorudan net değil. Dolayısıyla çıkarılacak basit bir sonuç, zaten umduğunuz kadar hızlı çalıştığıdır. Ve daha hızlı gitmesi gerekiyorsa, daha hızlı bir bellek veri yolu ile daha iyi donanıma ihtiyacınız olacak. DDR4 bu yıl ana akım olacak.


1
Olabilir. Birbiri ardına ayırdığınız için bunlar büyük olasılıkla bellekte ve dolayısıyla önbellekte zaten bitişiktir. GC yığın sıkıştırma algoritması ile aksi takdirde bu konuda öngörülemeyen bir etkisi olur. Bunu tahmin etmeme izin vermemenin en iyisi, ölçün ki bir gerçeği biliyorsunuz.
Hans Passant

11
İplikler bu sorunu çözmez. Size daha fazla çekirdek verir, hala yalnızca bir bellek veri yolunuz vardır.
Hans Passant

2
Belki b-ağacını kullanmak ağacın yüksekliğini sınırlayacaktır, bu nedenle her bir düğüm tek bir yapı olduğundan, önbellekte verimli bir şekilde saklanabildiğinden, daha az işaretleyiciye erişmeniz gerekecektir. Ayrıca bu soruya bakın .
MatthieuBizien

4
Her zamanki gibi geniş kapsamlı ilgili bilgilerle derinlemesine açıklayıcı. +1
Tigran

1
Ağaca erişim modelini biliyorsanız ve 80/20 (erişimin% 80'i her zaman düğümlerin aynı% 20'si üzerindedir) kuralını takip ederse, yaylı ağaç gibi kendi kendini ayarlayan bir ağaç da daha hızlı sonuçlanabilir. en.wikipedia.org/wiki/Splay_tree
Jens Timmerman

10

Hans'ın bellek önbellek etkileri hakkındaki harika yanıtını tamamlamak için, fiziksel bellek çevirisine ve NUMA efektlerine sanal bellek üzerine bir tartışma ekledim.

Sanal bellek bilgisayarında (tüm mevcut bilgisayar), bir bellek erişimi yapılırken, her sanal bellek adresi bir fiziksel bellek adresine çevrilmelidir. Bu, bir çeviri tablosu kullanılarak bellek yönetimi donanımı tarafından yapılır. Bu tablo, her işlem için işletim sistemi tarafından yönetilir ve kendisi RAM'de saklanır. Sanal belleğin her sayfası için , bu çeviri tablosunda bir sanal ile fiziksel sayfayı eşleyen bir giriş vardır. Hans'ın pahalı olan bellek erişimleriyle ilgili tartışmasını hatırlayın: eğer her sanaldan fiziksele çeviriye bir bellek araması gerekiyorsa, tüm bellek erişimi iki katına mal olur. Çözelti adlandırılır için tablo için bir önbellek sahip olmaktır çeviri denetleme tamponu(Kısaca TLB). TLB büyük değildir (12 ila 4096 giriş) ve x86-64 mimarisinde tipik sayfa boyutu yalnızca 4 KB'dir; bu , TLB isabetleriyle doğrudan erişilebilen en fazla 16 MB olduğu anlamına gelir (muhtemelen bundan daha azdır, Sandy 512 öğelik bir TLB boyutuna sahip köprü ). TLB kayıplarının sayısını azaltmak için, işletim sistemi ve uygulamanın 2 MB gibi daha büyük bir sayfa boyutu kullanmak üzere birlikte çalışmasını sağlayarak TLB isabetleriyle erişilebilen çok daha büyük bir bellek alanı elde edebilirsiniz. Bu sayfa , Java ile bellek erişimlerini büyük ölçüde hızlandırabilen büyük sayfaların nasıl kullanılacağını açıklar .

Bilgisayarınızda çok sayıda soket varsa, muhtemelen bir NUMA mimarisidir. NUMA, Tekdüzen Olmayan Bellek Erişimi anlamına gelir. Bu mimarilerde, bazı bellek erişimlerinin maliyeti diğerlerinden daha fazladır.. Örnek olarak, 32 GB RAM'e sahip 2 soketli bir bilgisayarda, her soket muhtemelen 16 GB RAM'e sahiptir. Bu örnek bilgisayarda, yerel bellek erişimi, başka bir soketin belleğine erişimden daha ucuzdur (uzaktan erişim% 20 ila% 100 daha yavaştır, hatta belki daha da fazla). Böyle bir bilgisayarda ağacınız 20 GB RAM kullanıyorsa, verilerinizin en az 4 GB'ı diğer NUMA düğümünde ise ve erişim uzak bellek için% 50 daha yavaşsa, NUMA erişimi bellek erişiminizi% 10 yavaşlatır. Ek olarak, tek bir NUMA düğümünde yalnızca boş belleğiniz varsa, açlıktan ölmüş düğümde belleğe ihtiyaç duyan tüm işlemler, diğer düğümden daha pahalı olan erişimler için bellek tahsis edilecektir. Daha da kötüsü, işletim sistemi aç düğümün belleğinin bir kısmını değiştirmenin iyi bir fikir olduğunu düşünebilir.bu daha pahalı bellek erişimlerine neden olur . Bu, MySQL “takas deliliği” probleminde ve Linux için bazı çözümlerin verildiği NUMA mimarisinin (tüm NUMA düğümlerine bellek erişimlerini yaymak, değiş tokuşu önlemek için uzak NUMA erişimlerinde mermiyi ısırmak) etkilerinde daha ayrıntılı olarak açıklanmıştır . Ayrıca bir sokete daha fazla RAM ayırmayı (16 ve 16 GB yerine 24 ve 8 GB) ve programınızın daha büyük NUMA düğümünde planlandığından emin olmayı düşünebilirim, ancak bunun bilgisayara fiziksel erişim ve bir tornavida ;-) .


4

Bu tek başına bir cevap değil, Hans Passant'ın bellek sistemindeki gecikmeler hakkında yazdıklarına bir vurgu.

Bilgisayar oyunları gibi gerçekten yüksek performanslı yazılımlar yalnızca oyunun kendisini uygulamak için yazılmaz, aynı zamanda kod ve veri yapılarının önbellek ve bellek sistemlerinden en iyi şekilde yararlanmasını sağlayacak şekilde uyarlanır, yani bunları sınırlı bir kaynak olarak görür. Önbellek sorunları ile uğraşırken, genellikle veriler mevcutsa L1'in 3 döngüde teslim edeceğini varsayıyorum. Değilse ve L2'ye gitmem gerekiyorsa 10 döngü varsayıyorum. L3 30 döngü ve RAM belleği 100 için.

Hafızayla ilgili ek bir eylem var - eğer onu kullanmanız gerekiyorsa - daha da büyük bir ceza uyguluyor ve bu bir veriyolu kilidi. Windows NT işlevini kullanıyorsanız, veri yolu kilitleri kritik bölümler olarak adlandırılır. Evde yetiştirilen bir çeşidi kullanırsanız, ona spinlock diyebilirsiniz. Kilit yerine oturmadan önce sistemdeki en yavaş veri yolu yönetim cihazına senkronize ettiği ad ne olursa olsun. En yavaş veri yolu yönetim aygıtı, 33 MHz'de bağlanan klasik bir 32 bitlik PCI kartı olabilir. 33MHz, tipik bir x86 CPU'nun (@ 3.3 GHz) frekansının yüzde biridir. Bir otobüs kilidini tamamlamak için en az 300 döngü olduğunu varsayıyorum, ancak bu kadar uzun süreceklerini biliyorum, bu nedenle 3000 döngü görürsem şaşırmayacağım.

Acemi çoklu iş parçacığı yazılım geliştiricileri her yerde veri yolu kilitleri kullanacak ve sonra kodlarının neden yavaş olduğunu merak edecekler. İşin püf noktası - hafıza ile ilgili her şeyde olduğu gibi - erişimden tasarruf etmektir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.