512x512'lik bir matrisin aktarılması neden 513x513'lük bir matrisin aktarılmasından çok daha yavaş?


218

Farklı boyutlardaki kare matrisler üzerinde bazı deneyler yaptıktan sonra bir desen ortaya çıktı. Değişmez bir şekilde, bir boyut matrisinin transpozisyonu, bir boyut 2^ntranspozisyonunun transpozisyonundan daha yavaştır2^n+1 . Küçük değerleri niçin fark büyük değildir.

Bununla birlikte 512'lik bir değer üzerinde büyük farklılıklar meydana gelir. (En azından benim için)

Feragatname: Fonksiyonun, öğelerin çift takası nedeniyle matrisi gerçekten aktarmadığını biliyorum, ancak hiçbir fark yaratmıyor.

Kodu takip eder:

#define SAMPLES 1000
#define MATSIZE 512

#include <time.h>
#include <iostream>
int mat[MATSIZE][MATSIZE];

void transpose()
{
   for ( int i = 0 ; i < MATSIZE ; i++ )
   for ( int j = 0 ; j < MATSIZE ; j++ )
   {
       int aux = mat[i][j];
       mat[i][j] = mat[j][i];
       mat[j][i] = aux;
   }
}

int main()
{
   //initialize matrix
   for ( int i = 0 ; i < MATSIZE ; i++ )
   for ( int j = 0 ; j < MATSIZE ; j++ )
       mat[i][j] = i+j;

   int t = clock();
   for ( int i = 0 ; i < SAMPLES ; i++ )
       transpose();
   int elapsed = clock() - t;

   std::cout << "Average for a matrix of " << MATSIZE << ": " << elapsed / SAMPLES;
}

Değiştirmek MATSIZEboyutu değiştirmemize izin verir (duh!). İdeone'de iki versiyon yayınladım:

Çevremde (MSVS 2010, tam optimizasyonlar), fark benzer:

  • boyut 512 - ortalama 2,19 ms
  • boyut 513 - ortalama 0,57 ms

Bu neden oluyor?


9
Kodun bana düşmanca önbellek gibi geliyor.
CodesInChaos

7
Bu soru ile hemen hemen aynı sorun: stackoverflow.com/questions/7905760/…
Mysticial

Eklemek ister misin, @CodesInChaos? (Veya başka biri.)
corazza

@Bane Kabul edilen cevabı okumaya ne dersiniz?
CodesInChaos

4
@nzomkxia Herhangi bir şeyi optimizasyon olmadan ölçmek anlamsızdır. Optimizasyonlar devre dışı bırakıldığında, oluşturulan kod, diğer darboğazları gizleyecek yabancı çöplerle çevrilir. (bellek gibi)
Mistik

Yanıtlar:


197

Açıklama , C ++ 'da yazılım optimizasyonunda Agner Fog'dan gelir ve verilere önbellekte nasıl erişildiğini ve depolandığını azaltır.

Şartlar ve ayrıntılı bilgi için, önbelleğe alma hakkındaki wiki girişine bakın, burada daraltacağım.

Önbellek kümeler ve çizgiler halinde düzenlenir . Bir kerede, içerdiği satırlardan herhangi birinin kullanılabileceği sadece bir set kullanılır. Bir çizginin, satır sayısının katlarını yansıtabileceği bellek, bize önbellek boyutunu verir.

Belirli bir bellek adresi için, hangi setin formülü yansıtacağını hesaplayabiliriz:

set = ( address / lineSize ) % numberOfsets

Bu tür bir formül ideal olarak setler arasında tekdüze bir dağılım sağlar, çünkü her bellek adresinin okunma olasılığı yüksektir ( ideal olarak söyledim ).

Çakışmaların olabileceği açıktır. Önbellek kaybında bellek önbellekte okunur ve eski değer değiştirilir. Her kümede, en son kullanılanın yeni okunmuş bellekle üzerine yazıldığı birkaç satır olduğunu unutmayın.

Agner'ın örneğini bir şekilde takip etmeye çalışacağım:

Her kümenin her biri 64 bayt tutan 4 satır olduğunu varsayın. Öncelikle 0x2710sette yazan adresi okumaya çalışıyoruz 28. Sonra da adresleri okumaya teşebbüs 0x2F00, 0x3700, 0x3F00ve 0x4700. Bunların hepsi aynı sete ait. Okumadan önce 0x4700, setteki tüm satırlar dolu olurdu. Bu hafızayı okumak, sette mevcut olan bir çizgiyi, başlangıçta tutan çizgiyi ortaya çıkarır 0x2710. Sorun, (bu örnek için) 0x800ayrı olan adresleri okuduğumuz gerçeğinde yatmaktadır . Bu kritik adımdır (yine bu örnek için).

Kritik adım ayrıca hesaplanabilir:

criticalStride = numberOfSets * lineSize

criticalStrideAynı önbellek satırları için aralıklı değişkenler veya çoklu aralıklar.

Bu teori kısmı. Sonra, açıklama (ayrıca Agner, hata yapmaktan kaçınmak için yakından takip ediyorum):

8kb önbellek, set başına 4 satır * 64 baytlık satır boyutu ile 64x64 (hatırla, efektler önbelleğe göre değişir) matrisini varsayalım. Her satır, matristeki 8 öğeyi (64 bit int) tutabilir .

Kritik adım, matrisin 4 satırına (bellekte sürekli) karşılık gelen 2048 bayt olacaktır.

28. satırı işlediğimizi varsayalım. Bu satırın öğelerini almaya ve 28. sütundaki öğelerle değiştirmeye çalışıyoruz. Satırın ilk 8 öğesi önbellek satırını oluşturuyor, ancak 8 farklı bölüme girecekler Kritik adım 4 satır arayla (bir sütunda 4 ardışık öğe) unutmayın.

Sütunda eleman 16'ya ulaşıldığında (set başına 4 önbellek satırı & 4 satır aralık = sorun) ex-0 öğesi önbellekten çıkartılır. Sütunun sonuna geldiğimizde, önceki tüm önbellek satırları kaybolur ve bir sonraki öğeye erişimde yeniden yükleme yapılması gerekirdi (tüm satırın üzerine yazılır).

Kritik adımın katları olmayan bir boyuta sahip olmak , artık dikeyde kritik adım olan unsurlarla uğraşmadığımız için felaket için bu mükemmel senaryoyu bozuyor, bu yüzden önbellek yeniden yükleme sayısı ciddi ölçüde azalıyor.

Başka bir feragatname - Başımı açıkladım ve umarım çiviledim, ama yanılmış olabilirim. Her neyse, Mysticial'tan bir yanıt (veya onay) bekliyorum . :)


Ah ve bir dahaki sefere. Sadece doğrudan Lounge'a ping at . SO'da adın her örneğini bulamıyorum. :) Bunu yalnızca periyodik e-posta bildirimleriyle gördüm.
Gizemli

Arkadaşlarımın @Mysticial @Luchian Grigore biri onun söylüyor Intel core i3pc üzerinde çalışan Ubuntu 11.04 i386ile hemen hemen aynı performansı gösteren gcc 4.6 yani benim bilgisayar için aynıdır .ve Intel Core 2 Duoile mingw gcc4.4 üzerinde çalışıyor, windows 7(32)ne zaman büyük bir fark gösterir Ashton kapısı Ben bu segment üzerinde çalışan gcc 4.6intel centrino ile biraz eski bir bilgisayar ile derlemek . ubuntu 12.04 i386
Hongxu Chen

Ayrıca, adreslerin 4096'nın katları arasında farklılık gösterdiği bellek erişiminin Intel SnB ailesi CPU'lara yanlış bağımlılığı olduğunu unutmayın. (ör. bir sayfadaki aynı ofset). Bu, bazı işlemler saklandığında verimi düşürebilir, özellikle. yüklerin ve depoların bir karışımı.
Peter Cordes

which goes in set 24bunun yerine " set 28 " demek mi? 32 set olduğunu mu düşünüyorsunuz?
Ruslan

Doğru, 28. :) Ayrıca bağlantılı kağıdı iki kez kontrol ettim, orijinal açıklama için 9.2 Önbellek organizasyonu
Luchian Grigore

78

Luchian, bu davranışın neden olduğunu açıklıyor , ancak bu soruna olası bir çözümü göstermenin ve aynı zamanda önbellek kayıtsız algoritmalar hakkında biraz göstermenin iyi bir fikir olacağını düşündüm.

Algoritmanız temel olarak şunları yapar:

for (int i = 0; i < N; i++) 
   for (int j = 0; j < N; j++) 
        A[j][i] = A[i][j];

Bu da modern bir CPU için korkunç. Bir çözüm, önbellek sisteminizle ilgili ayrıntıları bilmek ve bu sorunları önlemek için algoritmayı değiştirmektir. Bu ayrıntıları bildiğiniz sürece harika çalışıyor .. özellikle taşınabilir değil.

Bundan daha iyisini yapabilir miyiz? Evet yapabiliriz: Bu soruna genel bir yaklaşım , adından da anlaşılacağı gibi, belirli önbellek boyutlarına bağımlı olmaktan kaçınan önbellek habersiz algoritmalardır [1]

Çözüm şöyle görünecektir:

void recursiveTranspose(int i0, int i1, int j0, int j1) {
    int di = i1 - i0, dj = j1 - j0;
    const int LEAFSIZE = 32; // well ok caching still affects this one here
    if (di >= dj && di > LEAFSIZE) {
        int im = (i0 + i1) / 2;
        recursiveTranspose(i0, im, j0, j1);
        recursiveTranspose(im, i1, j0, j1);
    } else if (dj > LEAFSIZE) {
        int jm = (j0 + j1) / 2;
        recursiveTranspose(i0, i1, j0, jm);
        recursiveTranspose(i0, i1, jm, j1);
    } else {
    for (int i = i0; i < i1; i++ )
        for (int j = j0; j < j1; j++ )
            mat[j][i] = mat[i][j];
    }
}

Biraz daha karmaşık, ancak kısa bir test VS2010 x64 sürümü ile eski e8400'ümde oldukça ilginç bir şey gösteriyor, test kodu MATSIZE 8192

int main() {
    LARGE_INTEGER start, end, freq;
    QueryPerformanceFrequency(&freq);
    QueryPerformanceCounter(&start);
    recursiveTranspose(0, MATSIZE, 0, MATSIZE);
    QueryPerformanceCounter(&end);
    printf("recursive: %.2fms\n", (end.QuadPart - start.QuadPart) / (double(freq.QuadPart) / 1000));

    QueryPerformanceCounter(&start);
    transpose();
    QueryPerformanceCounter(&end);
    printf("iterative: %.2fms\n", (end.QuadPart - start.QuadPart) / (double(freq.QuadPart) / 1000));
    return 0;
}

results: 
recursive: 480.58ms
iterative: 3678.46ms

Düzenleme: Boyutu etkisi hakkında: Bu hala bir dereceye kadar fark rağmen çok daha az belirgindir, çünkü biz yinelenen çözüm 1'e kadar tekrarlayan yerine bir yaprak düğüm olarak (özyinelemeli algoritmalar için her zamanki optimizasyon) kullanıyoruz. LEAFSIZE = 1 değerini ayarlarsak, önbelleğin benim için bir etkisi olmaz [ 8193: 1214.06; 8192: 1171.62ms, 8191: 1351.07ms- bu hata payının içindedir, dalgalanmalar 100 ms'dir; bu "karşılaştırmalı değerlendirme", tamamen doğru değerler istiyorsak çok rahat edeceğim bir şey değildir])

[1] Bu tür kaynaklar: Leiserson ve co ile birlikte çalışmış birinden ders alamazsanız .. Kağıtlarının iyi bir başlangıç ​​noktası olduğunu varsayıyorum. Bu algoritmalar hala oldukça nadiren açıklanmaktadır - CLR'nin onlar hakkında tek bir dipnotu vardır. Yine de insanları şaşırtmak için harika bir yol.


Düzenleme (not: Ben bu cevabı gönderen değilim; Sadece eklemek istedim):
İşte yukarıdaki kodun tam bir C ++ sürümü:

template<class InIt, class OutIt>
void transpose(InIt const input, OutIt const output,
    size_t const rows, size_t const columns,
    size_t const r1 = 0, size_t const c1 = 0,
    size_t r2 = ~(size_t) 0, size_t c2 = ~(size_t) 0,
    size_t const leaf = 0x20)
{
    if (!~c2) { c2 = columns - c1; }
    if (!~r2) { r2 = rows - r1; }
    size_t const di = r2 - r1, dj = c2 - c1;
    if (di >= dj && di > leaf)
    {
        transpose(input, output, rows, columns, r1, c1, (r1 + r2) / 2, c2);
        transpose(input, output, rows, columns, (r1 + r2) / 2, c1, r2, c2);
    }
    else if (dj > leaf)
    {
        transpose(input, output, rows, columns, r1, c1, r2, (c1 + c2) / 2);
        transpose(input, output, rows, columns, r1, (c1 + c2) / 2, r2, c2);
    }
    else
    {
        for (ptrdiff_t i1 = (ptrdiff_t) r1, i2 = (ptrdiff_t) (i1 * columns);
            i1 < (ptrdiff_t) r2; ++i1, i2 += (ptrdiff_t) columns)
        {
            for (ptrdiff_t j1 = (ptrdiff_t) c1, j2 = (ptrdiff_t) (j1 * rows);
                j1 < (ptrdiff_t) c2; ++j1, j2 += (ptrdiff_t) rows)
            {
                output[j2 + i1] = input[i2 + j1];
            }
        }
    }
}

2
Özyinelemeli ve yinelemeli olmayan farklı boyutlardaki matrisler arasındaki süreleri karşılaştırırsanız bu anlamlı olacaktır. Yinelenen çözümü belirtilen boyutlarda bir matriste deneyin.
Luchian Grigore

@Luchian Davranışı neden gördüğünü zaten açıkladığınızdan beri , genel olarak bu soruna bir çözüm getirmenin oldukça ilginç olduğunu düşündüm.
Voo

Çünkü, daha büyük bir matrisin neden daha hızlı bir algoritma aramak yerine işlemek için daha kısa sürdüğünü
soruyorum

@Luchian 16383 ve 16384 arasındaki farklar .. 28 vs 27ms burada, ya da yaklaşık% 3.5 - gerçekten önemli değil. Ve olsaydı şaşırırdım.
Voo

3
Bunun ne olduğunu açıklamak ilginç olabilir recursiveTranspose, yani küçük karolar ( LEAFSIZE x LEAFSIZEboyut) üzerinde çalışarak önbelleği doldurmuyor .
Matthieu M.10

60

Luchian Grigore'un cevabındaki açıklamanın bir örneği olarak, 64x64 ve 65x65 matrislerinin iki vakası için matris önbellek varlığı nasıl görünüyor (sayılarla ilgili ayrıntılar için yukarıdaki bağlantıya bakın).

Aşağıdaki animasyonlardaki renkler şu anlama gelir:

  • beyaz - önbellekte değil,
  • açık yeşil - önbellekte,
  • parlak yeşil - önbellek isabet,
  • Portakal - sadece RAM'den okuyun,
  • kırmızı - önbellek özledim.

64x64 kasa:

64x64 matris için önbellek varlığı animasyonu

Neredeyse yeni bir satıra her erişimin önbellek kaybıyla sonuçlandığına dikkat edin . Ve şimdi normal durum nasıl görünüyor, 65x65 matris:

65x65 matris için önbellek varlığı animasyonu

Burada, ilk ısınma sonrasındaki erişimlerin çoğunun önbellek isabetleri olduğunu görebilirsiniz. CPU önbelleğinin genel olarak çalışması amaçlanmıştır.


Yukarıdaki animasyonlar için kareler oluşturan kod burada görülebilir .


Dikey tarama önbellek isabet neden ilk durumda kaydedilmiyor, ancak ikinci durumda? Her iki örnekteki çoğu blok için belirli bir bloğa tam olarak bir kez erişilmiş gibi görünüyor.
Josiah Yoder

@ LuchianGrigore'un cevabından görebiliyorum, çünkü sütundaki tüm çizgiler aynı kümeye ait.
Josiah Yoder

Evet, harika illüstrasyon. Aynı hızda olduklarını görüyorum. Ama aslında değiller, değil mi?
kelalaka

@kelalaka evet, animasyon FPS aynı. Yavaşlamayı simüle etmedim, burada sadece renkler önemlidir.
Ruslan

Farklı önbellek kümelerini gösteren iki statik görüntüye sahip olmak ilginç olurdu.
Josiah Yoder
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.