Sıralı bir diziyi işlemek neden sıralanmamış bir diziyi işlemekten daha hızlı?


24445

İşte bazı tuhaf davranışlar gösteren bir C ++ kodu parçası. Tuhaf bir nedenden dolayı, verileri mucizevi bir şekilde sıralamak kodu neredeyse altı kat daha hızlı hale getirir:

#include <algorithm>
#include <ctime>
#include <iostream>

int main()
{
    // Generate data
    const unsigned arraySize = 32768;
    int data[arraySize];

    for (unsigned c = 0; c < arraySize; ++c)
        data[c] = std::rand() % 256;

    // !!! With this, the next loop runs faster.
    std::sort(data, data + arraySize);

    // Test
    clock_t start = clock();
    long long sum = 0;

    for (unsigned i = 0; i < 100000; ++i)
    {
        // Primary loop
        for (unsigned c = 0; c < arraySize; ++c)
        {
            if (data[c] >= 128)
                sum += data[c];
        }
    }

    double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;

    std::cout << elapsedTime << std::endl;
    std::cout << "sum = " << sum << std::endl;
}
  • Olmadan std::sort(data, data + arraySize);, kod 11.54 saniye içinde çalışır.
  • Sıralanan verilerle kod 1,93 saniyede çalışır.

Başlangıçta, bunun sadece bir dil veya derleyici anomalisi olabileceğini düşündüm, bu yüzden Java'yı denedim:

import java.util.Arrays;
import java.util.Random;

public class Main
{
    public static void main(String[] args)
    {
        // Generate data
        int arraySize = 32768;
        int data[] = new int[arraySize];

        Random rnd = new Random(0);
        for (int c = 0; c < arraySize; ++c)
            data[c] = rnd.nextInt() % 256;

        // !!! With this, the next loop runs faster
        Arrays.sort(data);

        // Test
        long start = System.nanoTime();
        long sum = 0;

        for (int i = 0; i < 100000; ++i)
        {
            // Primary loop
            for (int c = 0; c < arraySize; ++c)
            {
                if (data[c] >= 128)
                    sum += data[c];
            }
        }

        System.out.println((System.nanoTime() - start) / 1000000000.0);
        System.out.println("sum = " + sum);
    }
}

Benzer ama daha az uç bir sonuçla.


Benim ilk düşüncem sıralama önbellek veri getiriyordu, ama sonra dizi sadece oluşturulan çünkü bunun ne kadar aptalca olduğunu düşündüm.

  • Ne oluyor?
  • Sıralı bir diziyi işlemek neden sıralanmamış bir diziyi işlemekten daha hızlı?

Kod bazı bağımsız terimleri özetliyor, bu nedenle sipariş önemli olmamalı.



16
@SachinVerma Başımın üstünden: 1) JVM sonunda koşullu hareketleri kullanabilecek kadar akıllı olabilir. 2) Kod belleğe bağlıdır. 200M, CPU önbelleğine sığmayacak kadar büyük. Böylece performans dallanma yerine bellek bant genişliği ile tıkanacaktır.
Gizemli

12
@ Gizemli, yaklaşık 2). Tahmin tablosunun desenleri izlediğini (bu model için kontrol edilen gerçek değişkenlere bakılmaksızın) ve tahmin çıktısını tarihe göre değiştirdiğini düşündüm. Bana bir neden verebilir misiniz, neden süper geniş bir dizi şube tahmininden faydalanmasın?
Sachin Verma

15
@SachinVerma Var, ancak dizi bu kadar büyük olduğunda, büyük olasılıkla daha büyük bir faktör devreye giriyor - bellek bant genişliği. Bellek düz değil . Belleğe erişim çok yavaştır ve sınırlı miktarda bant genişliği vardır. İşleri aşırı basitleştirmek için, CPU ve bellek arasında sabit bir süre içinde aktarılabilecek çok fazla bayt vardır. Bu sorudaki gibi basit bir kod, yanlış tahminlerle yavaşlansa bile muhtemelen bu sınıra ulaşacaktır. CPU'nun L2 önbelleğine sığdığı için 32768 (128KB) dizisi ile bu gerçekleşmez.
Gizemli

13
BranchScope adında yeni bir güvenlik açığı var: cs.ucr.edu/~nael/pubs/asplos18.pdf
Veve

Yanıtlar:


31790

Sen şube tahmin kurbanı başarısız.


Şube Tahmini nedir?

Bir demiryolu kavşağını düşünün:

Demiryolu kavşağı gösteren resim Mecanismo, Wikimedia Commons üzerinden görüntü . CC-By-SA 3.0 lisansı altında kullanılır .

Şimdi tartışma uğruna, bunun 1800'lerde - uzun mesafe veya radyo iletişiminden önce olduğunu varsayalım.

Bir kavşağın operatörü sizsiniz ve bir trenin geldiğini duyuyorsunuz. Hangi yöne gitmesi gerektiği hakkında hiçbir fikrin yok. Sürücüye hangi yönde istediklerini sormak için treni durdurursunuz. Ve sonra anahtarı uygun şekilde ayarladınız.

Trenler ağırdır ve çok fazla atalete sahiptir. Bu yüzden başlamak ve yavaşlamak sonsuza dek sürüyor.

Daha iyi bir yol var mı? Trenin hangi yöne gideceğini tahmin et!

  • Doğru tahmin ederseniz, devam eder.
  • Yanlış tahmin ederseniz, kaptan duracak, geri dönecek ve anahtarı çevirmeniz için size bağıracaktır. Sonra diğer yoldan yeniden başlayabilir.

Her seferinde doğru tahmin ederseniz , tren asla durmak zorunda kalmayacak.
Çok sık yanlış tahmin ederseniz , tren durmak, yedeklemek ve yeniden başlamak için çok zaman harcayacaktır.


Bir if ifadesini düşünün: İşlemci düzeyinde, bir dal talimatıdır:

Bir if ifadesi içeren derlenmiş kodun ekran görüntüsü

Siz bir işlemcisiniz ve bir şube görüyorsunuz. Hangi yöne gideceğine dair hiçbir fikrin yok. Ne yaparsın? Yürütmeyi durdurur ve önceki talimatlar tamamlanana kadar beklersiniz. Sonra doğru yolda devam.

Modern işlemciler karmaşıktır ve uzun boru hatlarına sahiptir. Bu yüzden sonsuza dek "ısınmak" ve "yavaşlamak" için uğraşırlar.

Daha iyi bir yol var mı? Şubenin hangi yöne gideceğini tahmin edin!

  • Doğru tahmin ederseniz, yürütmeye devam edersiniz.
  • Yanlış tahmin ederseniz, boru hattını yıkamanız ve şubeye geri dönmeniz gerekir. Ardından diğer yolu yeniden başlatabilirsiniz.

Her seferinde doğru tahmin ederseniz , yürütme asla durmak zorunda kalmayacak.
Çok sık yanlış tahmin ederseniz , durmak, geri dönmek ve yeniden başlatmak için çok zaman harcıyorsunuz.


Bu dal tahmini. Bunun en iyi benzetme olmadığını itiraf ediyorum çünkü tren sadece bir bayrakla yön gösterebiliyordu. Ancak bilgisayarlarda, işlemci bir dalın son ana kadar hangi yöne gideceğini bilmiyor.

Öyleyse, trenin diğer yoldan kaç kez geri gitmesi gerektiğini stratejik olarak tahmin edersiniz? Geçmiş tarihe bakıyorsunuz! Tren zamanın% 99'undan ayrılırsa, o zaman ayrıldınız demektir. Değişiyorsa, tahminlerinizi değiştirirsiniz. Her üç seferde bir şekilde giderse, aynı şeyi tahmin edersiniz ...

Başka bir deyişle, bir deseni belirlemeye ve onu izlemeye çalışırsınız. Şube tahmin edicilerinin çalışma şekli budur.

Uygulamaların çoğunun iyi davranmış dalları vardır. Dolayısıyla, modern şube tahmincileri genellikle% 90'ın üzerinde isabet oranlarına ulaşacaktır. Ancak, tanınabilir paternleri olmayan öngörülemeyen dallarla karşılaşıldığında, dal tahmincileri neredeyse işe yaramaz.

İlave okumalar: Wikipedia'da "Şube öngörücüsü" makalesi .


Yukarıda ima edildiği gibi, suçlu bu if-ifadesidir:

if (data[c] >= 128)
    sum += data[c];

Verilerin 0 ile 255 arasında eşit olarak dağıtıldığına dikkat edin. Veriler sıralandığında kabaca yinelemelerin ilk yarısı if-ifadesine girmez. Bundan sonra hepsi if ifadesine girecek.

Şube defalarca aynı yöne gittiğinden, bu durum şube öngörücüsü için çok uygundur. Basit bir doygunluk sayacı bile, yönü değiştirdikten sonraki birkaç yineleme haricinde dalı doğru şekilde tahmin edecektir.

Hızlı görselleştirme:

T = branch taken
N = branch not taken

data[] = 0, 1, 2, 3, 4, ... 126, 127, 128, 129, 130, ... 250, 251, 252, ...
branch = N  N  N  N  N  ...   N    N    T    T    T  ...   T    T    T  ...

       = NNNNNNNNNNNN ... NNNNNNNTTTTTTTTT ... TTTTTTTTTT  (easy to predict)

Ancak, veriler tamamen rasgele olduğunda, şube öngörücüsü işe yaramaz hale getirilir, çünkü rastgele verileri tahmin edemez. Böylece muhtemelen yaklaşık% 50 yanlış tahmin olacaktır (rastgele tahmin etmekten daha iyi değildir).

data[] = 226, 185, 125, 158, 198, 144, 217, 79, 202, 118,  14, 150, 177, 182, 133, ...
branch =   T,   T,   N,   T,   T,   T,   T,  N,   T,   N,   N,   T,   T,   T,   N  ...

       = TTNTTTTNTNNTTTN ...   (completely random - hard to predict)

Peki ne yapılabilir?

Derleyici, dalı koşullu bir hareketle optimize edemezse, performans için okunabilirliği feda etmek istiyorsanız bazı hack'leri deneyebilirsiniz.

Değiştir:

if (data[c] >= 128)
    sum += data[c];

ile:

int t = (data[c] - 128) >> 31;
sum += ~t & data[c];

Bu, dalı ortadan kaldırır ve bazı bitsel işlemlerle değiştirir.

(Bu kesmek kesinlikle orijinal if-ifadesiyle eşdeğer değildir. Ancak bu durumda, tüm giriş değerleri için geçerlidir data[].)

Deneyler: Core i7 920 @ 3,5 GHz

Visual Studio 2010 - x64 Sürümü

//  Branch - Random
seconds = 11.777

//  Branch - Sorted
seconds = 2.352

//  Branchless - Random
seconds = 2.564

//  Branchless - Sorted
seconds = 2.587

Java - NetBeans 7.1.1 JDK 7 - x64

//  Branch - Random
seconds = 10.93293813

//  Branch - Sorted
seconds = 5.643797077

//  Branchless - Random
seconds = 3.113581453

//  Branchless - Sorted
seconds = 3.186068823

Gözlemler:

  • Şube ile: Sıralanan ve sıralanmamış veriler arasında büyük bir fark vardır.
  • Hack ile: Sıralanmış ve sıralanmamış veriler arasında fark yoktur.
  • C ++ durumunda, veri sıralandığında kesmek aslında daldan biraz daha yavaştır.

Genel bir kural, kritik döngülerde (bu örnekte olduğu gibi) verilere bağlı dallanmadan kaçınmaktır.


Güncelleme:

  • X64 ile -O3veya -ftree-vectorizeüzerinde GCC 4.6.1, koşullu bir hareket oluşturabilir. Dolayısıyla, sıralanan ve sıralanmamış veriler arasında fark yoktur - her ikisi de hızlıdır.

    (Ya da biraz hızlı: zaten sıralanmış durum için, cmovözellikle GCC bunu sadece yerine kritik yola koyarsa add, özellikle cmov2 döngü gecikmesi olan Broadwell'den önce Intel'e daha yavaş olabilir : gcc optimizasyon bayrağı -O3 kodu -O2'den daha yavaş yapar )

  • VC ++ 2010, bu dal için altında bile koşullu hareketler oluşturamaz /Ox.

  • Intel C ++ Derleyici (ICC) 11 mucizevi bir şey yapar. Bu iki döngü alışverişini sağlar , böylece dış döngüye öngörülemeyen dalı kaldırma. Bu yüzden sadece yanlış tahminlere karşı bağışık değil, aynı zamanda VC ++ ve GCC'nin üretebildiği her şeyin iki katı daha hızlı! Başka bir deyişle, ICC karşılaştırmayı yenmek için test döngüsünün avantajlarından yararlandı ...

  • Intel derleyicisine dalsız kod verirseniz, tam olarak sağda vektörleştirir ... ve dalda olduğu kadar hızlıdır (döngü değişimli).

Bu, olgun modern derleyicilerin bile kodu optimize etme yeteneklerinde çılgınca değişebileceğini gösteriyor ...


256
Şu soruya bir göz atın: stackoverflow.com/questions/11276291/… Intel Derleyici, dış döngüden tamamen kurtulmaya oldukça yaklaştı.
Mysticial

24
@Mysticial Tren / derleyici yanlış yola girdiğini nasıl biliyor?
onmyway133

26
@obe: Hiyerarşik bellek yapıları göz önüne alındığında, bir önbellek özledim masrafının ne olacağını söylemek mümkün değildir. L1'de özlüyor olabilir ve daha yavaş L2'de çözülebilir veya L3'te özür ve sistem belleğinde çözülebilir. Ancak, garip bir nedenden ötürü, bu önbellek kaçırma yerleşik olmayan bir sayfadaki belleğin diskten yüklenmesine neden olmadıkça, iyi bir noktaya sahipsiniz ... belleğin yaklaşık 25-30 yıl içinde milisaniye aralığında erişim süresi olmamıştır. ;)
Andon M. Coleman

21
Modern bir işlemcide verimli kod yazma için temel kural : Programınızın yürütülmesini daha düzenli (daha az düzensiz) yapan her şey onu daha verimli hale getirme eğiliminde olacaktır. Bu örnekteki sıralama, şube tahmini nedeniyle bu etkiye sahiptir. Erişim konumu (uzak ve geniş rasgele erişimler yerine) önbellekler nedeniyle bu etkiye sahiptir.
Lutz Prechelt

22
@Sandeep Evet. İşlemcilerin hala şube tahmini var. Bir şey değiştiyse, derleyiciler. Bugünlerde, ICC ve GCC'nin (-O3 altında) burada yaptıklarını yapma olasılıklarının daha yüksek olduğuna eminim - yani şubeyi kaldırın. Bu sorunun ne kadar yüksek profilli olduğu göz önüne alındığında, derleyicilerin bu sorudaki özel durum için özel olarak güncellenmesi çok olasıdır. Kesinlikle SO dikkat edin. Ve GCC'nin 3 hafta içinde güncellendiği bu soruda oldu. Burada da neden olmayacağını anlamıyorum.
Mistik

4086

Şube tahmini.

Sıralı bir dizide, data[c] >= 128ilk önce falsebir değer dizisi için koşul daha sonra truetüm sonraki değerler için olur . Bunu tahmin etmek kolay. Sıralanmamış bir dizi ile, dallanma maliyetini ödersiniz.


105
Şube tahmini, sıralı diziler ve farklı desenlere sahip diziler üzerinde daha iyi çalışır mı? Örneğin, -> {10, 5, 20, 10, 40, 20, ...} dizisi için dizideki bir sonraki öğenin desenini 80 olur. Bu tür bir dizi, eğer desen takip edilirse bir sonraki eleman 80'dir? Yoksa genellikle sıralı dizilere yardımcı olur mu?
Adam Freeman

133
Temelde big-O hakkında geleneksel olarak öğrendiğim her şey pencereden mi çıkıyor? Bir sıralama maliyetine dallanma maliyetinden daha mı iyi?
Agrim Pathak

133
@AgrimPathak Bu duruma bağlı. Çok büyük olmayan girdiler için, daha yüksek karmaşıklığa sahip bir algoritma, daha yüksek karmaşıklığa sahip algoritma için sabitler daha küçük olduğunda, daha düşük karmaşıklığa sahip bir algoritmadan daha hızlıdır. Başabaş noktasının nerede olduğunu tahmin etmek zor olabilir. Ayrıca, bunu karşılaştırın , yerellik önemlidir. Big-O önemlidir, ancak performans için tek kriter bu değildir.
Daniel Fischer

65
Şube tahmini ne zaman gerçekleşir? Dil dizinin sıralandığını ne zaman bilecek? Şuna benzeyen dizinin durumunu düşünüyorum: [1,2,3,4,5, ... 998,999,1000, 3, 10001, 10002]? Bu karanlık 3 çalışma süresini artıracak mı? Sıralanmamış dizi kadar uzun olacak mı?
Filip Bartuzi

63
@FilipBartuzi Şube tahmini işlemcide, dil seviyesinin altında gerçekleşir (ancak dil derleyiciye neyin olası olduğunu söyleme yolları sunabilir, böylece derleyici buna uygun kod yayabilir). Örneğinizde, sıra dışı 3 bir şube yanlış tahminine yol açacaktır (3 için 1000'den farklı bir sonuç veren uygun koşullar için) ve böylece bu dizinin işlenmesi muhtemelen bir düzine veya yüz nanosaniye daha uzun sürecektir. sıralı dizi, neredeyse hiç farkedilir. Ne zaman maliyeti yüksek ben yanlış tahmin oranı, 1000 başına bir yanlış tahmin çok değil.
Daniel Fischer

3310

Veriler sıralandığında performansın önemli ölçüde iyileşmesinin nedeni, Mysticial'ın cevabında güzel açıklandığı gibi dal tahmin cezasının kaldırılmasıdır .

Şimdi, koda bakarsak

if (data[c] >= 128)
    sum += data[c];

bu if... else...dalın anlamının, bir koşul yerine getirildiğinde bir şeyler eklemek olduğunu bulabiliriz . Bu dal türü, bir sistemde koşullu bir hareket yönergesine derlenecek olan koşullu bir hareket ifadesine kolayca dönüştürülebilir . Şube ve dolayısıyla potansiyel şube tahmin cezası kaldırılır.cmovlx86

In C, böylece C++, koşullu taşıma talimatı içine (herhangi bir optimizasyon olmadan) doğrudan derlemek istiyorum açıklamada, x86, üçlü operatördür ... ? ... : .... Bu yüzden yukarıdaki ifadeyi eşdeğer bir ifadeye yeniden yazıyoruz:

sum += data[c] >=128 ? data[c] : 0;

Okunabilirliği korurken hız faktörünü kontrol edebiliriz.

Intel Core i7 -2600K @ 3.4 GHz ve Visual Studio 2010 Sürüm Modunda, karşılaştırma ölçütü (format Mysticial'dan kopyalanmıştır):

x86

//  Branch - Random
seconds = 8.885

//  Branch - Sorted
seconds = 1.528

//  Branchless - Random
seconds = 3.716

//  Branchless - Sorted
seconds = 3.71

x64

//  Branch - Random
seconds = 11.302

//  Branch - Sorted
 seconds = 1.830

//  Branchless - Random
seconds = 2.736

//  Branchless - Sorted
seconds = 2.737

Sonuç, çoklu testlerde sağlamdır. Şube sonucu tahmin edilemediğinde büyük bir hız kazanırız, ancak tahmin edilebilir olduğunda biraz acı çekeriz. Aslında, koşullu bir hareket kullanırken, veri modelinden bağımsız olarak performans aynıdır.

Şimdi x86oluşturdukları montajı araştırarak daha yakından bakalım . Basitlik için iki işlev kullanıyoruz max1ve max2.

max1koşullu dalı kullanır if... else ...:

int max1(int a, int b) {
    if (a > b)
        return a;
    else
        return b;
}

max2üçlü operatörü kullanır ... ? ... : ...:

int max2(int a, int b) {
    return a > b ? a : b;
}

X86-64 makinesinde GCC -Saşağıdaki montajı oluşturur.

:max1
    movl    %edi, -4(%rbp)
    movl    %esi, -8(%rbp)
    movl    -4(%rbp), %eax
    cmpl    -8(%rbp), %eax
    jle     .L2
    movl    -4(%rbp), %eax
    movl    %eax, -12(%rbp)
    jmp     .L4
.L2:
    movl    -8(%rbp), %eax
    movl    %eax, -12(%rbp)
.L4:
    movl    -12(%rbp), %eax
    leave
    ret

:max2
    movl    %edi, -4(%rbp)
    movl    %esi, -8(%rbp)
    movl    -4(%rbp), %eax
    cmpl    %eax, -8(%rbp)
    cmovge  -8(%rbp), %eax
    leave
    ret

max2talimat kullanımı nedeniyle çok daha az kod kullanır cmovge. Ancak asıl kazanç, tahmin edilen sonuç doğru değilse önemli bir performans cezasına sahip olacak olan max2şube sıçramalarını jmpiçermemesidir.

Peki koşullu bir hareket neden daha iyi performans gösteriyor?

Tipik bir x86işlemcide, bir komutun yürütülmesi birkaç aşamaya ayrılır. Kabaca, farklı aşamalarla başa çıkmak için farklı donanımlarımız var. Bu nedenle, yeni bir komut başlatmak için bir komutun bitmesini beklemek zorunda değiliz. Buna boru hattı denir .

Bir şube durumunda, aşağıdaki talimat bir öncekiyle belirlenir, bu nedenle boru hattı yapamayız. Beklemek ya da tahmin etmek zorundayız.

Koşullu bir hareket durumunda, yürütme koşullu hareket talimatı birkaç aşamaya ayrılır, ancak önceki aşamalar önceki talimatın sonucunu beğenir Fetchve Decodebuna bağlı değildir; sadece son aşamalar sonuca ihtiyaç duyar. Bu nedenle, bir talimatın yürütme süresinin bir kısmını bekleriz. Bu nedenle, tahmin kolay olduğunda koşullu taşıma sürümü daldan daha yavaştır.

Bilgisayar Sistemleri: Bir Programcı'nın Perspektifi, ikinci baskı kitabı bunu ayrıntılı olarak açıklamaktadır. Sen için Bölüm 3.6.6 kontrol edebilirsiniz Şartlı Taşı Talimatları için tüm Bölüm 4 İşlemci Mimarisi için özel bir tedavi için, ve Bölüm 5.11.2 Şube Prediction ve Misprediction Cezaları .

Bazen, bazı modern derleyiciler kodumuzu daha iyi performansla montaj için optimize edebilir, bazen bazı derleyiciler yapamaz (söz konusu kod Visual Studio'nun yerel derleyicisini kullanıyor). Öngörülemediğinde dal ve koşullu hareket arasındaki performans farkını bilmek, senaryo o kadar karmaşık hale geldiğinde daha iyi performansa sahip kod yazmamıza yardımcı olabilir, derleyici bunları otomatik olarak optimize edemez.


7
@ BlueRaja-DannyPflughoeft Bu optimize edilmemiş versiyon. Derleyici üçlü operatörü optimize etmedi, sadece ÇEVİRİN. GCC eğer yeterli optimizasyon seviyesi verilirse optimizasyon yapabilir, yine de bu koşullu hareketin gücünü gösterir ve manuel optimizasyon fark yaratır.
WiSaGaN

100
@WiSaGaN Kod hiçbir şey göstermiyor, çünkü iki kod parçanız aynı makine kodunu derliyor. İnsanların, örneğinizdeki if ifadesinin bir şekilde örneğinizdeki terenary'den farklı olduğu fikrini almaması kritik önem taşır. Son paragrafınızdaki benzerliğe sahip olduğunuz doğrudur, ancak bu, örneğin geri kalanının zararlı olduğu gerçeğini silmez.
Justin L.

55
Sorunuza cevap yanıltıcı kaldırmak üzere modifiye eğer @WiSaGaN Benim downvote kesinlikle upvote dönüşeceğini -O0örnek ve farkını göstermek için optimize senin iki testcases üzerinde asm.
Justin L.

56
@UpAndAdam Test anında VS2010, orijinal optimizasyonu yüksek optimizasyon seviyesi belirtirken bile koşullu bir harekete optimize edemezken, gcc yapabilir.
WiSaGaN

9
Bu üçlü operatör hilesi Java için güzel çalışıyor. Mystical'in cevabını okuduktan sonra, Java'nın -O3'e eşdeğer bir şeyi olmadığından yanlış şube tahmininden kaçınmak için Java için neler yapılabileceğini merak ediyordum. üçlü operatör: 2.1943s ve orijinal: 6.0303s.
Kin Cheung

2271

Bu kodda yapılabilecek daha fazla optimizasyon merak ediyorsanız, şunları göz önünde bulundurun:

Orijinal döngü ile başlayarak:

for (unsigned i = 0; i < 100000; ++i)
{
    for (unsigned j = 0; j < arraySize; ++j)
    {
        if (data[j] >= 128)
            sum += data[j];
    }
}

Döngü değişimi ile bu döngüyü güvenli bir şekilde değiştirebiliriz:

for (unsigned j = 0; j < arraySize; ++j)
{
    for (unsigned i = 0; i < 100000; ++i)
    {
        if (data[j] >= 128)
            sum += data[j];
    }
}

Ardından, ifkoşulun idöngünün yürütülmesi boyunca sabit olduğunu görebilirsiniz, böylece ifdışarı çekebilirsiniz :

for (unsigned j = 0; j < arraySize; ++j)
{
    if (data[j] >= 128)
    {
        for (unsigned i = 0; i < 100000; ++i)
        {
            sum += data[j];
        }
    }
}

Ardından, kayan nokta modelinin izin verdiği varsayılarak ( /fp:fastörneğin atılır) iç döngünün tek bir ifadeye daraltılabileceğini görürsünüz.

for (unsigned j = 0; j < arraySize; ++j)
{
    if (data[j] >= 128)
    {
        sum += data[j] * 100000;
    }
}

Bu, öncekinden 100.000 kat daha hızlı.


276
Hile yapmak isterseniz, çarpmayı döngü dışında da alabilir ve döngüden sonra toplam * = 100000 yapabilirsiniz.
Jyaif

78
@Michael - Bu örneğin aslında döngü değişmez kaldırma (LIH) optimizasyonu ve döngü döngü takas örneği olduğuna inanıyorum . Bu durumda, tüm iç döngü dış döngüden bağımsızdır ve bu nedenle dış döngüden kaldırılabilir, bunun üzerine sonuç basitçe ibir birimin toplamı = 1e5 ile çarpılır. Sonuçta hiçbir fark yaratmaz, ancak sadece bu kadar sık ​​kullanılan bir sayfa olduğu için kaydı düz ayarlamak istedim.
Yair Altman

54
Döngüleri değiştirmenin basit ruhu içinde olmasa da if, bu noktada iç sum += (data[j] >= 128) ? data[j] * 100000 : 0;kısma dönüştürülebilir: derleyicinin azaltabileceği cmovgeveya eşdeğer olabileceği .
Alex North-Keys

43
Dış döngü, iç döngü tarafından harcanan süreyi profile göre yeterince büyük hale getirmektir. Öyleyse neden değiş tokuş yapasın Sonunda, bu döngü yine de kaldırılacak.
saurabheights

34
@saurabheights: Yanlış soru: Derleyici neden döngü değiştirmiyor? Microbenchmarks zordur;)
Matthieu M.

1884

Şüphesiz, bazılarımız CPU'nun şube tahmincisi için sorunlu olan kodu tanımlama yollarıyla ilgileniriz. Valgrind aracı cachegrind, --branch-sim=yesbayrak kullanılarak etkinleştirilen bir dal tahmincisi simülatörüne sahiptir . Bu sorudaki örneklerin üzerinden geçmek, dış döngülerin sayısı 10000'e düşürülmüş ve derlenmiş olarak g++şu sonuçları verir:

sıralama:

==32551== Branches:        656,645,130  (  656,609,208 cond +    35,922 ind)
==32551== Mispredicts:         169,556  (      169,095 cond +       461 ind)
==32551== Mispred rate:            0.0% (          0.0%     +       1.2%   )

Sınıflandırılmamış:

==32555== Branches:        655,996,082  (  655,960,160 cond +  35,922 ind)
==32555== Mispredicts:     164,073,152  (  164,072,692 cond +     460 ind)
==32555== Mispred rate:           25.0% (         25.0%     +     1.2%   )

cg_annotateSöz konusu döngü için gördüğümüz satır satır çıkışın ayrıntılarına inmek :

sıralama:

          Bc    Bcm Bi Bim
      10,001      4  0   0      for (unsigned i = 0; i < 10000; ++i)
           .      .  .   .      {
           .      .  .   .          // primary loop
 327,690,000 10,016  0   0          for (unsigned c = 0; c < arraySize; ++c)
           .      .  .   .          {
 327,680,000 10,006  0   0              if (data[c] >= 128)
           0      0  0   0                  sum += data[c];
           .      .  .   .          }
           .      .  .   .      }

Sınıflandırılmamış:

          Bc         Bcm Bi Bim
      10,001           4  0   0      for (unsigned i = 0; i < 10000; ++i)
           .           .  .   .      {
           .           .  .   .          // primary loop
 327,690,000      10,038  0   0          for (unsigned c = 0; c < arraySize; ++c)
           .           .  .   .          {
 327,680,000 164,050,007  0   0              if (data[c] >= 128)
           0           0  0   0                  sum += data[c];
           .           .  .   .          }
           .           .  .   .      }

Bu, sorunlu satırı kolayca tanımlamanıza olanak tanır - sıralanmamış sürümde, if (data[c] >= 128)satır önbellek dalını Bcmöngörme modeli altında 164,050,007 yanlış öngörülen koşullu dallara ( ) neden olurken, sıralı sürümde yalnızca 10,006'ya neden olur.


Alternatif olarak, Linux'ta aynı görevi gerçekleştirmek için performans sayaçları alt sistemini kullanabilirsiniz, ancak CPU sayaçlarını kullanan yerel performansla.

perf stat ./sumtest_sorted

sıralama:

 Performance counter stats for './sumtest_sorted':

  11808.095776 task-clock                #    0.998 CPUs utilized          
         1,062 context-switches          #    0.090 K/sec                  
            14 CPU-migrations            #    0.001 K/sec                  
           337 page-faults               #    0.029 K/sec                  
26,487,882,764 cycles                    #    2.243 GHz                    
41,025,654,322 instructions              #    1.55  insns per cycle        
 6,558,871,379 branches                  #  555.455 M/sec                  
       567,204 branch-misses             #    0.01% of all branches        

  11.827228330 seconds time elapsed

Sınıflandırılmamış:

 Performance counter stats for './sumtest_unsorted':

  28877.954344 task-clock                #    0.998 CPUs utilized          
         2,584 context-switches          #    0.089 K/sec                  
            18 CPU-migrations            #    0.001 K/sec                  
           335 page-faults               #    0.012 K/sec                  
65,076,127,595 cycles                    #    2.253 GHz                    
41,032,528,741 instructions              #    0.63  insns per cycle        
 6,560,579,013 branches                  #  227.183 M/sec                  
 1,646,394,749 branch-misses             #   25.10% of all branches        

  28.935500947 seconds time elapsed

Ayrıca sökme ile kaynak kodu ek açıklama yapabilir.

perf record -e branch-misses ./sumtest_unsorted
perf annotate -d sumtest_unsorted
 Percent |      Source code & Disassembly of sumtest_unsorted
------------------------------------------------
...
         :                      sum += data[c];
    0.00 :        400a1a:       mov    -0x14(%rbp),%eax
   39.97 :        400a1d:       mov    %eax,%eax
    5.31 :        400a1f:       mov    -0x20040(%rbp,%rax,4),%eax
    4.60 :        400a26:       cltq   
    0.00 :        400a28:       add    %rax,-0x30(%rbp)
...

Daha fazla ayrıntı için performans eğitimine bakın.


74
Bu korkutucu, sıralanmamış listede,% 50 eklenti isabet şansı olmalıdır. Her nasılsa, şube tahmini sadece% 25'lik bir eksiklik oranına sahipse,% 50'den fazla özledim nasıl daha iyi olabilir?
TallBrian

128
@ tall.b.lo:% 25 tüm dallardan oluşuyor - döngüde iki dal var , biri için data[c] >= 128(önerdiğiniz gibi% 50'lik bir özledim oranına sahip) ve bir tanesi c < arraySize~ % 0'lık eksiklik oranına sahip döngü koşulu için .
caf

1340

Bu soruyu ve cevaplarını okudum ve bir cevabın eksik olduğunu hissediyorum.

Yönetilen dillerde özellikle iyi çalıştığını bulduğum şube tahminini ortadan kaldırmanın yaygın bir yolu, bir şube kullanmak yerine bir tablo aramasıdır (bu durumda test etmedim).

Bu yaklaşım genel olarak şu durumlarda işe yarar:

  1. bu küçük bir tablo ve muhtemelen işlemcide önbelleğe alınacak ve
  2. işleri oldukça sıkı bir döngüde çalıştırıyorsunuz ve / veya işlemci verileri önceden yükleyebilir.

Arka plan ve neden

İşlemci açısından belleğiniz yavaş. Hız farkını telafi etmek için işlemcinize birkaç önbellek yerleştirilmiştir (L1 / L2 önbellek). Güzel hesaplamalar yaptığınızı düşünün ve bir hafızaya ihtiyacınız olduğunu anlayın. İşlemci 'yükleme' işlemini alacak ve bellek parçasını önbelleğe yükleyecektir - ve sonra diğer hesaplamaları yapmak için önbelleği kullanacaktır. Bellek nispeten yavaş olduğundan, bu 'yükleme' programınızı yavaşlatır.

Şube tahmini gibi, bu da Pentium işlemcilerde optimize edildi: işlemci bir parça veri yüklemesi gerektiğini tahmin ediyor ve işlem aslında önbelleğe çarpmadan önce önbelleğe yüklemeye çalışıyor. Daha önce gördüğümüz gibi, şube tahmini bazen korkunç bir şekilde yanlış gidiyor - en kötü senaryoda geri dönmeniz ve aslında sonsuza kadar sürecek bir bellek yükü beklemeniz gerekir ( diğer bir deyişle: başarısız şube tahmini kötüdür, bir bellek bir şube tahmin başarısız sonra yük sadece korkunç! ).

Neyse ki bizim için, bellek erişim modeli tahmin edilebilirse, işlemci onu hızlı önbelleğine yükleyecek ve her şey yolunda.

Bilmemiz gereken ilk şey küçük olan nedir? Daha küçük olmak genellikle daha iyi olmakla birlikte, genel kural <= 4096 bayt boyutundaki arama tablolarına bağlı kalmaktır. Bir üst sınır olarak: arama tablonuz 64K'dan büyükse, yeniden düşünmeye değer.

Bir tablo oluşturma

Böylece küçük bir masa oluşturabileceğimizi anladık. Yapılacak bir sonraki şey, bir arama işlevini yerine yerleştirmektir. Arama işlevleri genellikle birkaç temel tamsayı işlemi kullanan küçük işlevlerdir (ve, veya xor, shift, add, remove ve belki çarpma). Girişinizin arama işlevi tarafından tablonuzdaki bir tür 'benzersiz anahtara' çevrilmesini istiyorsanız, bu size basitçe yapmasını istediğiniz tüm işlerin cevabını verir.

Bu durumda:> = 128, değeri koruyabileceğimiz anlamına gelir; <128, ondan kurtulacağımız anlamına gelir. Bunu yapmanın en kolay yolu bir 'AND' kullanmaktır: eğer tutarsak, 7FFFFFFF ile AND; ondan kurtulmak istiyorsak, VE ile 0 oluruz. 128'in 2 gücü olduğuna dikkat edin - böylece devam edip 32768/128 tamsayılardan oluşan bir tablo oluşturabilir ve bir sıfır ve bir sürü ile doldurabiliriz. 7FFFFFFFF en.

Yönetilen diller

Bunun neden yönetilen dillerde iyi çalıştığını merak edebilirsiniz. Sonuçta, yönetilen diller, karışıklıktan kaçınmak için dizilerin sınırlarını bir şube ile kontrol eder ...

Tam olarak değil ... :-)

Bu şubeyi yönetilen diller için ortadan kaldırma konusunda oldukça fazla çalışma yapılmıştır. Örneğin:

for (int i = 0; i < array.Length; ++i)
{
   // Use array[i]
}

Bu durumda, derleyici için sınır koşuluna asla çarpılmayacağı açıktır. En azından Microsoft JIT derleyicisi (ancak Java'nın benzer şeyler yapmasını bekliyorum) bunu fark edecek ve kontrolü tamamen kaldıracaktır. WOW, bu şube yok demektir. Benzer şekilde, diğer bariz durumlarla da ilgilenecektir.

Yönetilen dillerdeki aramalarla ilgili sorun yaşarsanız - anahtar, & 0x[something]FFFsınır kontrolünü öngörülebilir hale getirmek için arama işlevinize bir ekleme yapmak ve daha hızlı ilerlemesini izlemek.

Bu davanın sonucu

// Generate data
int arraySize = 32768;
int[] data = new int[arraySize];

Random random = new Random(0);
for (int c = 0; c < arraySize; ++c)
{
    data[c] = random.Next(256);
}

/*To keep the spirit of the code intact, I'll make a separate lookup table
(I assume we cannot modify 'data' or the number of loops)*/

int[] lookup = new int[256];

for (int c = 0; c < 256; ++c)
{
    lookup[c] = (c >= 128) ? c : 0;
}

// Test
DateTime startTime = System.DateTime.Now;
long sum = 0;

for (int i = 0; i < 100000; ++i)
{
    // Primary loop
    for (int j = 0; j < arraySize; ++j)
    {
        /* Here you basically want to use simple operations - so no
        random branches, but things like &, |, *, -, +, etc. are fine. */
        sum += lookup[data[j]];
    }
}

DateTime endTime = System.DateTime.Now;
Console.WriteLine(endTime - startTime);
Console.WriteLine("sum = " + sum);
Console.ReadLine();

57
Şube tahmincisini atlamak istiyorsunuz, neden? Bu bir optimizasyon.
Dustin Oprea

108
Hiçbir dal bir daldan daha iyi olmadığı için :-) Pek çok durumda bu çok daha hızlıdır ... eğer optimizasyon yapıyorsanız, kesinlikle denemeye değer. Ayrıca f.ex'te biraz kullanıyorlar. graphics.stanford.edu/~seander/bithacks.html
atlaste

36
Genel olarak arama tabloları hızlı olabilir, ancak bu özel durum için testleri yaptınız mı? Kodunuzda hala bir şube koşulunuz olacak, ancak şimdi arama tablosu oluşturma bölümüne taşındı. Hala mükemmel desteğini alamazsın
Zain Rizvi

38
@Zain gerçekten bilmek istiyorsanız ... Evet: şubeyle 15 saniye ve sürümümle 10 saniye. Ne olursa olsun, her iki şekilde de bilmek yararlı bir tekniktir.
atlaste

42
Neden 256 girişi olan bir dizi sum += lookup[data[j]]nerede lookup, birincisi sıfır ve sonuncusu dizine eşit değil?
Kris Vandermotten

1200

Dizi sıralandığında veriler 0 ile 255 arasında dağıtıldığından, yinelemelerin ilk yarısında if-statement girilmez ( ififade aşağıda paylaşılır).

if (data[c] >= 128)
    sum += data[c];

Soru şudur: Yukarıdaki ifadenin, sıralı verilerde olduğu gibi bazı durumlarda yürütülmemesini sağlayan nedir? İşte "dal tahmincisi". Dal tahmincisi, bir dalın (örneğin bir if-then-elseyapı) kesin olarak bilinmeden önce hangi yöne gideceğini tahmin etmeye çalışan dijital bir devredir . Şube öngörücüsünün amacı, talimat boru hattındaki akışı iyileştirmektir. Şube tahmin edicileri, yüksek etkili performans elde etmede kritik bir rol oynamaktadır!

Daha iyi anlamak için bazı işaretleme yapalım

Bir ifdurumun performansı , durumunun öngörülebilir bir desene sahip olup olmadığına bağlıdır. Koşul her zaman doğru veya her zaman yanlışsa, işlemcideki dal tahmin mantığı deseni alır. Öte yandan, eğer desen öngörülemezse, if-taşaması çok daha pahalı olacaktır.

Bu döngünün performansını farklı koşullarla ölçelim:

for (int i = 0; i < max; i++)
    if (condition)
        sum++;

Farklı gerçek-yanlış kalıpları ile döngü zamanlamaları şunlardır:

Condition                Pattern             Time (ms)
-------------------------------------------------------
(i & 0×80000000) == 0    T repeated          322

(i & 0xffffffff) == 0    F repeated          276

(i & 1) == 0             TF alternating      760

(i & 3) == 0             TFFFTFFF           513

(i & 2) == 0             TTFFTTFF           1675

(i & 4) == 0             TTTTFFFFTTTTFFFF   1275

(i & 8) == 0             8T 8F 8T 8F        752

(i & 16) == 0            16T 16F 16T 16F    490

Kötü ” gerçek-yanlış kalıp,ifiyi ” kalıptan altı kata kadar daha yavaş ! Tabii ki, hangi model iyi ve kötü olan, derleyici ve belirli işlemci tarafından oluşturulan kesin talimatlara bağlıdır.

Dolayısıyla, şube tahmininin performans üzerindeki etkisi hakkında hiç şüphe yoktur!


23
@MooingDuck 'Çünkü bir fark yaratmaz - bu değer herhangi bir şey olabilir, ancak yine de bu eşiklerin sınırları içinde olacaktır. Peki, sınırları zaten bildiğinizde neden rastgele bir değer gösteresiniz? Her ne kadar bir bütünlük adına ve 'sadece onun için' bir tane gösterebileceğinizi kabul etsem de.
cst1992

24
@ cst1992: Şu anda en yavaş zamanlaması TTFFTTFFTTFF, insan gözüme göre oldukça tahmin edilebilir. Rastgele doğal olarak öngörülemez, bu yüzden hala daha yavaş ve dolayısıyla burada gösterilen sınırların dışında olması tamamen mümkündür. OTOH, TTFFTTFF'nin patolojik duruma mükemmel bir şekilde çarpması olabilir. Söyleyemiyorum, çünkü rastgele zamanlamaları göstermedi.
Mooing Duck

21
@MooingDuck Bir insan gözü için "TTFFTTFFTTFF" tahmin edilebilir bir dizidir, ancak burada bahsettiğimiz şey, bir CPU içine yerleştirilen dal tahmincisinin davranışıdır. Dal tahmincisi AI düzeyinde örüntü tanıma değildir; çok basit. Sadece dalları değiştirdiğinizde iyi tahmin edemezsiniz. Çoğu kodda, dallar neredeyse her zaman aynı şekilde gider; bin defa çalışan bir döngü düşünün. Döngünün sonundaki dal, 999 kez döngünün başlangıcına geri döner ve sonra bininci kez farklı bir şey yapar. Çok basit bir dal tahmincisi genellikle iyi çalışır.
steveha

18
@steveha: Sanırım CPU şube tahmin edicisinin nasıl çalıştığı hakkında varsayımlar yapıyorsunuz ve bu metodolojiye katılmıyorum. Şube tahmincisinin ne kadar gelişmiş olduğunu bilmiyorum, ama sanırım sizden çok daha gelişmiş. Muhtemelen haklısınız, ancak ölçümler kesinlikle iyi olurdu.
Mooing Duck

5
@steveha: İki seviyeli uyarlanabilir öngörücü, TTFFTTFF modeline hiçbir sorun olmadan kilitlenebilir. "Bu tahmin yönteminin varyantları çoğu modern mikroişlemcide kullanılmaktadır". Yerel şube tahmini ve Global şube tahmini iki seviyeli uyarlanabilir bir öngörücüye dayanır, onlar da yapabilir. "Global dal tahmini, AMD işlemcilerinde ve Intel Pentium M, Core, Core 2 ve Silvermont tabanlı Atom işlemcilerinde kullanılıyor" Ayrıca, bu listeye Agre öngörücüsü, Hibrit öngörücü, dolaylı atlamaların öngörülmesini ekleyin. Döngü belirleyici kilitlenmeyecek, ancak% 75'e ulaşacak. Bu sadece kilitlenemeyen 2 bırakır
Mooing Duck

1126

Şube tahmin hatalarından kaçınmanın bir yolu, bir arama tablosu oluşturmak ve verileri kullanarak dizine eklemektir. Stefan de Bruijn bunu cevabında tartıştı.

Ancak bu durumda, değerlerin [0, 255] aralığında olduğunu biliyoruz ve yalnızca> = 128 değerlerini önemsiyoruz. Bu, bize bir değer isteyip istemediğimizi söyleyecek tek bir biti kolayca ayıklayabileceğimiz anlamına gelir: verileri sağ 7 bite bırakırsak, 0 bit veya 1 bit kaldı ve yalnızca 1 bitimiz olduğunda değeri eklemek istiyoruz. Bu biti "karar biti" olarak adlandıralım.

Karar bitinin 0/1 değerini bir dizine dizin olarak kullanarak, verilerin sıralanıp sıralanmadığına bakılmaksızın eşit derecede hızlı kod oluşturabiliriz. Kodumuz her zaman bir değer katar, ancak karar biti 0 olduğunda, değeri umursadığımız bir yere ekleriz. İşte kod:

// Test
clock_t start = clock();
long long a[] = {0, 0};
long long sum;

for (unsigned i = 0; i < 100000; ++i)
{
    // Primary loop
    for (unsigned c = 0; c < arraySize; ++c)
    {
        int j = (data[c] >> 7);
        a[j] += data[c];
    }
}

double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;
sum = a[1];

Bu kod, eklentilerin yarısını boşa harcar, ancak hiçbir zaman dal tahmini hatası yoktur. Rastgele verilerde, gerçek bir if ifadesine sahip sürümden çok daha hızlıdır.

Ancak testlerimde, açık bir arama tablosu bundan biraz daha hızlıydı, muhtemelen bir arama tablosuna endeksleme, bit kaydırma işleminden biraz daha hızlı olduğu için. Bu, lutkodumun nasıl ayarlandığını ve arama tablosunu nasıl kullandığını gösterir ( kodda düşünülmeden "Arama Tablosu" olarak adlandırılır ). İşte C ++ kodu:

// Declare and then fill in the lookup table
int lut[256];
for (unsigned c = 0; c < 256; ++c)
    lut[c] = (c >= 128) ? c : 0;

// Use the lookup table after it is built
for (unsigned i = 0; i < 100000; ++i)
{
    // Primary loop
    for (unsigned c = 0; c < arraySize; ++c)
    {
        sum += lut[data[c]];
    }
}

Bu durumda, arama tablosu sadece 256 bayttı, bu yüzden bir önbelleğe güzel oturuyor ve hepsi hızlıydı. Veriler 24 bitlik değerler olsaydı ve sadece yarısını isteseydik bu teknik işe yaramazdı ... arama tablosu pratik olamayacak kadar büyük olurdu. Öte yandan, yukarıda gösterilen iki tekniği birleştirebiliriz: önce bitleri kaydırın, sonra bir arama tablosunu dizinleyin. Yalnızca ilk yarı değeri istediğimiz 24 bitlik bir değer için, verileri potansiyel olarak 12 bit sağa kaydırabilir ve tablo dizini için 12 bitlik bir değerle bırakabiliriz. 12 bit tablo dizini, pratik olabilecek 4096 değerinde bir tablo anlamına gelir.

ifHangi işaretçiyi kullanacağına karar vermek için, ifade kullanmak yerine diziye indeksleme tekniği kullanılabilir. İkili ağaçlar uygulayan bir kütüphane gördüm ve iki adlandırılmış işaretçi ( pLeftve pRightveya her ne olursa olsun) bir uzunluk-2 işaretçi dizisi vardı ve hangisini izleyeceğine karar vermek için "karar biti" tekniğini kullandım. Örneğin:

if (x < node->value)
    node = node->pLeft;
else
    node = node->pRight;

bu kütüphane şöyle bir şey yapar:

i = (x < node->value);
node = node->link[i];

İşte bu koda bir bağlantı: Kırmızı Siyah Ağaçlar , Eternally Confuzzled


29
Doğru, sadece biti doğrudan kullanabilir ve çarpabilirsiniz ( data[c]>>7- burada da bir yerde tartışılır); Bu çözümü kasten dışarıda bıraktım, ama elbette haklısın. Sadece küçük bir not: Arama tabloları için temel kural, 4KB'ye (önbellek nedeniyle) sığarsa işe yarayacağıdır - tercihen tabloyu mümkün olduğunca küçük hale getirmektir. Yönetilen diller için bunu 64 KB'ye iterdim, C ++ ve C gibi düşük seviyeli diller için muhtemelen tekrar düşünürdüm (bu sadece benim deneyimim). O zamandan beri typeof(int) = 4, en fazla 10 bite geçmeyi denerdim.
atlaste

17
Bence 0/1 değeri ile indeksleme muhtemelen bir tamsayı çarpımından daha hızlı olacaktır, ancak performans gerçekten kritikse profil yapmalısınız sanırım. Önbellek baskısından kaçınmak için küçük arama tablolarının gerekli olduğunu kabul ediyorum, ancak açıkça daha büyük bir önbelleğe sahipseniz, daha büyük bir arama tablosuyla kurtulabilirsiniz, bu nedenle 4KB zor bir kuraldan daha önemli bir kuraldır. Sanırım demek istedin sizeof(int) == 4? Bu 32 bit için geçerli olurdu. İki yaşındaki cep telefonumun 32KB L1 önbelleği var, bu yüzden 4K arama tablosu bile, özellikle arama değerleri int yerine bir bayt ise çalışabilir.
steveha

12
Muhtemelen bir şey eksik ama jeşittir 0 veya 1 yönteminde neden sadece jdizi indeksleme kullanmak yerine eklemeden önce değerinizi çarpmıyorsunuz (muhtemelen 1-jyerine çarpılmalıdır j)
Richard Tingle

6
@steveha Çarpma daha hızlı olmalı, Intel kitaplarında aramaya çalıştım, ancak bulamadım ... her iki durumda da kıyaslama bana bu sonucu da veriyor.
atlaste

10
@steveha PS: int c = data[j]; sum += c & -(c >> 7);Çarpma gerektirmeyen başka bir olası cevap olacaktır .
atlaste

1021

Sıralanan durumda, başarılı dal tahminine veya herhangi bir dalsız karşılaştırma numarasına güvenmekten daha iyisini yapabilirsiniz: dalı tamamen kaldırın.

Gerçekten, dizi bitişik bir bölgede data < 128ve diğeri ile bölünür data >= 128. Bu yüzden ikiye bölme araması ile bölme noktasını bulmalısınız ( Lg(arraySize) = 15karşılaştırmaları kullanarak ), sonra o noktadan düz bir birikim yapın.

Gibi bir şey (işaretlenmemiş)

int i= 0, j, k= arraySize;
while (i < k)
{
  j= (i + k) >> 1;
  if (data[j] >= 128)
    k= j;
  else
    i= j;
}
sum= 0;
for (; i < arraySize; i++)
  sum+= data[i];

veya biraz daha şaşırmış

int i, k, j= (i + k) >> 1;
for (i= 0, k= arraySize; i < k; (data[j] >= 128 ? k : i)= j)
  j= (i + k) >> 1;
for (sum= 0; i < arraySize; i++)
  sum+= data[i];

Hem sıralı hem de sıralanmamış için yaklaşık bir çözüm sağlayan daha hızlı bir yaklaşım : sum= 3137536;(gerçekten tekdüze bir dağılım varsayarak, beklenen değeri 191.5 olan 16384 örnek) :-)


23
sum= 3137536- akıllı. Açıkçası sorunun konusu bu değil. Soru, şaşırtıcı performans özelliklerini açıklamakla ilgili. Bunun std::partitionyerine yapmanın eklenmesinin std::sortdeğerli olduğunu söylemeye meyilliyim . Her ne kadar asıl soru verilen sentetik ölçütten daha fazlasını kapsamaktadır.
sehe

12
@DeadMG: bu aslında belirli bir anahtar için standart ikilik arama değil, bölümleme indeksi için bir aramadır; yineleme başına tek bir karşılaştırma gerektirir. Ama bu koda güvenmeyin, kontrol etmedim. Garantili bir doğru uygulama ile ilgileniyorsanız bana bildirin.
Yves Daoust

831

Yukarıdaki davranış Şube tahmini nedeniyle olmaktadır.

Şube tahminini anlamak için önce Talimat Boru Hattı'nı anlamak gerekir :

Herhangi bir talimat bir dizi aşamaya bölünür, böylece farklı adımlar aynı anda paralel olarak yürütülebilir. Bu teknik komut satırı olarak bilinir ve modern işlemcilerdeki verimi arttırmak için kullanılır. Bunu daha iyi anlamak için lütfen Wikipedia'daki bu örneğe bakın .

Genel olarak, modern işlemciler oldukça uzun boru hatlarına sahiptir, ancak kolaylık için sadece bu 4 adımı ele alalım.

  1. EĞER - Talimatları bellekten al
  2. Kimlik - Talimatın kodunu çözme
  3. EX - Talimatı yürüt
  4. WB - CPU kaydına geri yaz

2 talimat için genel olarak 4 aşamalı boru hattı. Genel olarak 4 aşamalı boru hattı

Yukarıdaki soruya geri dönelim, aşağıdaki talimatları ele alalım:

                        A) if (data[c] >= 128)
                                /\
                               /  \
                              /    \
                        true /      \ false
                            /        \
                           /          \
                          /            \
                         /              \
              B) sum += data[c];          C) for loop or print().

Şube tahmini olmadan aşağıdakiler gerçekleşir:

B talimatını veya C talimatını uygulamak için işlemcinin B talimatına veya C talimatına gitme kararı A talimatının sonucuna bağlı olduğu için A talimatı boru hattında EX aşamasına gelene kadar beklemesi gerekecektir. böyle görünecek.

koşul doğruysa: resim açıklamasını buraya girin

Koşul yanlış döndürürse: resim açıklamasını buraya girin

A komutunun sonucunu beklemenin bir sonucu olarak, yukarıdaki durumda harcanan toplam CPU döngüleri (dal tahmini olmadan; hem doğru hem de yanlış için) 7'dir.

Peki şube tahmini nedir?

Şube öngörücüsü, bir dalın (eğer o zaman başka bir yapı) kesin olarak bilinmeden önce hangi yöne gideceğini tahmin etmeye çalışacaktır. A komutunun boru hattının EX aşamasına ulaşmasını beklemez, ancak kararı tahmin eder ve bu talimata gider (örneğimiz durumunda B veya C).

Doğru bir tahmin durumunda, boru hattı şöyle görünür: resim açıklamasını buraya girin

Daha sonra tahminin yanlış olduğu tespit edilirse, kısmen yürütülen talimatlar atılır ve boru hattı bir gecikmeye neden olacak şekilde doğru dal ile başlar. Şube yanlış tahmininde boşa harcanan zaman, getirme aşamasından yürütme aşamasına kadar boru hattındaki aşama sayısına eşittir. Modern mikroişlemciler oldukça uzun boru hatlarına sahip olma eğilimindedir, bu nedenle yanlış tahmin gecikmesi 10 ila 20 saat arasındadır. Boru hattı ne kadar uzun olursa iyi bir dal öngörücüsüne duyulan ihtiyaç o kadar büyük olur .

OP kodunda, koşullu ilk kez, şube öngörücüsünün tahmini temel alacak herhangi bir bilgisi yoktur, bu nedenle ilk kez rastgele bir sonraki talimatı seçecektir. Daha sonra for döngüsünde, öngörü tarihe dayandırılabilir. Artan sırada sıralanmış bir dizi için üç olasılık vardır:

  1. Tüm elemanlar 128'den az
  2. Tüm elemanlar 128'den büyük
  3. Bazı yeni başlayan elemanlar 128'den az ve daha sonra 128'den fazla

Tahmin edicinin her zaman ilk dalda gerçek dalı alacağını varsayalım.

Yani ilk durumda, tarihsel olarak tüm tahminleri doğru olduğundan her zaman gerçek dalı alacaktır. 2. durumda, başlangıçta yanlış tahmin eder, ancak birkaç tekrardan sonra doğru tahmin eder. 3. durumda, başlangıçta elemanlar 128'den az olana kadar doğru bir şekilde tahmin eder. Bundan sonra bir süre başarısız olur ve tarihte şube tahmin hatasını gördüğünde kendini düzeltir.

Tüm bu durumlarda, hata sayısı çok daha az olacaktır ve sonuç olarak, sadece birkaç kez kısmen yürütülen talimatları atması ve doğru dal ile başlaması, daha az CPU döngüsü ile sonuçlanacaktır.

Ancak rastgele sıralanmamış bir dizi olması durumunda, öngörmenin kısmen yürütülen talimatları atması ve çoğu zaman doğru dal ile başlaması ve sıralanan diziye kıyasla daha fazla CPU döngüsüyle sonuçlanması gerekir.


1
iki talimat birlikte nasıl yürütülür? Bu ayrı CPU çekirdeği ile mi yapılıyor yoksa boru hattı talimatı tek CPU çekirdeğine entegre mi?
M.kazem Akhgary

1
@ M.kazemAkhgary Hepsi tek bir mantıksal çekirdeğin içinde. İlgileniyorsanız, bu örneğin Intel Yazılım Geliştirici Kılavuzu
Sergey.quixoticaxis.Ivanov

727

Resmi bir cevap,

  1. Intel - Şube Yanlış Tahmininin Maliyetinden Kaçınma
  2. Intel - Yanlış Tahminleri Önlemek için Şube ve Döngü Yeniden Düzenleme
  3. Bilimsel Makaleler - Şube Tahmin Bilgisayar Mimarisi
  4. Kitaplar: JL Hennessy, DA Patterson: Bilgisayar mimarisi: nicel bir yaklaşım
  5. Bilimsel yayınlardaki makaleler: TY Yeh, YN Patt, şube tahminleriyle ilgili birçok şey yaptı.

Bu güzel diyagramdan , şube öngörücüsünün neden karıştığını da görebilirsiniz .

2 bit durum diyagramı

Orijinal koddaki her öğe rastgele bir değerdir

data[c] = std::rand() % 256;

böylece öngörücü tarafları std::rand() darbe .

Öte yandan, bir kez sıralandığında, yordayıcı önce güçlü bir şekilde alınmayan bir duruma geçecek ve değerler yüksek değere dönüştüğünde, yordayıcı güçlü bir şekilde alınmayandan güçlü bir şekilde alınana kadar üç aşamada değişecektir.



696

Aynı satırda (bunun herhangi bir cevapla vurgulanmadığını düşünüyorum) bazen (özellikle performansın önemli olduğu yazılımlarda - Linux çekirdeğinde olduğu gibi) aşağıdaki gibi bazı ifadeler bulabileceğinizden bahsetmek iyi olur:

if (likely( everything_is_ok ))
{
    /* Do something */
}

veya benzer şekilde:

if (unlikely(very_improbable_condition))
{
    /* Do something */    
}

Her ikisi de likely()ve unlikely()aslında __builtin_expect, derleyicinin kullanıcı tarafından sağlanan bilgileri dikkate alarak koşulu lehine tahmin kodu eklemesine yardımcı olmak için GCC'ler gibi bir şey kullanılarak tanımlanan makrolardır . GCC, çalışan programın davranışını değiştirebilecek veya önbelleği temizlemek gibi düşük düzeyli talimatlar yayan diğer yerleşikleri destekler . Kullanılabilir GCC'nin yerleşiklerinden geçen bu belgelere bakın .

Normalde bu tür optimizasyonlar esas olarak, gerçek zamanlı uygulamalarda veya yürütme süresinin önemli olduğu ve kritik olduğu gömülü sistemlerde bulunur. Örneğin, yalnızca 1/10000000 kez gerçekleşen bir hata durumunu kontrol ediyorsanız, derleyiciyi bu konuda neden bilgilendirmiyorsunuz? Bu şekilde, varsayılan olarak, şube tahmini koşulun yanlış olduğunu varsayar.


678

C ++ 'da sık kullanılan Boole işlemleri derlenmiş programda birçok dal üretir. Bu dallar döngülerin içindeyse ve öngörülmesi zorsa, uygulamayı önemli ölçüde yavaşlatabilirler. Boolean değişkenleri değeri ile 8-bit tamsayı olarak depolanır 0için falseve 1içintrue .

Boolean değişkenleri, girdi olarak Boole değişkenleri olan tüm işleçlerin girdilerin 0veya değerlerinden başka bir değere sahip olup olmadığını denetlemesi anlamında fazla tanımlanmıştır 1, ancak çıktı olarak Boole değerlerine sahip işleçler 0veya değerinden başka bir değer üretemez 1. Bu, girdi olarak Boole değişkenleriyle yapılan işlemleri gerekenden daha az verimli hale getirir. Örneği düşünün:

bool a, b, c, d;
c = a && b;
d = a || b;

Bu genellikle derleyici tarafından aşağıdaki şekilde uygulanır:

bool a, b, c, d;
if (a != 0) {
    if (b != 0) {
        c = 1;
    }
    else {
        goto CFALSE;
    }
}
else {
    CFALSE:
    c = 0;
}
if (a == 0) {
    if (b == 0) {
        d = 0;
    }
    else {
        goto DTRUE;
    }
}
else {
    DTRUE:
    d = 1;
}

Bu kod optimal olmaktan uzak. Yanlış tahminlerde şubeler uzun sürebilir. Eğer işlenenlerin 0ve değerlerinden başka bir değeri olmadığı kesin olarak biliniyorsa, Boole işlemleri çok daha verimli hale getirilebilir 1. Derleyicinin böyle bir varsayımda bulunmamasının nedeni, değişkenlerin başlatılmamışsa veya bilinmeyen kaynaklardan gelmesi durumunda başka değerlere sahip olabilmesidir. Yukarıdaki kod, geçerli değerlere başlatılmışsa ave bbaşlatılmışsa veya Boole çıktısı üreten operatörlerden geliyorsa optimize edilebilir . Optimize edilmiş kod şöyle görünür:

char a = 0, b = 1, c, d;
c = a & b;
d = a | b;

charyerine kullanılan boolsırayla mümkün bitsel operatörleri (kullanmak yapmak &ve |) yerine Boolean operatörleri ( &&ve ||). Bitsel operatörler yalnızca bir saat döngüsü alan tek talimatlardır. VEYA operatörü ( |) bile çalışır ave bdışındaki değerlere sahip 0veya 1. İşlenenler ve değerlerinden başka değerlere sahipse, AND işleci ( &) ve EXCLUSIVE OR işleci ( ^) tutarsız sonuçlar verebilir .01

~NOT için kullanılamaz. Bunun yerine, olduğu bilinen bir değişken üzerinde 0veya 1bunu XOR ile yaparak bir Boolean NOT yapabilirsiniz 1:

bool a, b;
b = !a;

aşağıdakiler için optimize edilebilir:

char a = 0, b;
b = a ^ 1;

a && bile ikame edilemez a & bise b, eğer değerlendirilmesi gereken bir ifade olan abir false( &&değerlendirmek olmaz b, &olacaktır). Benzer şekilde, eğer a || bdeğiştirilmemesi gereken bir ifade ise if ile değiştirilemeza | bbatrue .

İşlenenler değişkense, işlenenlerin karşılaştırmalar olmasına göre bitsel işleçlerin kullanılması daha avantajlıdır:

bool a; double x, y, z;
a = x > y && z < 5.0;

çoğu durumda en uygunudur ( &&ifadenin birçok dal yanlış tahminleri üretmesini beklemiyorsanız ).


341

Kesinlikle!...

Şube tahmini , kodunuzda gerçekleşen geçiş nedeniyle mantığın daha yavaş çalışmasını sağlar! Düz bir caddeye veya çok fazla dönüşe sahip bir caddeye gidiyorsunuz, elbette düz olanın daha hızlı yapılması gerekiyor! ...

Dizi sıralanırsa, koşulunuz ilk adımda yanlış data[c] >= 128olur : sonra sokağın sonuna kadar gerçek bir değer olur. Mantığın sonuna bu şekilde daha hızlı ulaşırsınız. Öte yandan, sıralanmamış bir dizi kullanarak, kodunuzun daha yavaş çalışmasını sağlayan çok fazla tornalama ve işleme ihtiyacınız var ...

Aşağıda sizin için oluşturduğum resme bakın. Hangi cadde daha hızlı bitecek?

Şube Tahmini

Programlı olarak, şube tahmini sürecin daha yavaş olmasına neden oluyor ...

Ayrıca sonunda, her birinin kodunuzu farklı şekilde etkileyeceğini belirten iki tür şube tahminimiz olduğunu bilmekte fayda vardır:

1. Statik

2. Dinamik

Şube Tahmini

Statik dal tahmini, bir koşullu dal ile ilk kez karşılaşıldığında mikroişlemci tarafından kullanılır ve koşullu dal kodunun başarılı bir şekilde yürütülmesi için dinamik dal tahmini kullanılır.

Bu kurallardan yararlanmak için kodunuzu etkin bir şekilde yazmak için if-else veya switch deyimlerini yazarken , önce en yaygın durumları kontrol edin ve en az yaygın olana kadar aşamalı olarak çalışın. Döngüler, normalde yalnızca döngü yineleyicinin durumu normal olarak kullanıldığından, statik dal tahmini için herhangi bir özel kod sırası gerektirmez.


304

Bu soru defalarca mükemmel bir şekilde cevaplanmıştır. Yine de grubun dikkatini bir başka ilginç analize çekmek istiyorum.

Son zamanlarda bu örnek (çok az değiştirilmiş), bir kod parçasının Windows'ta programın içinde nasıl profilli olabileceğini göstermenin bir yolu olarak da kullanılmıştır. Yol boyunca, yazar ayrıca kodun zamanının çoğunu hem sıralı hem de sıralanmamış durumda nerede harcadığını belirlemek için sonuçların nasıl kullanılacağını da gösterir. Son olarak, parça, sıralanmamış durumda ne kadar şube yanlış tahmininin olduğunu belirlemek için HAL'ın (Donanım Soyutlama Katmanı) az bilinen bir özelliğinin nasıl kullanılacağını da gösterir.

Bağlantı burada: http://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/profile/demo.htm


3
Bu çok ilginç bir makale (aslında hepsini okudum), ama soruyu nasıl cevaplıyor?
Peter Mortensen

2
@PeterMortensen Sorunuzla biraz karıştım. Örneğin, işte o parçadan ilgili bir satır var: When the input is unsorted, all the rest of the loop takes substantial time. But with sorted input, the processor is somehow able to spend not just less time in the body of the loop, meaning the buckets at offsets 0x18 and 0x1C, but vanishingly little time on the mechanism of looping. Yazar, burada yayınlanan kod bağlamında profil oluşturmayı ve bu sırada sıralanan durumun neden bu kadar çok daha hızlı olduğunu açıklamaya çalışıyor.
ForeverLearning

260

Başkaları tarafından daha önce bahsedildiği gibi, gizemin arkasında Şube Prediktörü var .

Bir şey eklemeye çalışmıyorum ama konsepti başka bir şekilde açıklıyorum. Wiki'de metin ve diyagram içeren kısa bir giriş var. Şube Predictor'unu sezgisel olarak detaylandırmak için bir diyagram kullanan aşağıdaki açıklamayı seviyorum.

Bilgisayar mimarisinde, bir dal tahmincisi, bir dalın (örneğin, eğer başka bir yapı) kesin olarak bilinmeden önce hangi yoldan gideceğini tahmin etmeye çalışan dijital bir devredir. Şube öngörücüsünün amacı, talimat boru hattındaki akışı iyileştirmektir. Şube tahmin edicileri, x86 gibi birçok modern boru hatlı mikroişlemci mimarisinde yüksek etkili performans elde etmede kritik bir rol oynamaktadır.

İki yönlü dallanma genellikle koşullu atlama talimatı ile uygulanır. Koşullu bir atlama ya "alınmaz" ve koşullu atlamadan hemen sonra gelen ilk kod dalıyla yürütmeye devam edebilir ya da "alınabilir" ve program belleğinde ikinci kodun bulunduğu farklı bir yere atlayabilir saklanmış. Koşul hesaplanana ve koşullu sıçramanın talimat boru hattındaki yürütme aşamasını geçinceye kadar bir koşullu sıçramanın alınıp alınmayacağı kesin olarak bilinmemektedir (bkz. Şekil 1).

Şekil 1

Açıklanan senaryoya dayanarak, talimatların farklı durumlarda bir boru hattında nasıl yürütüldüğünü göstermek için bir animasyon demosu yazdım.

  1. Şube Tahmincisi olmadan.

Şube tahmini olmadan, işlemcinin bir sonraki komutun boru hattındaki getirme aşamasına girebilmesi için koşullu atlama talimatının yürütme aşamasını geçmesini beklemesi gerekir.

Örnekte üç yönerge bulunmaktadır ve ilki koşullu bir atlama yönergesidir. Son iki talimat, koşullu atlama talimatı yürütülene kadar boru hattına girebilir.

şube öngörücüsü olmadan

3 talimatın tamamlanması 9 saat sürecektir.

  1. Şube Tahmincisi'ni kullanın ve koşullu bir atlama yapmayın. Diyelim ki tahminin koşullu sıçramayı almadığını varsayalım .

resim açıklamasını buraya girin

3 talimatın tamamlanması 7 saat sürecektir.

  1. Şube Tahmincisi'ni kullanın ve koşullu bir sıçrama yapın. Diyelim ki tahminin koşullu sıçramayı almadığını varsayalım .

resim açıklamasını buraya girin

3 talimatın tamamlanması 9 saat sürecektir.

Şube yanlış tahmininde boşa harcanan zaman, getirme aşamasından yürütme aşamasına kadar boru hattındaki aşama sayısına eşittir. Modern mikroişlemciler oldukça uzun boru hatlarına sahip olma eğilimindedir, bu nedenle yanlış tahmin gecikmesi 10 ila 20 saat arasındadır. Sonuç olarak, bir boru hattının daha uzun yapılması, daha gelişmiş bir dal öngörücüsüne olan ihtiyacı artırır.

Gördüğünüz gibi, Şube Tahmincisi'ni kullanmamak için bir nedenimiz yok gibi görünüyor.

Şube Tahmincisi'nin çok temel kısmını açıklayan oldukça basit bir demo. Bu gifler can sıkıcı ise, lütfen cevaptan kaldırmaktan çekinmeyin ve ziyaretçiler ayrıca BranchPredictorDemo'dan canlı demo kaynak kodunu da alabilirler.


1
Intel pazarlama animasyonları kadar iyi ve sadece şube tahmini ile değil, yürütme dışı yürütme ile takıntılıydılar, her iki strateji de "spekülatif". Hafızada ve depolamada okuma (ara belleğe ardışık getirme) de spekülatiftir. Hepsi toplar.
mckenzm

@mckenzm: sıra dışı spekülatif yürütme, şube tahminini daha da değerli kılıyor; getirme / kod çözme kabarcıklarını gizlemenin yanı sıra, şube tahmini + spekülatif yürütme, denetim bağımlılıklarını kritik yol gecikmesinden kaldırır. Bir if()bloğun içindeki veya sonrasındaki kod , şube durumu bilinmeden önce yürütülebilir . Veya strlenveya gibi bir arama döngüsü için memchrtümleştirmeler çakışabilir. Bir sonraki yinelemeden herhangi birini çalıştırmadan önce eşleşme veya sonucun bilinmesini beklemeniz gerekiyorsa, önbellek yükü + veri akışı yerine ALU gecikmesi üzerinde tıkanıklık yaşarsınız.
Peter Cordes

209

Şube tahmini kazancı!

Şube yanlış tahmininin programları yavaşlatmadığını anlamak önemlidir. Cevapsız bir tahminin maliyeti, tıpkı şube tahmininin olmaması gibidir ve hangi kodun çalıştırılacağına karar vermek için ifadenin değerlendirilmesini beklediniz (sonraki paragrafta daha fazla açıklama).

if (expression)
{
    // Run 1
} else {
    // Run 2
}

Bir if-else\ switchifadesi olduğunda, hangi bloğun yürütüleceğini belirlemek için ifadenin değerlendirilmesi gerekir. Derleyici tarafından üretilen montaj koduna koşullu dal talimatları eklenir.

Bir dal talimatı, bir bilgisayarın farklı bir komut dizisi yürütmeye başlamasına ve böylece talimatların yürütülmesi için varsayılan davranışından sapmasına neden olabilir (yani, ifade yanlışsa, program ifbloğun kodunu atlar ); bizim durumumuzda ifade değerlendirmesi.

Bununla birlikte, derleyici, gerçekte değerlendirilmeden önce sonucu tahmin etmeye çalışır. ifBloktan talimatlar getirecek ve eğer ifade doğru olduğu ortaya çıkarsa, o zaman harika! Değerlendirmek için gereken zamanı kazandık ve kodda ilerleme kaydettik; değilse, yanlış kodu çalıştırıyoruz, boru hattı temizlendi ve doğru blok çalıştırıldı.

Görselleştirme:

Diyelim ki rota 1 veya rota 2 seçmeniz gerekiyor. Ortağınızın haritayı kontrol etmesini beklerken, ## numarada durdunuz ve beklediniz, ya da sadece route1'i ve şanslıysanız (rota 1 doğru rotadır), o zaman harika eşinizin haritayı kontrol etmesini beklemek zorunda kalmadınız (haritayı kontrol etmesi için ona zaman kazandıracaksınız), aksi takdirde geri döneceksiniz.

Boru hatlarını yıkamak süper hızlı olsa da, bugünlerde bu kumar oynamak buna değer. Sıralı verileri veya yavaş değişen verileri tahmin etmek, hızlı değişiklikleri tahmin etmekten her zaman daha kolay ve daha iyidir.

 O      Route 1  /-------------------------------
/|\             /
 |  ---------##/
/ \            \
                \
        Route 2  \--------------------------------

Boru hatlarını yıkarken süper hızlı değil. Önbellekle karşılaştırıldığında DRAM'a kadar hızlıdır, ancak modern bir yüksek performanslı x86'da (Intel Sandybridge ailesi gibi) yaklaşık bir düzine döngü. Hızlı kurtarma, kurtarmaya başlamadan önce tüm eski bağımsız talimatların emekliliğe ulaşmasını beklemesine izin vermemesine rağmen, bir yanlış tahminde çok fazla ön uç döngüsünü kaybedersiniz. Skylake CPU bir dalı yanlış tahmin ettiğinde tam olarak ne olur? . (Ve her döngü yaklaşık 4 çalışma talimatı olabilir.) Yüksek verimli kod için kötü.
Peter Cordes

153

ARM'de dal gerekli değildir, çünkü her komutta İşlemci Durum Kaydında ortaya çıkabilecek 16 farklı farklı durumdan herhangi birini test eden (sıfır maliyetle) ve bir komuttaki koşulun false, yönerge atlanır. Bu kısa dallara olan ihtiyacı ortadan kaldırır ve bu algoritma için herhangi bir dal tahmin vuruşu olmaz. Bu nedenle, sıralamanın ek yükü nedeniyle, bu algoritmanın sıralı sürümü ARM'deki sıralanmamış sürümden daha yavaş çalışacaktır.

Bu algoritmanın iç döngüsü ARM montaj dilinde aşağıdaki gibi görünecektir:

MOV R0, #0     // R0 = sum = 0
MOV R1, #0     // R1 = c = 0
ADR R2, data   // R2 = addr of data array (put this instruction outside outer loop)
.inner_loop    // Inner loop branch label
    LDRB R3, [R2, R1]     // R3 = data[c]
    CMP R3, #128          // compare R3 to 128
    ADDGE R0, R0, R3      // if R3 >= 128, then sum += data[c] -- no branch needed!
    ADD R1, R1, #1        // c++
    CMP R1, #arraySize    // compare c to arraySize
    BLT inner_loop        // Branch to inner_loop if c < arraySize

Ama bu aslında daha büyük bir resmin parçası:

CMPopcodes her zaman İşlemci Durum Kaydı'ndaki (PSR) durum bitlerini günceller, çünkü bu onların amacıdır, ancak talimatlara isteğe bağlı bir Sson ek eklemediğiniz sürece PSR'nin, talimat sonucu. Tıpkı 4 bitlik koşul soneki gibi, PSR'yi etkilemeden talimatları yürütebilmek, ARM'deki dallara olan ihtiyacı azaltan ve ayrıca donanım düzeyinde sipariş dışı gönderimi kolaylaştıran bir mekanizmadır. , çünkü güncellenen bazı operasyon X'i gerçekleştirdikten sonra durum bitleri, daha sonra (veya paralel olarak), durum bitlerini açıkça etkilememesi gereken bir sürü başka iş yapabilirsiniz, daha sonra X tarafından daha önce ayarlanan durum bitlerinin durumunu test edebilirsiniz.

Durum testi alanı ve isteğe bağlı "ayarlanan durum biti" alanı birleştirilebilir, örneğin:

  • ADD R1, R2, R3R1 = R2 + R3herhangi bir durum bitini güncellemeden çalışır .
  • ADDGE R1, R2, R3 aynı işlemi yalnızca durum bitlerini etkileyen önceki bir talimat Büyüktür veya Eşittir koşuluyla sonuçlandıysa gerçekleştirir.
  • ADDS R1, R2, R3Daha sonra gerçekleştirdiği toplama ve günceller N, Z, Cve Vsonuç (imzasız ek için) taşınan, Sıfır, Negatif olmasına dayalı İşlemci Durumu Kayıt bayrakları veya taştı (imzalı ek için).
  • ADDSGE R1, R2, R3ekleme işlemini yalnızca GEsınama doğru olduğunda gerçekleştirir ve daha sonra ekleme bitinin sonucuna göre durum bitlerini güncelleştirir.

Çoğu işlemci mimarisi, belirli bir işlem için durum bitlerinin güncellenip güncellenmeyeceğini belirleme yeteneğine sahip değildir; bu, durum bitlerini kaydetmek ve daha sonra geri yüklemek için ek kod yazmayı gerektirebilir veya ek dallar gerektirebilir veya işlemcinin çıkışını sınırlandırabilir sipariş yürütme verimliliği: çoğu CPU komut seti mimarisinin çoğu talimattan sonra durum bitlerini zorlayarak güncellemenin yan etkilerinden biri, hangi talimatların birbirine müdahale etmeden paralel olarak çalıştırılabileceğini birbirinden ayırmanın çok daha zor olmasıdır. Durum bitlerinin güncellenmesinin yan etkileri vardır, bu nedenle kod üzerinde doğrusallaştırıcı bir etkiye sahiptir.ARM'nin, herhangi bir komutta dalsız durum testini, herhangi bir komuttan sonra durum bitlerini güncelleme veya güncelleştirme seçeneği ile karıştırma ve eşleştirme yeteneği, hem montaj dili programcıları hem de derleyiciler için son derece güçlüdür ve çok verimli kod üretir.

ARM'nin neden bu kadar olağanüstü başarılı olduğunu merak ettiyseniz, bu iki mekanizmanın parlak etkinliği ve etkileşimi hikayenin büyük bir parçasıdır, çünkü bunlar ARM mimarisinin verimliliğinin en büyük kaynaklarından biridir. ARM ISA'nın orijinal tasarımcılarının 1983'te parlaklığı, Steve Furber ve Roger (şimdi Sophie) Wilson, abartılamaz.


1
ARM'deki diğer yenilik, S talimatı son ekinin eklenmesidir, ayrıca (neredeyse) tüm talimatlarda isteğe bağlıdır, eğer yoksa, talimatların durum bitlerini değiştirmesini önler (işi durum bitlerini ayarlamak olan CMP talimatı hariç, S ekine ihtiyaç duymaz). Bu, çoğu durumda karşılaştırma sıfır veya benzer olduğu sürece CMP talimatlarından kaçınmanıza izin verir (örn. SUBS R0, R0, # 1, R0 sıfıra ulaştığında Z (Sıfır) bitini ayarlayacaktır). Şartlı koşullar ve S soneki sıfır ek yüke neden olur. Oldukça güzel bir ISA.
Luke Hutchison

2
S son ekinin eklenmemesi, bunlardan birinin durum bitlerini değiştirebileceğinden endişe etmeden arka arkaya birkaç koşullu talimat almanıza izin verir, aksi takdirde koşullu talimatların geri kalanını atlamanın yan etkisi olabilir.
Luke Hutchison

OP olduğunu Not değil onların ölçümünde sıralamak için zaman dahil. Sıralanmamış durum, döngüyü çok daha yavaş çalıştırmasına rağmen, bir şube x86 döngüsünü çalıştırmadan önce sıralamak genel bir kayıptır. Ancak büyük bir diziyi sıralamak çok fazla iş gerektirir .
Peter Cordes

BTW, dizinin sonuna göre dizine ekleyerek bir komutu döngüye kaydedebilirsiniz. Döngüden önce, ayarlayın R2 = data + arraySize, ardından ile başlayın R1 = -arraySize. Döngünün alt kısmı adds r1, r1, #1/ olur bnz inner_loop. Derleyiciler bu optimizasyonu bir nedenden dolayı kullanmazlar: / Ama yine de, eklentinin tahmini yürütülmesi bu durumda x86 gibi diğer ISA'larda dalsız kodla yapabileceklerinizden temel olarak farklı değildir cmov. Her ne kadar hoş olmasa da: gcc optimizasyon bayrağı -O3 kodu -O2'den daha yavaş yapar
Peter Cordes

1
(ARM tarafından belirtilen yürütme gerçekten talimatı NOP yapar, böylece cmovbellek kaynağı işlenenli x86'dan farklı olarak hataya neden olabilecek yüklerde veya mağazalarda bile kullanabilirsiniz . AArch64 dahil çoğu ISA'nın yalnızca ALU seçme işlemleri vardır. ve çoğu ISA'da şubesiz koddan daha verimli kullanılabilir.)
Peter Cordes

146

Şube tahmini ile ilgili. Bu ne?

  • Şube tahmincisi, modern mimarilerle hala alakalı olan eski performans iyileştirme tekniklerinden biridir. Basit tahmin teknikleri hızlı arama ve güç verimliliği sağlarken, yüksek yanlış tahmin oranından muzdariptirler.

  • Öte yandan, karmaşık dal tahminleri - ya nöral tabanlı ya da iki seviyeli dal tahmininin varyantları - daha iyi tahmin doğruluğu sağlar, ancak daha fazla güç tüketir ve karmaşıklık katlanarak artar.

  • Buna ek olarak, karmaşık tahmin tekniklerinde, dalları tahmin etmek için harcanan zamanın kendisi çok yüksektir - 2 ila 5 döngü arasındadır - ki bu gerçek dalların yürütme süresi ile karşılaştırılabilir.

  • Şube tahmini esasen mümkün olan en düşük eksiklik oranını, düşük güç tüketimini ve minimum kaynaklarla düşük karmaşıklığı elde etmek için vurgu yapılan bir optimizasyon (minimizasyon) problemidir.

Gerçekten üç farklı dal türü vardır:

İleri koşullu dalları iletme - çalışma zamanı koşuluna bağlı olarak, PC (program sayacı), yönerge akışında bir adrese işaret edecek şekilde değiştirilir.

Geriye dönük koşullu dallar - PC, talimat akışında geriye bakacak şekilde değiştirilir. Dal, döngünün sonunda yapılan bir test, döngünün tekrar yürütülmesi gerektiğini belirttiğinde, bir program döngüsünün başına geriye doğru dallanma gibi bazı koşullara dayanır.

Koşulsuz dallar - bu, belirli bir koşulu olmayan atlamaları, prosedür çağrılarını ve iadeleri içerir. Örneğin, koşulsuz atlama talimatı montaj dilinde basitçe "jmp" olarak kodlanabilir ve talimat akışı hemen atlama talimatı ile gösterilen hedef konuma yönlendirilirken, "jmpne" olarak kodlanabilecek koşullu bir atlama yönlendirilmelidir. yönerge akışını yalnızca önceki bir "karşılaştırma" talimatındaki iki değerin karşılaştırılmasının sonucu değerlerin eşit olmadığını göstermesi durumunda yeniden yönlendirir. (X86 mimarisi tarafından kullanılan bölümlenmiş adresleme şeması, atlamalar "yakın" (bir segment içinde) veya "uzak" (segment dışında) olabileceğinden ekstra karmaşıklık katar. Her türün dal tahmin algoritmaları üzerinde farklı etkileri vardır.)

Statik / dinamik Dal Tahmin : Bir koşullu dal ilk kez karşılaşıldığında statik işlemcisi mikroişlemci tarafından kullanılır ve koşullu dal kodunun başarılı bir şekilde yürütülmesi için dinamik dal tahmini kullanılır.

Referanslar:


145

Şube tahmininin sizi yavaşlatabileceği gerçeğinin yanı sıra, sıralanmış bir dizinin başka bir avantajı vardır:

Sadece değeri kontrol etmek yerine bir durma koşulunuz olabilir, bu şekilde sadece ilgili verilerin üzerine dönüp geri kalanını görmezden gelirsiniz.
Şube tahmini sadece bir kez özleyecektir.

 // sort backwards (higher values first), may be in some other part of the code
 std::sort(data, data + arraySize, std::greater<int>());

 for (unsigned c = 0; c < arraySize; ++c) {
       if (data[c] < 128) {
              break;
       }
       sum += data[c];               
 }

1
Doğru, ancak diziyi sıralamanın kurulum maliyeti O (N log N) 'dir, bu nedenle diziyi sıralamanın tek nedeni erken kırılabilirse, erken kırmak size yardımcı olmaz. Ancak, diziyi önceden sıralamak için başka nedenleriniz varsa, evet, bu değerlidir.
Luke Hutchison

Verileri kaç kez sıraladığınıza göre kaç kez sıraladığınıza bağlıdır. Bu örnekteki sıralama sadece bir örnektir, döngüden hemen önce olmak zorunda değildir
Yochai Timmer

2
Evet, ilk yorumumda tam olarak bu noktaya değindim :-) "Şube tahmini sadece bir kez özleyecek" dersiniz. Ancak sıralama algoritması içinde O (N log N) dal tahmin özledikleri sayılmaz, aslında sıralanmamış durumda O (N) dal tahmin özlüyor özledim daha büyüktür. Bu nedenle, sıralama algoritmasına bağlı olarak, örneğin önbellek özledikleri nedeniyle hızlı sıralama için, hatta (muhtemelen aslında O'ya (10 günlük N) daha yakın olmak için sıralanan verilerin O (log N) sürelerinin tamamını kullanmanız gerekir - mergesort daha önbellek uyumludur, bu yüzden bile kırmak için O (2 günlük N) kullanımlarına daha yakın olmanız gerekir.)
Luke Hutchison

Bununla birlikte, önemli bir optimizasyon, yalnızca hedef yarıçap değeri 127'den az olan öğeleri sıralayarak yalnızca "yarı hızlı bir sıralama" yapmak olacaktır (pivottan küçük veya ona eşit olan her şeyin pivottan sonra sıralandığı varsayılarak ). Pivoya ulaştığınızda, pivottan önceki öğeleri toplayın. Bu, O (N log N) yerine O (N) başlangıç ​​zamanında çalışacaktır, ancak yine de daha önce verdiğim sayılara dayanarak muhtemelen O (5 N) sırasına göre çok fazla dal tahmini özlemesi olacaktır. yarım hızlı bir spor.
Luke Hutchison

132

Sıralama dizileri, dal tahmini olarak adlandırılan bir fenomen nedeniyle sıralanmamış bir diziden daha hızlı işlenir.

Şube tahmincisi, bir dalın hangi yöne gideceğini tahmin etmeye çalışan ve talimat boru hattındaki akışı iyileştiren dijital bir devredir (bilgisayar mimarisinde). Devre / bilgisayar bir sonraki adımı tahmin eder ve yürütür.

Yanlış bir tahminde bulunmak, önceki adıma geri dönmeye ve başka bir tahminde bulunmaya yol açar. Tahminin doğru olduğu varsayıldığında, kod bir sonraki adıma devam edecektir. Yanlış bir tahmin, doğru bir tahmin gerçekleşinceye kadar aynı adımı tekrarlamakla sonuçlanır.

Sorunuzun cevabı çok basit.

Sıralanmamış bir dizide, bilgisayar birden çok tahmin yapar ve hata olasılığının artmasına neden olur. Oysa sıralı bir dizide bilgisayar daha az tahmin yapar ve hata olasılığını azaltır. Daha fazla tahmin yapmak daha fazla zaman gerektirir.

Sıralama Dizisi: Düz Yol ____________________________________________________________________________________ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT

Sıralanmamış Dizi: Kavisli Yol

______   ________
|     |__|

Şube tahmini: Hangi yolun düz olduğunu tahmin etme / tahmin etme ve kontrol etmeden takip etme

___________________________________________ Straight road
 |_________________________________________|Longer road

Her iki yol da aynı hedefe ulaşsa da, düz yol daha kısadır ve diğeri daha uzundur. O zaman diğerini yanlışlıkla seçerseniz, geri dönüş yoktur ve bu nedenle daha uzun yolu seçerseniz ekstra zaman kaybedersiniz. Bu bilgisayarda olanlara benzer ve umarım bu daha iyi anlamanıza yardımcı olur.


Ayrıca @Simon_Weaver yorumlardan alıntı yapmak istiyorum :

Daha az tahmin yapmaz - daha az yanlış tahmin yapar. Hala döngü boyunca her zaman tahmin etmek zorunda ...


123

Aşağıdaki MATLAB kodu için MacBook Pro (Intel i7, 64 bit, 2.4 GHz) ile MATLAB 2011b ile aynı kodu denedim:

% Processing time with Sorted data vs unsorted data
%==========================================================================
% Generate data
arraySize = 32768
sum = 0;
% Generate random integer data from range 0 to 255
data = randi(256, arraySize, 1);


%Sort the data
data1= sort(data); % data1= data  when no sorting done


%Start a stopwatch timer to measure the execution time
tic;

for i=1:100000

    for j=1:arraySize

        if data1(j)>=128
            sum=sum + data1(j);
        end
    end
end

toc;

ExeTimeWithSorting = toc - tic;

Yukarıdaki MATLAB kodunun sonuçları aşağıdaki gibidir:

  a: Elapsed time (without sorting) = 3479.880861 seconds.
  b: Elapsed time (with sorting ) = 2377.873098 seconds.

@ GManNickG gibi C kodu sonuçları olsun:

  a: Elapsed time (without sorting) = 19.8761 sec.
  b: Elapsed time (with sorting ) = 7.37778 sec.

Buna dayanarak, MATLAB sıralamadan C uygulamasından neredeyse 175 kat ve sıralama ile 350 kat daha yavaş görünüyor. Başka bir deyişle, (şube tahmin) etkisidir 1.46x MATLAB uygulanması ve için 2.7x C uygulanması için.


7
Sadece tamlık uğruna, muhtemelen Matlab'a böyle uygulayamazsınız. Bahse girerim, sorunu vektörleştirdikten sonra çok daha hızlı olurdu.
ysap

1
Matlab birçok durumda otomatik paralelleştirme / vektörleştirme yapar ancak buradaki konu, şube tahmininin etkisini kontrol etmektir. Matlab zaten bağışık değil!
Shan

1
(? Böylece basamak sonsuz miktarı veya) matlab kullanılması, doğal sayılar veya mat laboratuar özel uygulama mı
Thorbjorn Ravn Andersen

54

Verilerin sıralanması için diğer cevapların varsayımı doğru değildir.

Aşağıdaki kod tüm diziyi sıralamaz, yalnızca 200 öğeden oluşan bölümlerini sıralar ve böylece en hızlı şekilde çalışır.

Yalnızca k-elemanı bölümlerinin sıralanması, tüm diziyi sıralamak için gereken zaman O(n)yerine ön işlemi doğrusal zamanda tamamlar O(n.log(n)).

#include <algorithm>
#include <ctime>
#include <iostream>

int main() {
    int data[32768]; const int l = sizeof data / sizeof data[0];

    for (unsigned c = 0; c < l; ++c)
        data[c] = std::rand() % 256;

    // sort 200-element segments, not the whole array
    for (unsigned c = 0; c + 200 <= l; c += 200)
        std::sort(&data[c], &data[c + 200]);

    clock_t start = clock();
    long long sum = 0;

    for (unsigned i = 0; i < 100000; ++i) {
        for (unsigned c = 0; c < sizeof data / sizeof(int); ++c) {
            if (data[c] >= 128)
                sum += data[c];
        }
    }

    std::cout << static_cast<double>(clock() - start) / CLOCKS_PER_SEC << std::endl;
    std::cout << "sum = " << sum << std::endl;
}

Bu aynı zamanda sıralama düzeni gibi herhangi bir algoritmik sorunla hiçbir ilgisi olmadığını "kanıtlar" ve gerçekten de şube tahminidir.


4
Bunun nasıl bir şey kanıtladığını gerçekten görmüyorum? Gösterdiğiniz tek şey, "tüm diziyi sıralamak için tüm işi yapmamak, tüm diziyi sıralamaktan daha az zaman alır" dır. Bunun "en hızlı koştuğu" iddianız da mimariye bağlıdır. Bunun ARM'de nasıl çalıştığı ile ilgili cevabımı görün. PS, 200 elemanlı blok döngü içine toplamayı koyarak, tersine sıralayarak ve sonra aralık dışı bir değer elde ettiğinizde Yochai Timmer'ın kırılma önerisini kullanarak ARM olmayan mimarilerde kodunuzu daha hızlı hale getirebilirsiniz. Bu şekilde her 200 elemanlı blok toplamı erken sonlandırılabilir.
Luke Hutchison

Algoritmayı sıralanmamış veriler üzerinde verimli bir şekilde uygulamak istiyorsanız, bu işlemi dalsız olarak (ve SIMD ile, örneğin x86 pcmpgtbile, yüksek bit kümelerine sahip öğeleri bulmak için VE sonra daha küçük öğeleri sıfırlamak için) yaparsınız . Parçaları sıralamak için her zaman harcama yapmak daha yavaş olacaktır. Şubesiz bir sürümün, veriden bağımsız performansa sahip olması da maliyetin şube yanlış tahmininden geldiğini kanıtlar. Veya doğrudan Skylake gibi gözlemlemek int_misc.clear_resteer_cyclesveya int_misc.recovery_cyclesyanlış tahminlerden önden boşta kalan döngüleri saymak için performans sayaçlarını kullanın
Peter Cordes

Yukarıdaki her iki yorum da, özel donanımın özel makine talimatları ile savunulması lehine genel algoritmik sorunları ve karmaşıklığı göz ardı ediyor gibi görünmektedir. İlkini, özellikle bu cevaptaki önemli genel bilgileri, özel makine talimatlarının kör lehine körü körüne reddettiği için özellikle küçük buluyorum.
user2297550

36

Bjarne Stroustrup'un Bu sorunun cevabı:

Bu bir röportaj sorusu gibi geliyor. Bu doğru mu? Nasıl bilebilirsin? Önce bazı ölçümler yapmadan verimlilikle ilgili soruları cevaplamak kötü bir fikirdir, bu nedenle nasıl ölçüleceğini bilmek önemlidir.

Yani, bir milyon tamsayı ile denedim ve aldım:

Already sorted    32995 milliseconds
Shuffled          125944 milliseconds

Already sorted    18610 milliseconds
Shuffled          133304 milliseconds

Already sorted    17942 milliseconds
Shuffled          107858 milliseconds

Emin olmak için birkaç kez koştum. Evet, fenomen gerçek. Anahtar kodum:

void run(vector<int>& v, const string& label)
{
    auto t0 = system_clock::now();
    sort(v.begin(), v.end());
    auto t1 = system_clock::now();
    cout << label 
         << duration_cast<microseconds>(t1  t0).count() 
         << " milliseconds\n";
}

void tst()
{
    vector<int> v(1'000'000);
    iota(v.begin(), v.end(), 0);
    run(v, "already sorted ");
    std::shuffle(v.begin(), v.end(), std::mt19937{ std::random_device{}() });
    run(v, "shuffled    ");
}

En azından bu fenomen bu derleyici, standart kütüphane ve optimize edici ayarları ile gerçek. Farklı uygulamalar farklı cevaplar verebilir ve verebilir. Aslında, birisi daha sistematik bir çalışma yaptı (hızlı bir web araması bulacaktır) ve çoğu uygulama bu etkiyi göstermektedir.

Bunun bir nedeni dal tahminidir: sıralama algoritmasındaki temel işlem “if(v[i] < pivot]) …” veya eşdeğeridir. Sıralı bir dizi için bu test her zaman doğrudur, oysa rastgele bir dizi için seçilen dal rastgele değişir.

Başka bir neden, vektör zaten sıralandığında, elemanları asla doğru konumlarına taşımamıza gerek yoktur. Bu küçük detayların etkisi gördüğümüz beş ya da altı faktörüdür.

Quicksort (ve genel olarak sıralama), bilgisayar biliminin en büyük zihinlerini çeken karmaşık bir çalışmadır. İyi bir sıralama işlevi, hem iyi bir algoritma seçmenin hem de uygulamasında donanım performansına dikkat edilmesinin bir sonucudur.

Verimli bir kod yazmak istiyorsanız, makine mimarisi hakkında biraz bilgi sahibi olmanız gerekir.


28

Bu soru, CPU'lardaki Şube Tahmin Modellerinde yatmaktadır. Bu makaleyi okumanızı tavsiye ederim:

Birden Çok Şube Tahmini ve Şube Adresi Önbelleği ile Talimat Getirme Oranını Artırma

Öğeleri sıraladığınızda, IR tüm CPU talimatlarını tekrar tekrar almak için rahatsız edilemedi, bunları önbellekten alır.


Yanlış tahminlere bakılmaksızın, CPU'nun L1 talimat önbelleğinde talimatlar sıcak kalır. Sorun, hemen önceki talimatların kodu çözülmeden ve yürütmeyi bitirmeden önce bunları doğru sırada boru hattına getirmektir.
Peter Cordes

15

Şube tahmin hatalarından kaçınmanın bir yolu, bir arama tablosu oluşturmak ve verileri kullanarak dizine eklemektir. Stefan de Bruijn bunu cevabında tartıştı.

Ancak bu durumda, değerlerin [0, 255] aralığında olduğunu biliyoruz ve yalnızca> = 128 değerlerini önemsiyoruz. Bu, bize bir değer isteyip istemediğimizi söyleyecek tek bir biti kolayca ayıklayabileceğimiz anlamına gelir: verileri sağ 7 bite bırakırsak, 0 bit veya 1 bit kaldı ve yalnızca 1 bitimiz olduğunda değeri eklemek istiyoruz. Bu biti "karar biti" olarak adlandıralım.

Karar bitinin 0/1 değerini bir dizine dizin olarak kullanarak, verilerin sıralanıp sıralanmadığına bakılmaksızın eşit derecede hızlı kod oluşturabiliriz. Kodumuz her zaman bir değer katar, ancak karar biti 0 olduğunda, değeri umursadığımız bir yere ekleriz. İşte kod:

// Ölçek

clock_t start = clock();
long long a[] = {0, 0};
long long sum;

for (unsigned i = 0; i < 100000; ++i)
{
    // Primary loop
    for (unsigned c = 0; c < arraySize; ++c)
    {
        int j = (data[c] >> 7);
        a[j] += data[c];
    }
}

double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;
sum = a[1];

Bu kod, eklentilerin yarısını boşa harcar, ancak hiçbir zaman dal tahmini hatası yoktur. Rastgele verilerde, gerçek bir if ifadesine sahip sürümden çok daha hızlıdır.

Ancak testlerimde, açık bir arama tablosu bundan biraz daha hızlıydı, muhtemelen bir arama tablosuna endeksleme, bit kaydırma işleminden biraz daha hızlı olduğu için. Bu, kodumun nasıl ayarlandığını ve arama tablosunu nasıl kullandığını gösterir (kodda "LookUp Table" için düşünülemez bir şekilde lut). İşte C ++ kodu:

// Bildirin ve ardından arama tablosunu doldurun

int lut[256];
for (unsigned c = 0; c < 256; ++c)
    lut[c] = (c >= 128) ? c : 0;

// Use the lookup table after it is built
for (unsigned i = 0; i < 100000; ++i)
{
    // Primary loop
    for (unsigned c = 0; c < arraySize; ++c)
    {
        sum += lut[data[c]];
    }
}

Bu durumda, arama tablosu sadece 256 bayttı, bu yüzden önbelleğe güzel oturuyor ve hepsi hızlıydı. Veriler 24 bitlik değerler olsaydı ve sadece yarısını isteseydik bu teknik işe yaramazdı ... arama tablosu pratik olamayacak kadar büyük olurdu. Öte yandan, yukarıda gösterilen iki tekniği birleştirebiliriz: önce bitleri kaydırın, sonra bir arama tablosunu indeksleyin. Yalnızca ilk yarı değeri istediğimiz 24 bitlik bir değer için, verileri potansiyel olarak 12 bit sağa kaydırabilir ve tablo dizini için 12 bitlik bir değerle bırakabiliriz. 12 bit tablo dizini, pratik olabilecek 4096 değerinde bir tablo anlamına gelir.

Hangi iĢaretçinin kullanılacağına karar vermek için if ifadesini kullanmak yerine diziye endeksleme tekniği kullanılabilir. İkili ağaçlar uygulayan bir kütüphane gördüm ve iki adlandırılmış işaretçi (pLeft ve pRight veya her neyse) uzunluğunda 2 işaretçi dizisine sahipti ve hangisini izleyeceğine karar vermek için "karar biti" tekniğini kullandım. Örneğin:

if (x < node->value)
    node = node->pLeft;
else
    node = node->pRight;
this library would do something like:

i = (x < node->value);
node = node->link[i];

güzel bir çözüm belki işe yarayacak


Hangi C ++ derleyici / donanımıyla bunu ve hangi derleyici seçenekleriyle test ettiniz? Orijinal sürümün güzel dalsız SIMD koduna otomatik olarak vektörleştirilmediğine şaşırdım. Tam optimizasyonu etkinleştirdiniz mi?
Peter Cordes

4096 giriş arama tablosu çılgınca geliyor. Herhangi bir biti kaydırırsanız , LUT sonucunu yalnızca orijinal numarayı eklemek istiyorsanız kullanamazsınız. Bunların hepsi, dalsız teknikler kullanarak kolayca derleyicinizin etrafında çalışmak için aptalca hileler gibi geliyor. Daha basit olurdu mask = tmp < 128 : 0 : -1UL;/total += tmp & mask;
Peter Cordes
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.