Sıralamanın, eğer… başka bir ifadeyle olasılıkla ifadelerinin etkisi nedir?


187

Özellikle, bir dizi if... else ififadem varsa ve bir şekilde her ifadenin değerlendireceği göreceli olasılığı önceden biliyorsanız true, yürütme süresinde olasılık sırasına göre sıralamak ne kadar fark yaratır? Örneğin, bunu tercih etmeliyim:

if (highly_likely)
  //do something
else if (somewhat_likely)
  //do something
else if (unlikely)
  //do something

buna?:

if (unlikely)
  //do something
else if (somewhat_likely)
  //do something
else if (highly_likely)
  //do something

Sıralanan sürümün daha hızlı olacağı açıktır, ancak okunabilirlik veya yan etkilerin varlığı için bunları en uygun şekilde sipariş etmek istemeyebiliriz. Kodu gerçekten çalışana kadar CPU'nun dal tahmini ile ne kadar iyi çalışacağını söylemek de zordur.

Böylece, bunu denerken, belirli bir vaka için kendi sorumu cevapladım, ancak diğer fikirleri / fikirleri de duymak istiyorum.

Önemli: Bu soru, ififadelerin programın davranışı üzerinde başka herhangi bir etkisi olmaksızın keyfi olarak yeniden sıralanabileceğini varsayar . Cevabımda, üç koşullu test birbirini dışlar ve hiçbir yan etki yaratmaz. Kuşkusuz, eğer istenen bir davranışı elde etmek için ifadelerin belirli bir sırada değerlendirilmesi gerekiyorsa, verimlilik konusu tartışmalıdır.


35
koşulların birbirini dışladığını, aksi takdirde iki sürümün eşdeğer olmadığını belirten bir not eklemek isteyebilirsiniz
idclev 463035818

28
Kendi kendine cevaplanan bir soru, bir saat içinde oldukça zayıf cevaplı 20+ upvotes nasıl elde edilir. OP'de bir şey çağırmamak, ancak üst düzey oyuncular bant vagonuna atlamaktan kaçınmalıdır. Soru ilginç olabilir, ancak sonuçlar şüphelidir.
luk32

3
Bunun bir kısa devre değerlendirmesi biçimi olarak tanımlanabileceğine inanıyorum çünkü bir karşılaştırmaya vurmak farklı bir karşılaştırmayı vurmayı reddediyor. Tek bir hızlı karşılaştırma, diyelim ki boolean, bir kaynak-ağır dize manipülasyonu, normal ifade veya veritabanı etkileşimi içerebilecek farklı bir karşılaştırmaya girmemi engelleyebiliyorsa, kişisel olarak böyle bir uygulamayı tercih ediyorum.
MonkeyZeus

11
Bazı derleyiciler, alınan dallar hakkında istatistik toplama ve daha iyi optimizasyonlar yapabilmeleri için bunları derleyiciye geri besleme olanağı sunar.

11
Bunun gibi bir performans sizin için önemliyse, muhtemelen Profil Kılavuzlu Optimizasyon'u denemeli ve manuel sonucunuzu derleyicinin sonucu ile karşılaştırmalısınız
Justin

Yanıtlar:


96

Genel bir kural olarak, tüm Intel CPU'lar ileri dalların ilk görüldüklerinde alınmadığını varsayarsa. Godbolt'un çalışmasına bakın .

Bundan sonra, şube bir şube tahmin önbelleğine girer ve gelecekteki şube tahminini bilgilendirmek için geçmiş davranış kullanılır.

Dolayısıyla, sıkı bir döngüde, yanlış ayarlamanın etkisi nispeten küçük olacaktır. Şube tahmincisi, hangi dal kümesinin büyük olasılıkla olduğunu öğrenecektir ve döngüde önemsiz olmayan bir işiniz varsa, küçük farklılıklar fazla toplanmaz.

Genel kodda, çoğu derleyici varsayılan olarak (başka bir nedenden yoksun) üretilen makine kodunu kabaca kodunuzda sipariş ettiğiniz şekilde sipariş edecektir. Böylece ifadeler başarısız olduklarında ileri dallar ise.

Bu nedenle dallarınızı, "ilk karşılaşma" dan en iyi dal tahminini elde etme olasılığını azaltarak sipariş etmelisiniz.

Bir dizi koşulda birçok kez sıkıca dolaşan ve önemsiz işler yapan bir mikrobenchmark, talimat sayısı ve benzerlerinin küçük etkileri ve göreceli dal tahmini sorunları yolunda çok az olacak. Yani bu durumda profil yapmalısınız , başparmak kuralları güvenilir olmayacağından .

Bunun da ötesinde, vektörleşme ve diğer birçok optimizasyon küçük sıkı döngüler için geçerlidir.

Bu nedenle, genel kodda, büyük olasılıkla kodu ifbloğun içine koyun ve bu, en az önbelleğe alınmamış dal tahmininin kaçırılmasına neden olur. Sıkı döngülerde, başlamak için genel kuralı izleyin ve daha fazlasını bilmeniz gerekiyorsa, profil yapmaktan başka seçeneğiniz yoktur.

Doğal olarak, bazı testler diğerlerinden çok daha ucuzsa, bunların hepsi pencereden dışarı çıkar.


19
Ayrıca, testlerin kendilerinin ne kadar pahalı olduğunu düşünmeye değer: bir test sadece biraz daha olası, ancak çok daha pahalıysa, diğer testi ilk sıraya koymaya değer, çünkü pahalı testi yapmamaktan tasarruf tahmin vb. tasarruf
psmears

Sağladığınız bağlantı sonucunuzu desteklemiyor Genel bir kural olarak, tüm Intel CPU'ların çoğu ileri dalların onları ilk kez görmediklerini varsayarsa . Aslında bu sadece sonuçları önce gösterilen nispeten belirsiz Arrendale CPU için geçerlidir. Ana Ivy Köprüsü ve Haswell sonuçları bunu desteklemiyor. Haswell, görünmeyen dallar için "her zaman düşmeyi tahmin et" e çok yakın görünüyor ve Ivy Bridge hiç net değil.
BeeOnRope

Genellikle CPU'ların geçmişte yaptıkları gibi gerçekten statik tahminler kullanmadığı anlaşılmaktadır. Gerçekten de modern Intel muhtemelen olasılıksal TAGE tahmincisi gibi bir şey kullanıyor. Şube geçmişini çeşitli geçmiş tablolarına ayırın ve en uzun geçmişe uyan bir tane alın. Diğer adlardan kaçınmak için bir "tag" kullanır, ancak etiketin yalnızca birkaç biti vardır. Tüm tarih uzunluklarını kaçırırsanız, muhtemelen şube yönüne bağlı olmayan bazı varsayılan tahminler yapılır (Haswell'de açıkça olmadığını söyleyebiliriz).
BeeOnRope

44

Biri olasılık sırasına göre sıralanmış, diğeri ters sırada sıralanmış iki farklı if... else ifbloğun yürütülmesini zamanlamak için aşağıdaki testi yaptım :

#include <chrono>
#include <iostream>
#include <random>
#include <algorithm>
#include <iterator>
#include <functional>

using namespace std;

int main()
{
    long long sortedTime = 0;
    long long reverseTime = 0;

    for (int n = 0; n != 500; ++n)
    {
        //Generate a vector of 5000 random integers from 1 to 100
        random_device rnd_device;
        mt19937 rnd_engine(rnd_device());
        uniform_int_distribution<int> rnd_dist(1, 100);
        auto gen = std::bind(rnd_dist, rnd_engine);
        vector<int> rand_vec(5000);
        generate(begin(rand_vec), end(rand_vec), gen);

        volatile int nLow, nMid, nHigh;
        chrono::time_point<chrono::high_resolution_clock> start, end;

        //Sort the conditional statements in order of increasing likelyhood
        nLow = nMid = nHigh = 0;
        start = chrono::high_resolution_clock::now();
        for (int& i : rand_vec) {
            if (i >= 95) ++nHigh;               //Least likely branch
            else if (i < 20) ++nLow;
            else if (i >= 20 && i < 95) ++nMid; //Most likely branch
        }
        end = chrono::high_resolution_clock::now();
        reverseTime += chrono::duration_cast<chrono::nanoseconds>(end-start).count();

        //Sort the conditional statements in order of decreasing likelyhood
        nLow = nMid = nHigh = 0;
        start = chrono::high_resolution_clock::now();
        for (int& i : rand_vec) {
            if (i >= 20 && i < 95) ++nMid;  //Most likely branch
            else if (i < 20) ++nLow;
            else if (i >= 95) ++nHigh;      //Least likely branch
        }
        end = chrono::high_resolution_clock::now();
        sortedTime += chrono::duration_cast<chrono::nanoseconds>(end-start).count();

    }

    cout << "Percentage difference: " << 100 * (double(reverseTime) - double(sortedTime)) / double(sortedTime) << endl << endl;
}

Sonuçlar / O2 ile MSVC2017 kullanıldığında, sıralanan sürümün sıralanmamış sürümden sürekli olarak yaklaşık% 28 daha hızlı olduğunu gösterir. Luk32'nin yorumuna göre, iki testin sırasını da değiştirdim, bu da fark edilir bir fark yaratıyor (% 22'ye karşı% 28). Kod, Intel Xeon E5-2697 v2'de Windows 7 altında çalıştırıldı. Bu elbette çok probleme özgüdür ve kesin bir cevap olarak yorumlanmamalıdır.


9
Ancak bir if... else ififadeyi değiştirmek mantığın koddan nasıl geçtiği üzerinde önemli bir etkiye sahip olabileceğinden OP dikkatli olmalıdır . unlikelyÇek sık gelip olmayabilir, ama kontrol etmek için bir iş ihtiyacı olabilir unlikelyilk diğerleri için kontrol etmeden önce durum.
Luke T Brooks

21
% 30 daha hızlı? Yani yapmak zorunda olmadığı ekstra ifadelerin kabaca% daha hızlı olduğunu mu demek istiyorsun? Oldukça makul bir sonuç gibi görünüyor.
UKMonkey

5
Nasıl karşılaştırdınız? Hangi derleyici, cpu, vs.? Bu sonucun taşınabilir olmadığından eminim.
luk32

12
Bu mikrobenchmark ile ilgili bir sorun, CPU'nun dallardan hangisinin büyük olasılıkla çalışacağını ve tekrar tekrar döngü yaptığınızda önbelleğe almasıdır. Küçük bir sıkı döngüde incelenmeyen dallar, dal tahmin önbelleği içinde olmayabilir ve CPU sıfır dal tahmin önbellek rehberliği ile yanlış tahmin ederse maliyetler çok daha yüksek olabilir.
Yakk - Adam Nevraumont

6
Bu karşılaştırma ölçütü çok güvenilir değil. Gcc 6.3.0 ile derleme : g++ -O2 -march=native -std=c++14sıralanmış koşullu ifadelere hafif bir kenar kazandırır, ancak çoğu zaman, iki çalışma arasındaki yüzde farkı ~% 5'tir. Birkaç kez, aslında daha yavaştı (varyanslar nedeniyle). Bu şekilde sipariş ifvermenin endişelenmeye değmeyeceğinden oldukça eminim ; PGO muhtemelen bu gibi durumları tamamen ele alacaktır
Justin

30

Hedef sistemin etkilendiğinden gerçekten emin olmadığınız sürece hayır yapmamalısınız. Varsayılan olarak okunabilirlik özelliğini kullanın.

Sonuçlarınızdan şüpheliyim. Örneğinizi biraz değiştirdim, bu yüzden tersine çevirme işlemi daha kolay. Ideone oldukça tutarlı bir şekilde ters sıralamanın daha hızlı olduğunu, ancak çok fazla olmadığını gösteriyor. Bazı koşularda bu bile zaman zaman ters çevrildi. Sonuçların sonuçsuz olduğunu söyleyebilirim. coliru gerçek bir fark olmadığını bildirdi. Daha sonra odroid xu4'ümde Exynos5422 CPU'yu kontrol edebilirim.

Mesele şu ki, modern CPU'ların dal tahmincileri var. Hem verileri hem de talimatları önceden getirmeye adanmış çok fazla mantık var ve bu konuda modern x86 CPU'lar oldukça akıllı. ARM'ler veya GPU'lar gibi bazı ince mimariler buna karşı savunmasız olabilir. Ancak hem derleyici hem de hedef sisteme oldukça bağımlıdır.

Şube sipariş optimizasyonunun oldukça kırılgan ve geçici olduğunu söyleyebilirim. Sadece gerçekten ince ayar adımı olarak yapın.

Kod:

#include <chrono>
#include <iostream>
#include <random>
#include <algorithm>
#include <iterator>
#include <functional>

using namespace std;

int main()
{
    //Generate a vector of random integers from 1 to 100
    random_device rnd_device;
    mt19937 rnd_engine(rnd_device());
    uniform_int_distribution<int> rnd_dist(1, 100);
    auto gen = std::bind(rnd_dist, rnd_engine);
    vector<int> rand_vec(5000);
    generate(begin(rand_vec), end(rand_vec), gen);
    volatile int nLow, nMid, nHigh;

    //Count the number of values in each of three different ranges
    //Run the test a few times
    for (int n = 0; n != 10; ++n) {

        //Run the test again, but now sort the conditional statements in reverse-order of likelyhood
        {
          nLow = nMid = nHigh = 0;
          auto start = chrono::high_resolution_clock::now();
          for (int& i : rand_vec) {
              if (i >= 95) ++nHigh;               //Least likely branch
              else if (i < 20) ++nLow;
              else if (i >= 20 && i < 95) ++nMid; //Most likely branch
          }
          auto end = chrono::high_resolution_clock::now();
          cout << "Reverse-sorted: \t" << chrono::duration_cast<chrono::nanoseconds>(end-start).count() << "ns" << endl;
        }

        {
          //Sort the conditional statements in order of likelyhood
          nLow = nMid = nHigh = 0;
          auto start = chrono::high_resolution_clock::now();
          for (int& i : rand_vec) {
              if (i >= 20 && i < 95) ++nMid;  //Most likely branch
              else if (i < 20) ++nLow;
              else if (i >= 95) ++nHigh;      //Least likely branch
          }
          auto end = chrono::high_resolution_clock::now();
          cout << "Sorted:\t\t\t" << chrono::duration_cast<chrono::nanoseconds>(end-start).count() << "ns" << endl;
        }
        cout << endl;
    }
}

Kodunuzda yapıldığı gibi, sıralı ve ters sıralamalı if-bloklarının sırasını değiştirdiğimde aynı ~% 30'luk performans farkını elde ediyorum. Ideone ve coliru'nun neden fark göstermediğinden emin değilim.
Carlton

Kesinlikle ilginç. Diğer sistemler için bazı veriler elde etmeye çalışacağım, ancak onunla oynamak zorunda kalıyorum. Soru, özellikle sonuçlarınız ışığında ilginç, ama o kadar muhteşem ki, çapraz kontrol etmek zorunda kaldım.
luk32

Soru şuysa, etkisi nedir? çare olamaz Hayır !
PJTraill

Evet. Ancak orijinal soruya ilişkin güncellemeler için bildirim almıyorum. Cevap formülasyonunu eskimiş hale getirdiler. Afedersiniz. İçeriği daha sonra düzenleyeceğim, orijinal soruya cevap verdiğini ve orijinal noktayı kanıtlayan bazı sonuçları göstereceğim.
luk32

Bu tekrar etmeye değer: "Varsayılan olarak okunabilirlik." Okunabilir kod yazmak, genellikle kodunuzu insanların ayrıştırmasını zorlaştırarak küçük bir performans artışı (mutlak terimlerle) bulmaya çalışmaktan daha iyi sonuçlar verir.
Andrew Brēza

26

Sadece 5 sentim. İfadelerin aşağıdakilere bağlı olması gerekirse sipariş vermenin etkisi görünüyor:

  1. Her if ifadesinin olasılığı.

  2. Yineleme sayısı, böylece şube tahmincisi devreye girebilir.

  3. Olası / olası olmayan derleyici ipuçları, yani kod düzeni.

Bu faktörleri araştırmak için aşağıdaki işlevleri karşılaştırdım:

ordered_ifs ()

for (i = 0; i < data_sz * 1024; i++) {
    if (data[i] < check_point) // highly likely
        s += 3;
    else if (data[i] > check_point) // samewhat likely
        s += 2;
    else if (data[i] == check_point) // very unlikely
        s += 1;
}

reversed_ifs ()

for (i = 0; i < data_sz * 1024; i++) {
    if (data[i] == check_point) // very unlikely
        s += 1;
    else if (data[i] > check_point) // samewhat likely
        s += 2;
    else if (data[i] < check_point) // highly likely
        s += 3;
}

ordered_ifs_with_hints ()

for (i = 0; i < data_sz * 1024; i++) {
    if (likely(data[i] < check_point)) // highly likely
        s += 3;
    else if (data[i] > check_point) // samewhat likely
        s += 2;
    else if (unlikely(data[i] == check_point)) // very unlikely
        s += 1;
}

reversed_ifs_with_hints ()

for (i = 0; i < data_sz * 1024; i++) {
    if (unlikely(data[i] == check_point)) // very unlikely
        s += 1;
    else if (data[i] > check_point) // samewhat likely
        s += 2;
    else if (likely(data[i] < check_point)) // highly likely
        s += 3;
}

veri

Veri dizisi 0 ile 100 arasında rastgele sayılar içerir:

const int RANGE_MAX = 100;
uint8_t data[DATA_MAX * 1024];

static void data_init(int data_sz)
{
    int i;
        srand(0);
    for (i = 0; i < data_sz * 1024; i++)
        data[i] = rand() % RANGE_MAX;
}

Sonuçlar

Aşağıdaki sonuçlar Intel i5 @ 3,2 GHz ve G ++ 6.3.0 içindir. İlk argüman check_point'tir (yani büyük olasılıkla if ifadesi için %% olasılığı), ikinci argüman data_sz'dir (yani yineleme sayısı).

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4                    4660 ns       4658 ns     150948
ordered_ifs/50/8                   25636 ns      25635 ns      27852
ordered_ifs/75/4                    4326 ns       4325 ns     162613
ordered_ifs/75/8                   18242 ns      18242 ns      37931
ordered_ifs/100/4                   1673 ns       1673 ns     417073
ordered_ifs/100/8                   3381 ns       3381 ns     207612
reversed_ifs/50/4                   5342 ns       5341 ns     126800
reversed_ifs/50/8                  26050 ns      26050 ns      26894
reversed_ifs/75/4                   3616 ns       3616 ns     193130
reversed_ifs/75/8                  15697 ns      15696 ns      44618
reversed_ifs/100/4                  3738 ns       3738 ns     188087
reversed_ifs/100/8                  7476 ns       7476 ns      93752
ordered_ifs_with_hints/50/4         5551 ns       5551 ns     125160
ordered_ifs_with_hints/50/8        23191 ns      23190 ns      30028
ordered_ifs_with_hints/75/4         3165 ns       3165 ns     218492
ordered_ifs_with_hints/75/8        13785 ns      13785 ns      50574
ordered_ifs_with_hints/100/4        1575 ns       1575 ns     437687
ordered_ifs_with_hints/100/8        3130 ns       3130 ns     221205
reversed_ifs_with_hints/50/4        6573 ns       6572 ns     105629
reversed_ifs_with_hints/50/8       27351 ns      27351 ns      25568
reversed_ifs_with_hints/75/4        3537 ns       3537 ns     197470
reversed_ifs_with_hints/75/8       16130 ns      16130 ns      43279
reversed_ifs_with_hints/100/4       3737 ns       3737 ns     187583
reversed_ifs_with_hints/100/8       7446 ns       7446 ns      93782

analiz

1. Sipariş Önemlidir

4K yinelemeleri ve (neredeyse)% 100 yüksek beğeni oranı olasılığı için, fark çok büyük% 223:

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/100/4                   1673 ns       1673 ns     417073
reversed_ifs/100/4                  3738 ns       3738 ns     188087

4K yinelemeleri ve% 50 oranında çok sevilen ifade olasılığı için fark yaklaşık% 14'tür:

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4                    4660 ns       4658 ns     150948
reversed_ifs/50/4                   5342 ns       5341 ns     126800

2. Yineleme Sayısı Önemli

4K ve 8K yinelemeleri arasındaki (neredeyse)% 100 yüksek oranda beğeni olasılığı olasılığı arasındaki fark yaklaşık iki kattır (beklendiği gibi):

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/100/4                   1673 ns       1673 ns     417073
ordered_ifs/100/8                   3381 ns       3381 ns     207612

Ancak,% 50 oranında çok beğenilen ifadenin olasılığı için 4K ve 8K yinelemeleri arasındaki fark 5,5 kattır:

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4                    4660 ns       4658 ns     150948
ordered_ifs/50/8                   25636 ns      25635 ns      27852

Neden böyle? Şube tahmincisi özlüyor çünkü. Yukarıda belirtilen her vaka için şube özlüyor:

ordered_ifs/100/4    0.01% of branch-misses
ordered_ifs/100/8    0.01% of branch-misses
ordered_ifs/50/4     3.18% of branch-misses
ordered_ifs/50/8     15.22% of branch-misses

Bu yüzden i5'imde dal tahmincisi, çok olası olmayan dallar ve büyük veri setleri için muhteşem bir şekilde başarısız oluyor.

3. İpuçları Biraz Yardım

4K yinelemelerinde sonuçlar% 50 olasılık için biraz daha kötü ve% 100'e yakın olasılık için biraz daha iyidir:

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4                    4660 ns       4658 ns     150948
ordered_ifs/100/4                   1673 ns       1673 ns     417073
ordered_ifs_with_hints/50/4         5551 ns       5551 ns     125160
ordered_ifs_with_hints/100/4        1575 ns       1575 ns     437687

Ancak 8K yinelemeleri için sonuçlar her zaman biraz daha iyidir:

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/8                   25636 ns      25635 ns      27852
ordered_ifs/100/8                   3381 ns       3381 ns     207612
ordered_ifs_with_hints/50/8        23191 ns      23190 ns      30028
ordered_ifs_with_hints/100/8        3130 ns       3130 ns     221205

Yani, ipuçları da yardımcı oluyor, ama sadece biraz.

Genel sonuç şudur: Her zaman kodu kıyaslayın, çünkü sonuçlar şaşırtıcı olabilir.

Umarım yardımcı olur.


1
i5 Nehalem? i5 Skylake? Sadece "i5" demek çok spesifik değil. Ayrıca, kullandığınızı varsayıyorum g++ -O2ya da -O3 -fno-tree-vectorize, ama söylemelisiniz.
Peter Cordes

With_hints'in sıralı veya ters çevrilmiş için hala farklı olması ilginçtir. Bir yere kaynak bağlasanız iyi olur. (örneğin bir Godbolt bağlantısı, tercihen bir tam bağlantı, böylece bağlantı kısalması çürümez.)
Peter Cordes

1
Şube öngörücüsünün 4K giriş veri boyutunda bile iyi tahmin edebilmesi, yani binlik bir döneme sahip bir döngü boyunca şube sonuçlarını hatırlayarak karşılaştırmayı "kırabilir" modernin gücünün bir kanıtıdır. şube belirleyicileri. Tahmincilerin bazı durumlarda hizalama gibi şeylere oldukça duyarlı olduğunu unutmayın, bu nedenle bazı değişiklikler hakkında güçlü sonuçlar çıkarmak zordur. Örneğin, farklı durumlarda ipucunun zıt davranışını fark ettiniz, ancak tahminciyi etkileyen ipucu rastgele değişen kod düzeni ile açıklanabilir.
BeeOnRope

1
@PeterCordes benim asıl amacım bir değişikliğin sonuçlarını tahmin etmeye çalışırken, yine de değişiklikten önce ve sonra performansı daha iyi ölçmemizdir ... Ve haklısınız, -O3 ve işlemci ile optimize edildiğinden bahsetmeliydim i5-4460 @ 3.20GHz
Andriy Berestovskyy

19

Buradaki diğer cevaplardan bazılarına dayanarak, tek gerçek cevap şudur: duruma bağlıdır . En azından aşağıdakilere bağlıdır (bu önem sırasına göre olmasa da):

  • Her dalın nispi olasılığı. Bu sorulan orijinal soru. Mevcut cevaplara dayanarak, olasılıkla sıralamanın yardımcı olduğu bazı durumlar var gibi görünüyor, ancak her zaman böyle değil. Göreceli olasılıklar çok farklı değilse, hangi sırayla oldukları konusunda herhangi bir fark yaratma olasılığı düşüktür. Ancak, ilk koşul zamanın% 99,999'u olur ve bir sonraki durum kalanın bir kısmı ise, o zaman ben en muhtemel olanı ilk sıralamanın zamanlama açısından yararlı olacağını varsayalım.
  • Her dal için doğru / yanlış koşulu hesaplama maliyeti. Koşulları test etmenin zaman maliyeti bir dal için diğerine karşı gerçekten yüksekse, bunun zamanlama ve verimlilik üzerinde önemli bir etkisi olması muhtemeldir. Örneğin, hesaplanması için 1 zaman birimi alan (örneğin, bir Boole değişkeninin durumunu kontrol etme) on bir, yüzlerce, binlerce ve hatta milyonlarca zaman birimini hesaplayan başka bir koşula (örneğin, diskteki bir dosya veya büyük bir veritabanına karşı karmaşık bir SQL sorgusu gerçekleştirme). Kodun koşulları her seferinde sırayla kontrol ettiği varsayılarak, daha hızlı koşullar ilk önce olmalıdır (önce başarısız olan diğer koşullara bağlı olmadıkça).
  • Derleyici / Tercüman Bazı derleyiciler (veya tercümanlar) performansı etkileyebilecek bir tür diğerinin optimizasyonunu içerebilir (ve bunların bazıları yalnızca derleme ve / veya yürütme sırasında belirli seçenekler seçiliyse mevcuttur). Dolayısıyla, tek farkın söz konusu dalların sırası olduğu tam olarak aynı derleyiciyi kullanarak aynı sistemde aynı kodun iki derlemesini ve yürütülmesini kıyaslamadığınız sürece, derleyici varyasyonları için biraz boşluk vermeniz gerekecektir.
  • İşletim Sistemi / Donanım luk32 ve Yakk tarafından belirtildiği gibi, çeşitli CPU'ların kendi optimizasyonları vardır (işletim sistemlerinde olduğu gibi). Bu yüzden kıyaslama kriterleri yine buradaki varyasyona açıktır.
  • Kod bloğu yürütme sıklığı Şubeleri içeren bloğa nadiren erişilirse (örneğin, başlatma sırasında yalnızca bir kez), şubeleri hangi sıraya koyduğunuz büyük olasılıkla çok az önemlidir. Öte yandan, kodunuzun kritik bir kısmı sırasında kodunuz bu kod bloğuna çarpıyorsa, sipariş vermek çok önemli olabilir (kıyaslama ölçütlerine bağlı olarak).

Kesin olarak bilmenin tek yolu, özel durumunuzu, tercihen kodun nihayet çalışacağı amaçlanan sistemle özdeş (veya çok benzer) bir sistemde karşılaştırmaktır. Farklı donanım, işletim sistemi vb. İle değişen bir dizi sistemde çalışması amaçlanıyorsa, hangisinin en iyi olduğunu görmek için birden fazla varyasyon arasında karşılaştırma yapmak iyi bir fikirdir. Kodun bir tür sistemde bir siparişle ve başka bir sistemde başka bir siparişle derlenmesi iyi bir fikir olabilir.

Kişisel kuralım (çoğu durumda, bir kıyaslama yokluğunda) aşağıdakilere dayanarak sipariş vermektir:

  1. Önceki koşulların sonucuna dayanan koşullar,
  2. Durumu hesaplama maliyeti, o zaman
  3. Her dalın nispi olasılığı.

13

Bunu genellikle yüksek performanslı kod için çözdüğüm şekilde, en okunaklı olan sıralamayı korumak, ancak derleyiciye ipuçları sağlamaktır. İşte Linux çekirdeğinden bir örnek :

if (likely(access_ok(VERIFY_READ, from, n))) {
    kasan_check_write(to, n);
    res = raw_copy_from_user(to, from, n);
}
if (unlikely(res))
    memset(to + (n - res), 0, res);

Burada varsayım, erişim kontrolünün geçeceği ve hiçbir hatanın döndürülmediği varsayımıdır res. Yan tümceler kodu karıştırırsa, bunlardan birini yeniden sıralamaya çalışmak, ancak likely()veunlikely() makroları aslında normal durumun ve istisnanın ne olduğunu göstererek okunabilirliğe yardımcı olur.

Bu makroların Linux uygulaması GCC'ye özgü özellikler kullanır . Görünüşe göre clang ve Intel C derleyicisi aynı sözdizimini destekliyor, ancak MSVC'nin böyle bir özelliği yok .


4
likely()Ve unlikely()makrolarının nasıl tanımlandığını açıklayabilir ve ilgili derleyici özelliği hakkında bazı bilgiler eklerseniz, bu daha yararlı olacaktır .
Nate Eldredge

1
AFAIK, bu ipuçlarını "sadece" kod bloklarının bellek düzenini ve bir evet veya hayırın atlamaya neden olup olmayacağını değiştirir. Bunun, bellek sayfalarını okuma ihtiyacı (veya eksikliği) için performans avantajları olabilir. Ancak bu, uzun bir başka if-listeler listesindeki koşulların değerlendirilme sırasını yeniden düzenlemez
Hagen von Eitzen

@HagenvonEitzen Hmm evet, bu iyi bir nokta, else ifderleyicinin koşulların karşılıklı olarak ayrıcalıklı olduğunu bilecek kadar akıllı olmaması sırasını etkileyemez .
jpa

7

Ayrıca derleyicinize ve derlediğiniz platforma da bağlıdır.

Teorik olarak, en olası durum, kontrolün mümkün olduğunca az sıçramasını sağlamalıdır.

Genellikle en olası koşul ilk olmalıdır:

if (most_likely) {
     // most likely instructions
} else 

En popüler asmlar koşul doğru olduğunda atlayan koşullu dallara dayanır . Bu C kodu muhtemelen bu tür sahte asma tercüme edilecektir:

jump to ELSE if not(most_likely)
// most likely instructions
jump to end
ELSE:

Bunun nedeni, atlamaların cpu'yu yürütme ardışık düzenini ve duraklamasını iptal ettirmesidir, çünkü program sayacı değişmiştir (gerçekten yaygın olan boru hatlarını destekleyen mimariler için). Daha sonra, derleyici hakkındadır; bu, kontrolün daha az sıçrama yapmasını sağlamak için istatistiksel olarak en olası koşula sahip olma konusunda bazı gelişmiş optimizasyonlar uygulayabilir veya uygulamıyor olabilir.


2
Koşullu dalın koşul doğru olduğunda oluştuğunu, ancak "sözde asm" örneğinin tersini gösterdiğini belirttiniz. Ayrıca, modern CPU'ların tipik olarak dal tahmini olduğu için koşullu sıçramaların (çok daha az tüm atlamaların) boru hattını durdurduğu söylenemez. Dal almış ama sonra olduğu öngörülür Aslında, değil alınan boru hattı durmuş olacaktır. Hala olasılık sırasına göre koşullarını sıralamak çalışıyorum, ama ne olmuş derleyici ve CPU yapmak olduğunu ediyorum son derece uygulama bağımlı.
Arne Vogel

1
“Not (most_likely)” koydum, bu yüzden most_likely doğruysa kontrol atlamadan devam edecektir.
NoImaginationGuy

1
"En popüler asmlar koşul doğru olduğunda atlayan koşullu dallara dayanmaktadır" .. bu hangi ISA'lar olurdu? X86 ve ARM için kesinlikle doğru değil. Cehennem dallanma öngörüsü bir ileri şube varsayar (genellikle hala varsayımına ile başlamak ve daha sonra adapte hatta karmaşık bps ve çok eski x86 olanlar) temel ARM işlemciler için değil iddianın tersi bu yüzden, alınan ve geriye dalları her zaman doğru.
Voo

1
Denediğim derleyiciler çoğunlukla basit bir test için yukarıda bahsettiğim yaklaşımı kullandı. Not clangaslında için farklı bir yaklaşım aldı test2ve test3: çünkü belirtmek sezgisel < 0veya == 0deney muhtemel yanlış olduğu, bu bunu yapmak mümkün, böylece her iki yollarında fonksiyonunun kalan klonlamak için karar condition == falseyolundan düşüş. Bu sadece işlevin geri kalanı kısa olduğu için mümkündür: içinde test4bir işlem daha ekledim ve bu yukarıda özetlediğim yaklaşıma geri döndü.
BeeOnRope

1
@ArneVogel - Doğru tahmin alınan dalları tamamen modern CPU'lar üzerinde boru hattı durak yok ama çoğu zaman yine alınmamıştır daha kötü şunlardır: (1) onlar kontrol akış sonra talimatların geri kalanı çok bitişik olmadığı anlamına jmpdeğil faydalı olması nedeniyle bant genişliğinin getirilmesi / boşa harcanması (2) tahminiyle bile modern büyük çekirdekler döngü başına sadece bir getirme yapar, böylece 1 alınan dal / döngüyü zorlar (OTOH modern Intel 2 alınamaz / döngü yapabilir) (3 ) Şube tahmini için ardışık alınan dallarla başa çıkmak ve hızlı + yavaş öngörücüler durumunda ...
BeeOnRope

6

Testi Lik32 kodunu kullanarak kendi makinemde tekrar çalıştırmaya karar verdim. Windows veya derleyicimin yüksek çözünürlük 1 ms olduğunu düşünerek değiştirmek zorunda kaldım,

mingw32-g ++. exe -O3 -Duvar -std = c ++ 11 -özellikler -g

vector<int> rand_vec(10000000);

GCC, her iki orijinal kodda da aynı dönüşümü gerçekleştirdi.

Üçüncünün her zaman doğru olması gerektiği için sadece ilk iki koşulun test edildiğine dikkat edin, GCC burada bir Sherlock türüdür.

Tersine çevirmek

.L233:
        mov     DWORD PTR [rsp+104], 0
        mov     DWORD PTR [rsp+100], 0
        mov     DWORD PTR [rsp+96], 0
        call    std::chrono::_V2::system_clock::now()
        mov     rbp, rax
        mov     rax, QWORD PTR [rsp+8]
        jmp     .L219
.L293:
        mov     edx, DWORD PTR [rsp+104]
        add     edx, 1
        mov     DWORD PTR [rsp+104], edx
.L217:
        add     rax, 4
        cmp     r14, rax
        je      .L292
.L219:
        mov     edx, DWORD PTR [rax]
        cmp     edx, 94
        jg      .L293 // >= 95
        cmp     edx, 19
        jg      .L218 // >= 20
        mov     edx, DWORD PTR [rsp+96]
        add     rax, 4
        add     edx, 1 // < 20 Sherlock
        mov     DWORD PTR [rsp+96], edx
        cmp     r14, rax
        jne     .L219
.L292:
        call    std::chrono::_V2::system_clock::now()

.L218: // further down
        mov     edx, DWORD PTR [rsp+100]
        add     edx, 1
        mov     DWORD PTR [rsp+100], edx
        jmp     .L217

And sorted

        mov     DWORD PTR [rsp+104], 0
        mov     DWORD PTR [rsp+100], 0
        mov     DWORD PTR [rsp+96], 0
        call    std::chrono::_V2::system_clock::now()
        mov     rbp, rax
        mov     rax, QWORD PTR [rsp+8]
        jmp     .L226
.L296:
        mov     edx, DWORD PTR [rsp+100]
        add     edx, 1
        mov     DWORD PTR [rsp+100], edx
.L224:
        add     rax, 4
        cmp     r14, rax
        je      .L295
.L226:
        mov     edx, DWORD PTR [rax]
        lea     ecx, [rdx-20]
        cmp     ecx, 74
        jbe     .L296
        cmp     edx, 19
        jle     .L297
        mov     edx, DWORD PTR [rsp+104]
        add     rax, 4
        add     edx, 1
        mov     DWORD PTR [rsp+104], edx
        cmp     r14, rax
        jne     .L226
.L295:
        call    std::chrono::_V2::system_clock::now()

.L297: // further down
        mov     edx, DWORD PTR [rsp+96]
        add     edx, 1
        mov     DWORD PTR [rsp+96], edx
        jmp     .L224

Yani bu bize çok fazla şey anlatmıyor, ancak son durum bir şube tahmininin gerekmediği.

Şimdi if'lerin 6 kombinasyonunun hepsini denedim, ilk 2 orijinal ters ve sıralanmıştır. yüksek> = 95, düşük <20, orta 20-94 ve her biri 10000000 yineleme.

high, low, mid: 43000000ns
mid, low, high: 46000000ns
high, mid, low: 45000000ns
low, mid, high: 44000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 44000000ns
mid, low, high: 47000000ns
high, mid, low: 44000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 45000000ns

high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 44000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 42000000ns
mid, low, high: 46000000ns
high, mid, low: 46000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 43000000ns

high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 44000000ns
low, mid, high: 44000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 43000000ns
mid, low, high: 48000000ns
high, mid, low: 44000000ns
low, mid, high: 44000000ns
mid, high, low: 45000000ns
low, high, mid: 45000000ns

high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 45000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 45000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 43000000ns
mid, low, high: 46000000ns
high, mid, low: 45000000ns
low, mid, high: 45000000ns
mid, high, low: 45000000ns
low, high, mid: 44000000ns

high, low, mid: 42000000ns
mid, low, high: 46000000ns
high, mid, low: 44000000ns
low, mid, high: 45000000ns
mid, high, low: 45000000ns
low, high, mid: 44000000ns

1900020, 7498968, 601012

Process returned 0 (0x0)   execution time : 2.899 s
Press any key to continue.

Peki neden sipariş yüksek, düşük, med sonra daha hızlı (marjinal)

Çünkü en tahmin edilemez sondur ve bu nedenle hiçbir zaman bir dal öngörücüsünden geçmez.

          if (i >= 95) ++nHigh;               // most predictable with 94% taken
          else if (i < 20) ++nLow; // (94-19)/94% taken ~80% taken
          else if (i >= 20 && i < 95) ++nMid; // never taken as this is the remainder of the outfalls.

Böylece şubelerin alınacağı, alınacağı ve

% 6 + (0,94 *)% 20 yanlış tahmin.

"Sıralama"

          if (i >= 20 && i < 95) ++nMid;  // 75% not taken
          else if (i < 20) ++nLow;        // 19/25 76% not taken
          else if (i >= 95) ++nHigh;      //Least likely branch

Dallar alınmaz, alınmaz ve Sherlock ile tahmin edilir.

% 25 + (0,75 *)% 24 yanlış tahminler

% 18-23 fark veriyor (~% 9 ölçülen fark), ancak% tahmin etmek yerine döngüleri hesaplamamız gerekiyor.

Nehalem CPU'mdaki 17 döngünün cezayı yanlış tahmin ettiğini ve her kontrolün 1 döngünün (4-5 talimat) ve döngü de bir döngünün alındığını varsayalım. Veri bağımlılıkları sayaçlar ve döngü değişkenleridir, ancak yanlış tahminler yoldan çıktığında zamanlamayı etkilememelidir.

Yani "geri" için zamanlamaları alıyoruz (bu Bilgisayar Mimarisinde kullanılan formül olmalı: Sayısal Bir Yaklaşım IIRC).

mispredict*penalty+count+loop
0.06*17+1+1+    (=3.02)
(propability)*(first check+mispredict*penalty+count+loop)
(0.19)*(1+0.20*17+1+1)+  (= 0.19*6.4=1.22)
(propability)*(first check+second check+count+loop)
(0.75)*(1+1+1+1) (=3)
= 7.24 cycles per iteration

ve "sıralama" için de aynı

0.25*17+1+1+ (=6.25)
(1-0.75)*(1+0.24*17+1+1)+ (=.25*7.08=1.77)
(1-0.75-0.19)*(1+1+1+1)  (= 0.06*4=0.24)
= 8.26

(8.26-7.24) /8.26 = ölçülen ~% 9'a karşılık% 13.8 (ölçülene yakın!?!).

Yani OP'nin barizliği belli değil.

Bu testlerle, daha karmaşık kodlu veya daha fazla veri bağımlılığına sahip diğer testler kesinlikle farklı olacaktır, bu nedenle durumunuzu ölçün.

Testin sırasını değiştirmek sonuçları değiştirdi, ancak bunun nedeni döngü başlangıcının farklı yeni hizalamalar nedeniyle olabilir; bu, ideal olarak tüm yeni Intel CPU'larda 16 bayt hizalanmalıdır, ancak bu durumda değildir.


4

Onları istediğiniz mantıksal sıraya koyun. Tabii, şube daha yavaş olabilir, ancak dallanma bilgisayarınızın yaptığı işin çoğunluğu olmamalıdır.

Kodun performans açısından kritik bir kısmı üzerinde çalışıyorsanız, o zaman kesinlikle mantıksal düzen, profil güdümlü optimizasyon ve diğer teknikleri kullanın, ancak genel kod için, bunun gerçekten daha stilistik bir seçim olduğunu düşünüyorum.


6
Şube tahmin başarısızlıkları pahalıdır. Microbenchmarks olarak, onlar maliyetlendirilmiş altında x86s şube belirteçlerinin büyük bir masa var çünkü. Aynı koşullar üzerinde sıkı bir döngü CPU'nun hangisinden daha iyi olduğunu bilmesini sağlar. Ancak, kodunuzun her yerinde şubeleriniz varsa, şube tahmin önbelleğinizin yuvalardan tükenmesini sağlayabilirsiniz ve CPU varsayılan olanı varsayar. Bu varsayılan tahminin ne olduğunu bilmek, kod tabanınızın her yerinde döngüleri kaydedebilir.
Yakk - Adam Nevraumont

@Yakk Jack'in yanıtı buradaki tek doğru cevaptır. Derleyiciniz bu optimizasyonu yapabiliyorsa okunabilirliği azaltan optimizasyonlar yapmayın. Derleyiciniz sizin için yaparsa, sürekli katlama, ölü kod ortadan kaldırma, döngü açma veya başka bir optimizasyon yapmazsınız, değil mi? Kodunuzu yazın, profil güdümlü optimizasyonu kullanın (kodlayıcılar tahminlerde emildiğinden bu sorunu çözmek için tasarlanmıştır) ve derleyicinizin bunu optimize edip etmediğini görün. Sonunda, performans açısından kritik bir kodda hiçbir şubeye sahip olmak istemezsiniz.
Christoph Diegelmann

@Christoph Öldüğünü bildiğim kodu eklemem. Ne i++zaman ++iyaparım, çünkü i++bazı yineleyiciler için optimize etmek zor olduğunu ++ive fark (benim için) önemli olmadığını biliyorum . Bu kötümserlikten kaçınmakla ilgilidir; en olası bloğu ilk olarak varsayılan bir alışkanlık olarak koymak, fark edilebilir bir okunabilirlik azalmasına neden olmaz (ve aslında yardımcı olabilir!) ve bu da dal tahmin dostu bir kodla sonuçlanır (ve böylece size yeniden yakalanamayacak tek tip küçük bir performans artışı sağlar. mikro optimizasyon ile)
Yakk - Adam Nevraumont 20:17

3

İf-else ifadesinin göreceli olasılığını zaten biliyorsanız, performans amacıyla, yalnızca bir koşulu (doğru olanı) kontrol edeceği için sıralı yolu kullanmak daha iyi olacaktır.

Derleyici sıralanmamış bir şekilde tüm koşulları gereksiz yere kontrol eder ve zaman alır.

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.