Sıralanmış bir diziyi sıralanmamış bir diziden daha hızlı işlemek neden daha hızlı?

İşte çok tuhaf görünen bir parça C ++ kodu. Bazı garip sebeplerden dolayı, verileri mucizevi bir şekilde sıralamak, kodu neredeyse altı kat daha hızlı yapar.

 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); } } 

Biraz benzer, ancak daha az aşırı sonuç.


İlk düşüncem, sıralama işleminin verileri önbelleğe getirdiği, ancak daha sonra ne kadar aptal olduğunu düşündüm, çünkü dizi henüz oluşturuldu.

  • Neler oluyor
  • Sıralanan dizi neden sıralanmamış diziden daha hızlı işleniyor?
  • Kod bazı bağımsız terimleri özetlemektedir ve düzen önemli değildir.
22512
27 июня '12 в 16:51 2012-06-27 16:51 GManNickG 27 Haziran 12'de saat 4:51'te ayarlandı 2012-06-27 16:51
@ 26 cevaplar

Daldan sapmanın kurbanı sizsiniz.


Dal tahmini nedir?

Demiryolu kavşağını düşünün:

2019

27 июня '12 в 16:56 2012-06-27 16:56 Cevap Mysticial tarafından 27 Haziran 'da 12:56 da 04:56 2012-06-27 16:56 tarihinde verilmiştir.

Dalların tahmini.

Sıralanmış bir diziyle, data[c] >= 128 koşulu, değer dizgisi için ilk false olur ve ardından sonraki tüm değerler için true olur. Tahmin etmek kolaydır. Sıralanmamış bir diziyle, dallanma maliyetlerini ödersiniz.

3815
27 июня '12 в 16:54 2012-06-27 16:54 Cevap Daniel Fischer tarafından 27 Haziran 12:40 saat 04:54 2012-06-27 16:54 tarihinde verilmiştir.

Verileri sıralarken performansın çarpıcı şekilde artmasının nedeni , Mysticial'in mükemmel bir şekilde açıkladığı gibi dal tahmini için cezanın ortadan kaldırılmasıdır.

Şimdi, koda bakarsak

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

Bu özel anlamın anlamını bulabiliriz, if... else... şube, koşul karşılandığında bir şeyler eklemek demektir. Bu tür cmovl , x86 sisteminde koşullu bir hareket talimatına ( cmovl derlenecek olan koşullu bir operatöre kolayca dönüştürülebilir. Branş ve dolayısıyla branşın öngörülmesi için olası ceza kaldırılır.

Bu nedenle, C++ , C++ , x86'da koşullu bir hareket komutunda derlenecek olan (herhangi bir optimizasyon olmadan) doğrudan olan operatör bir üçlü operatördür ...?... :... ...?... :... Bu nedenle, yukarıdaki ifadeyi eşdeğer olacak şekilde yeniden yazıyoruz:

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

Okunabilirliği koruyarak, ivme faktörünü kontrol edebiliriz.

Intel Core i7 -2600K @ 3.4 GHz ve Visual Studio 2010 sürüm modu referans testi (Mysticial'dan kopyalanan format) için:

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ç, birkaç testte güvenilirdir. Dallanma sonucu öngörülemez olduğunda önemli bir hızlanma elde ediyoruz, ancak öngörülebildiğinde biraz acı çekiyoruz. Aslında, koşullu bir hareket kullanılırken performans, veri deseninden bağımsız olarak aynı kalır.

Şimdi daha yakından bakalım, ürettikleri x86 yapısını keşfedelim. Basit max1 , max2 ve max2 üzere iki işlev max2 .

max1 koşullu dalı kullanır, if... else... :

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

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

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

X86-64 bilgisayarında, GCC -S aşağıda bir montaj yapar.

 :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 

max2 kullanımı nedeniyle daha az kod kullanır. Ancak asıl kazanç, max2 max2 , jmp üzerindeki geçişleri max2 ; bu, tahmin edilen sonuç max2 ise, önemli max2 performansına yol açabilir.

Öyleyse neden koşullu hareket daha iyi çalışıyor?

Tipik bir x86 işlemcide, x86 yürütme birkaç aşamaya ayrılır. Kabaca, farklı aşamalar için farklı donanımlarımız var. Bu nedenle, bir talimatın sonunun yenisini başlatmasını beklememiz gerekmez. Buna boru hattı denir.

Dallanma durumunda, bir sonraki komut bir öncekiyle belirlenir, bu yüzden boru hattını gerçekleştiremeyiz. Beklemeli ya da tahmin etmeliyiz.

Koşullu bir hareket durumunda, koşullu hareket komutunun yürütülmesi birkaç aşamaya ayrılır, ancak Fetch ve Decode gibi önceki aşamalar önceki komutun sonucuna bağlı değildir; sadece son aşamalar bir sonuca ihtiyaç duyar. Bu nedenle, bir komutun yürütme zamanının bir bölümünü bekliyoruz. Bu, koşullu taşıma sürümünün, tahmin yapmak kolay olduğunda şubeden daha yavaş olmasının nedeni budur.

Bilgisayar Sistemleri: Bir Programcı Beklentisi, ikinci baskı, bunu detaylı olarak açıklar. Koşullu Hareket Talimatları için Bölüm 3.6.6'yı, İşlemci Mimarisi için Bölüm 4'ün tamamını ve Öngörü Cezaları ve Hatalı Tahmin için özel işlem için Bölüm 5.11.2'yi kontrol edebilirsiniz.

Bazen bazı modern derleyiciler daha yüksek performansa sahip yapı kodumuzu optimize edebilir, bazen bazı derleyiciler yapamaz (bu kod kendi Visual Studio derleyicisini kullanır). Dallanma ve koşullu hareket arasındaki performans farkını bilmek, tahmin edilemez olduğunda, komut dosyası derleyici tarafından otomatik olarak optimize edilemeyecek kadar karmaşık hale geldiğinde daha iyi performansla kod yazmamıza yardımcı olabilir.

3064
28 июня '12 в 5:14 2012-06-28 05:14 Cevap WiSaGaN tarafından 28 Haziran, 12'de , 05 : 14'te 2012-06-28 05:14

Bu kodla yapılabilecek daha fazla optimizasyonla ilgileniyorsanız, aşağıdakileri göz önünde bulundurun:

İlk döngüden başlayarak:

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

Çevrim permutasyonu ile bu çevrimi güvenle değiştirebiliriz:

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

Daha sonra, i döngüsünün yürütülmesi sırasında if koşulunun sabit olduğunu görebilirsiniz, böylece if çıkarabilirsiniz:

 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ğini varsayarak iç döngünün tek bir ifadede daraltılabileceğini göreceksiniz (örneğin, / fp: fast).

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

Öncesinden 100.000 kat daha hızlı.

2105
03 июля '12 в 5:25 2012-07-03 05:25 Cevap, vulcan raven'e 3 Temmuz, 12'de, 05:25 de 2012-07-03 05:25

Hiç kuşkusuz, bazılarımız bir CPU tahmin işlemcisi için sorunlu olan kodları tanımlama yollarıyla ilgilenecektir. Valgrind cachegrind aracı, --branch-sim=yes bayrağı kullanılarak etkinleştirilen dal belirleyici dal sözdizimine sahiptir. Bu sorudaki örneklere bakarak, 10.000'e düşürülen ve g++ ile derlenen harici döngü sayısı aşağıdaki sonuçları verir:

Şuna göre sırala:

 ==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% ) 

Boylanmamış:

 ==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_annotate tarafından yaratılan doğrusal çıktıya cg_annotate , söz konusu döngü için şunu görürüz:

Şuna göre sırala:

  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]; . . . . } . . . . } 

Boylanmamış:

  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, sorun satırını kolayca tanımlamanıza olanak tanır - sıralanmamış bir sürümde, if (data[c] >= 128) , önbellek dalı dallandırma modelinin bir parçası olarak yanlış öngörülen koşullu dallara ( Bcm ) neden olur; oysa sıralı olarak yalnızca 10,006 sürümü.


Alternatif olarak, Linux'ta, aynı görevi gerçekleştirmek için bir performans sayacı alt sistemi kullanabilirsiniz, ancak CPU sayaçlarını kullanarak kendi performansıyla.

 perf stat ./sumtest_sorted 

Şuna göre sırala:

  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 

Boylanmamış:

  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 

Aynı zamanda demontaj ile kaynak kodu ek açıklamaları oluşturabilir.

 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) ... 

Ayrıntılar için performans kılavuzuna bakın .

1758
12 окт. cevap verilen caf 12 ekim. 2012-10-12 08:53 '12, 08:53 2012-10-12 08:53

Ben sadece bu soruyu ve cevaplarını okudum ve cevabın eksik olduğunu hissediyorum.

Özellikle yönetilen dillerde iyi çalıştığı düşünülen dal tahminini ortadan kaldırmanın genel yolu, dallanma kullanmak yerine tabloyu araştırmaktır (bu durumda kontrol etmeme rağmen).

Bu yaklaşım genel olarak şu durumlarda çalışır:

  1. bu küçük bir tablodur ve büyük olasılıkla işlemcide önbelleğe alınır ve
  2. Oldukça dar bir döngüde çalışıyorsunuz ve / veya işlemci verileri önceden yükleyebilir.

Arkaplan ve neden

İşlemci açısından, hafızanız yavaş. Hız farkını telafi etmek için, işlemcinize birkaç önbellek (L1 / L2 önbellek) yerleştirilmiştir. Öyleyse iyi hesaplamalarınızı yaptığınızı ve bir parça belleğe ihtiyacınız olduğunu öğrenin. İşlemci yükleme işlemini gerçekleştirecek ve belleğin bir kısmını önbelleğe yükleyecektir ve ardından kalan hesaplamaları yapmak için önbelleği kullanacaktır. Bellek nispeten yavaş olduğundan, bu "yükleme" programınızı yavaşlatır.

Dal tahmini gibi, Pentium işlemcilerde de optimize edildi: işlemci bazı verilerin yüklenmesi gerektiğini öngörüyor ve işlem gerçekten önbelleğe girmeden önce bunları önbelleğe yüklemeye çalışıyor. Görüldüğü gibi, şube tahmini bazen çok yanlış gider - en kötü durumda, geri dönmeniz ve gerçekte sonsuza dek sürecek bir bellek yükünü beklemeniz gerekir ( başka bir deyişle: başarısız bir şube tahmini kötüdür, şube tahmini başarısızlığından sonraki bellek yükü korkunç! ).

Neyse ki bizim için, eğer hafıza erişim planı öngörülebilir ise, işlemci onu hızlı önbelleğine yükler ve her şey yolundadır.

Bilmemiz gereken ilk şey az olduğu mu? Daha küçük bir boyut genellikle daha iyi olsa da, kuralın kural arama tablolarına <= 4096 byte bağlı kalmaktır. Üst sınır olarak: eğer referans tablonuz 64 KB'den büyükse, muhtemelen revize edilmelidir.

Masayı kur

Böylece küçük bir masa yaratabileceğimizi öğrendik. Yapılacak bir sonraki şey, arama işlevini değiştirmek. Arama işlevleri genellikle birkaç temel tamsayılı işlem kullanan küçük işlevlerdir (ve, veya xor, shift, add, remove ve muhtemelen çarpma). Masanızda bir tür "benzersiz anahtar" arayarak girişinizin çevrilmesini istiyorsunuz; bu da size yapmasını istediğiniz tüm çalışmalara cevap verecektir.

Bu durumda:> = 128 değeri kaydedebileceğimiz anlamına gelir, <128 ise ondan kurtulacağımız anlamına gelir. Bunu yapmanın en kolay yolu 'VE' kullanmaktır: eğer kurtarırsak, biz Ve bu 7FFFFFFF ile olur; если мы хотим избавиться от него, мы И это с 0. Отметим также, что 128 - это степень 2 - так что мы можем пойти дальше и составить таблицу из 32768/128 целых чисел и заполнить ее одним нулем и большим количеством 7FFFFFFFF годов.

Управляемые языки