Başka bir Stack Overflow sorusuna cevap verdim ( bu soru ) İlginç bir alt soruna rastladım. 6 tamsayı dizisini sıralamanın en hızlı yolu nedir?
Soru çok düşük olduğundan:
- kütüphanelerin kullanılabilir olduğunu (ve çağrının kendisinin maliyetinin olduğunu) varsayamayız, sadece düz C
- ( çok yüksek bir maliyeti olan) talimat boru hattının boşaltılmasını önlemek için muhtemelen dalları, sıçramaları ve diğer her türlü kontrol akışının kesilmesini en aza indirmeliyiz (
&&
veya sıra noktalarının arkasında gizli olanlar gibi||
). - oda sınırlıdır ve kayıtları ve bellek kullanımını en aza indirmek bir konudur, ideal olarak yer sıralaması muhtemelen en iyisidir.
Gerçekten bu soru, hedefin kaynak uzunluğunu en aza indirmek değil, yürütme süresini en aza indirmek olduğu bir tür Golf. Ben Michael Zrash ve onun devamı tarafından Kod Optimizasyonu Zen kitabının başlığında kullanılan 'Zening' kodu diyorum .
Neden ilginç olduğuna gelince, birkaç katman var:
- örnek basit ve anlaşılması ve ölçülmesi kolaydır, fazla C becerisi yoktur
- sorun için iyi bir algoritma seçiminin etkilerini değil, aynı zamanda derleyicinin ve temel donanımın etkilerini de gösterir.
İşte benim referans (naif, optimize edilmemiş) uygulaması ve test setim.
#include <stdio.h>
static __inline__ int sort6(int * d){
char j, i, imin;
int tmp;
for (j = 0 ; j < 5 ; j++){
imin = j;
for (i = j + 1; i < 6 ; i++){
if (d[i] < d[imin]){
imin = i;
}
}
tmp = d[j];
d[j] = d[imin];
d[imin] = tmp;
}
}
static __inline__ unsigned long long rdtsc(void)
{
unsigned long long int x;
__asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
return x;
}
int main(int argc, char ** argv){
int i;
int d[6][5] = {
{1, 2, 3, 4, 5, 6},
{6, 5, 4, 3, 2, 1},
{100, 2, 300, 4, 500, 6},
{100, 2, 3, 4, 500, 6},
{1, 200, 3, 4, 5, 600},
{1, 1, 2, 1, 2, 1}
};
unsigned long long cycles = rdtsc();
for (i = 0; i < 6 ; i++){
sort6(d[i]);
/*
* printf("d%d : %d %d %d %d %d %d\n", i,
* d[i][0], d[i][6], d[i][7],
* d[i][8], d[i][9], d[i][10]);
*/
}
cycles = rdtsc() - cycles;
printf("Time is %d\n", (unsigned)cycles);
}
Ham sonuçlar
Varyant sayısı büyük hale geliyor, ben bulunabilir bir test paketi hepsini topladı burada . Kullanılan gerçek testler, Kevin Stock sayesinde yukarıda gösterilenlerden biraz daha az saftır. Kendi ortamınızda derleyebilir ve yürütebilirsiniz. Farklı hedef mimariler / derleyiciler üzerindeki davranışlarla oldukça ilgileniyorum. (Tamam çocuklar, cevaplara koyun, yeni bir sonuç kümesinin tüm katılımcılarını + 1'leyeceğim).
Bir yıl önce Daniel Stutzbach'a (golf için) cevap verdim, çünkü o sırada en hızlı çözümün kaynağıydı (sıralama ağları).
Linux 64 bit, gcc 4.6.1 64 bit, Intel Core 2 Duo E8400, -O2
- Qsort kütüphane işlevine doğrudan çağrı: 689.38
- Saf uygulama (ekleme türü): 285.70
- Ekleme Sıralaması (Daniel Stutzbach): 142.12
- Ekleme Sıralama Açıldı: 125.47
- Sıralama Düzeni: 102.26
- Kayıtlı Rank Order: 58.03
- Sıralama Ağları (Daniel Stutzbach): 111.68
- Sıralama Ağları (Paul R): 66.36
- Hızlı Değiştirme ile Ağları Sıralama 12: 58.86
- Sıralama Ağları 12 yeniden sıralandı Takas: 53.74
- Sıralama Ağları 12 yeniden sıralandı Basit Değiştirme: 31.54
- Hızlı swap ile yeniden sıralanan Sıralama Ağı: 31.54
- Hızlı değiştirilebilen V2 ile yeniden sıralanan Sıralama Ağı: 33.63
- Eğik Kabarcık Sıralaması (Paolo Bonzini): 48.85
- Unrolled Ekleme Sıralaması (Paolo Bonzini): 75.30
Linux 64 bit, gcc 4.6.1 64 bit, Intel Core 2 Duo E8400, -O1
- Qsort kütüphane fonksiyonuna doğrudan çağrı: 705.93
- Saf uygulama (ekleme türü): 135.60
- Ekleme Sıralaması (Daniel Stutzbach): 142.11
- Ekleme Sıralama Açıldı: 126.75
- Sıralama Düzeni: 46.42
- Kayıtlı Sıralama Düzeni: 43.58
- Sıralama Ağları (Daniel Stutzbach): 115.57
- Sıralama Ağları (Paul R): 64.44
- Hızlı Değiştirme ile Ağları Sıralama 12: 61.98
- Sıralama Ağları 12 yeniden sıralandı Takas: 54.67
- Sıralama Ağları 12 yeniden sıralandı Basit Değiştirme: 31.54
- Hızlı swap ile yeniden sıralanan Sıralama Ağı: 31.24
- Hızlı değiştirilebilir V2 ile yeniden sıralanan Sıralama Ağı: 33.07
- Eğik Kabarcık Sıralaması (Paolo Bonzini): 45.79
- Unrolled Ekleme Sıralaması (Paolo Bonzini): 80.15
Hem -O1 hem de -O2 sonuçlarını dahil ettim çünkü şaşırtıcı bir şekilde birkaç program için O2, O1'den daha az verimlidir. Acaba bu etkinin belirli bir optimizasyonu var mı?
Önerilen çözümlerle ilgili yorumlar
Ekleme Sıralaması (Daniel Stutzbach)
Beklendiği gibi dalları en aza indirmek gerçekten iyi bir fikirdir.
Sıralama Ağları (Daniel Stutzbach)
Yerleştirme sıralamasından daha iyidir. Ana etkinin dış döngüden kaçınmamasını merak ettim. Kontrol etmek için unrolled ekleme sıralama ile denedim ve gerçekten kabaca aynı rakamlar (kod burada ).
Sıralama Ağları (Paul R)
Şu ana kadar en iyisi. Test etmek için kullandığım gerçek kod burada . Neden diğer sıralama ağ uygulaması neredeyse iki kat daha hızlı olduğunu bilmiyorum. Parametre geçiyor mu? Hızlı maksimum?
Hızlı Değiştirme ile Ağları Sıralama 12 SWAP
Daniel Stutzbach'ın önerdiği gibi, 12 takas sıralama ağını şubesiz hızlı takas ile birleştirdim (kod burada ). Gerçekten daha hızlı, şimdiye kadar en iyi küçük bir marjla (kabaca% 5) 1 daha az takas kullanılarak beklenebileceği gibi.
Ayrıca, dalsız takasın PPC mimarisinde kullanıldığında basit olandan çok (4 kat) daha az etkili olduğunu fark etmek de ilginçtir.
Calling Library qsort
Başka bir referans noktası vermek için ben de sadece kütüphane qsort (kod burada ) çağırmak için önerilen denedim . Beklendiği gibi çok daha yavaş: 10 ila 30 kat daha yavaş ... yeni test paketiyle belirginleştiği için, asıl sorun ilk çağrıdan sonra kütüphanenin ilk yükü gibi görünüyor ve diğerleriyle çok zayıf karşılaştırmıyor sürümü. Linux'umda sadece 3 ila 20 kat daha yavaş. Başkaları tarafından testler için kullanılan bazı mimarilerde bile daha hızlı görünüyor (kütüphane qsort daha karmaşık bir API kullandığından, bunun için gerçekten şaşırdım).
Rütbe sırası
Rex Kerr tamamen farklı bir yöntem önerdi: dizinin her bir öğesi için doğrudan son konumunu hesaplar. Hesaplama sırası sırasının dal gerektirmediği için bu etkilidir. Bu yöntemin dezavantajı, dizinin bellek miktarının üç katını almasıdır (dizinin bir kopyası ve sıralama düzenlerini depolamak için değişkenler). Performans sonuçları çok şaşırtıcı (ve ilginç). 32 bit işletim sistemi ve Intel Core2 Quad E8300 ile referans mimarimde, döngü sayısı 1000'in biraz altındaydı (dallanma takaslı sıralama ağları gibi). Ancak 64 bitlik kutumda (Intel Core2 Duo) derlendiğinde ve yürütüldüğünde çok daha iyi performans gösterdi: şimdiye kadarki en hızlı oldu. Sonunda gerçek nedeni buldum. 32 bitlik kutum gcc 4.4.1 ve 64 bitlik kutum gcc 4.4 kullanıyorum.
güncelleme :
Yukarıdaki yayınlanmış şekillerin gösterdiği gibi, bu etki hala gcc'nin daha sonraki sürümleri ile artmıştır ve Rank Order diğer alternatiflerden iki kat daha hızlı hale gelmiştir.
Yeniden Sıralanmış Takas ile Ağları 12 Sıralama
Gcc 4.4.3 ile Rex Kerr teklifinin inanılmaz etkinliği beni meraklandırdı: 3 kat daha fazla bellek kullanımı olan bir program şubesiz sıralama ağlarından daha hızlı olabilir mi? Benim hipotezim, yazdıktan sonra okunan türden daha az bağımlılığa sahip olması ve x86'nın süperskalar talimat zamanlayıcısının daha iyi kullanılmasına izin vermesiydi. Bu bana bir fikir verdi: yazma bağımlılıklarından sonra okumayı en aza indirmek için swapları yeniden sıralayın. Daha basit bir ifadeyle: bunu SWAP(1, 2); SWAP(0, 2);
yaptığınızda, ikisini de ortak bir bellek hücresine erişebildiğinden, ilk takasın bitmesini beklemeniz gerekir. Bunu yaptığınızda SWAP(1, 2); SWAP(4, 5);
işlemci her ikisini de paralel olarak yürütebilir. Denedim ve beklendiği gibi çalışıyor, sıralama ağları yaklaşık% 10 daha hızlı çalışıyor.
Basit Değiştirme ile Ağları 12 Sıralama
Orijinal görevinden bir yıl sonra Steinar H. Gunderson, derleyiciyi alt etmeye ve takas kodunu basit tutmaya çalışmamamızı önerdi. Ortaya çıkan kod yaklaşık% 40 daha hızlı olduğu için gerçekten iyi bir fikir! Ayrıca, yine de bazı döngüleri yedekleyebilen x86 satır içi montaj kodu kullanılarak elle optimize edilmiş bir takas önerdi. En şaşırtıcı olanı (programcının psikolojisi üzerine ciltler diyor), bir yıl önce hiçbirinin bu takas versiyonunu denememiş olması. Test etmek için kullandığım kod burada . Diğerleri C hızlı takas yazmanın başka yollarını önerdiler, ancak iyi bir derleyiciye sahip basit performansla aynı performansları veriyorlar.
"En iyi" kod şu şekildedir:
static inline void sort6_sorting_network_simple_swap(int * d){
#define min(x, y) (x<y?x:y)
#define max(x, y) (x<y?y:x)
#define SWAP(x,y) { const int a = min(d[x], d[y]); \
const int b = max(d[x], d[y]); \
d[x] = a; d[y] = b; }
SWAP(1, 2);
SWAP(4, 5);
SWAP(0, 2);
SWAP(3, 5);
SWAP(0, 1);
SWAP(3, 4);
SWAP(1, 4);
SWAP(0, 3);
SWAP(2, 5);
SWAP(1, 3);
SWAP(2, 4);
SWAP(2, 3);
#undef SWAP
#undef min
#undef max
}
Test setimize inanırsak (ve evet, oldukça zayıftır, sadece faydası kısa, basit ve neyi ölçtüğümüzü anlamak kolaydır), sonuçta ortaya çıkan kodun bir döngü için ortalama döngü sayısı 40 çevrimin altındadır ( 6 test yapılır). Bu, her bir swap'ı ortalama 4 döngüde koydu. Buna inanılmaz hızlı diyorum. Başka iyileştirme mümkün mü?
__asm__ volatile (".byte 0x0f, 0x31; shlq $32, %%rdx; orq %%rdx, %0" : "=a" (x) : : "rdx");
GCC tek bir 64 bit kayıtta beklerken rdtsc'nin EDX: EAX cevabını koyması nedeniyle olduğunu unutmayın. -O3'te derleyerek hatayı görebilirsiniz. Ayrıca daha hızlı bir SWAP hakkında Paul R'ye yaptığım yoruma bakın.
CMP EAX, EBX; SBB EAX, EAX
0 veya 0xFFFFFFFF değerini koyacaktır . "ödünç ile çıkarma", karşılığı ("taşıma ile ekleme"); Durumunuz başvurmak bit olan taşıma bit. Sonra tekrar, bunu hatırlıyorum ve & Pentium 4 vs. üzerine üretilen iş korkunç gecikme vardı ve ve iki kez hala Core işlemciler üzerinde olarak yavaş. 80386'dan beri koşullu mağaza ve koşullu taşıma talimatları da var, ancak bunlar da yavaş. EAX
EAX
EBX
SBB
ADC
ADC
SBB
ADD
SUB
SETcc
CMOVcc
x-y
vex+y
taşma veya taşmaya neden olmayacağını varsayabilir miyiz ?