popcount
Büyük veri dizilerine giden en hızlı yolu arıyordum . Ben karşılaştı çok garip bir etkisi: den döngü değişkeni değiştirme unsigned
için uint64_t
benim PC'de% 50 tarafından yapılan performans düşüşü.
Kıyaslamak
#include <iostream>
#include <chrono>
#include <x86intrin.h>
int main(int argc, char* argv[]) {
using namespace std;
if (argc != 2) {
cerr << "usage: array_size in MB" << endl;
return -1;
}
uint64_t size = atol(argv[1])<<20;
uint64_t* buffer = new uint64_t[size/8];
char* charbuffer = reinterpret_cast<char*>(buffer);
for (unsigned i=0; i<size; ++i)
charbuffer[i] = rand()%256;
uint64_t count,duration;
chrono::time_point<chrono::system_clock> startP,endP;
{
startP = chrono::system_clock::now();
count = 0;
for( unsigned k = 0; k < 10000; k++){
// Tight unrolled loop with unsigned
for (unsigned i=0; i<size/8; i+=4) {
count += _mm_popcnt_u64(buffer[i]);
count += _mm_popcnt_u64(buffer[i+1]);
count += _mm_popcnt_u64(buffer[i+2]);
count += _mm_popcnt_u64(buffer[i+3]);
}
}
endP = chrono::system_clock::now();
duration = chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
cout << "unsigned\t" << count << '\t' << (duration/1.0E9) << " sec \t"
<< (10000.0*size)/(duration) << " GB/s" << endl;
}
{
startP = chrono::system_clock::now();
count=0;
for( unsigned k = 0; k < 10000; k++){
// Tight unrolled loop with uint64_t
for (uint64_t i=0;i<size/8;i+=4) {
count += _mm_popcnt_u64(buffer[i]);
count += _mm_popcnt_u64(buffer[i+1]);
count += _mm_popcnt_u64(buffer[i+2]);
count += _mm_popcnt_u64(buffer[i+3]);
}
}
endP = chrono::system_clock::now();
duration = chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
cout << "uint64_t\t" << count << '\t' << (duration/1.0E9) << " sec \t"
<< (10000.0*size)/(duration) << " GB/s" << endl;
}
free(charbuffer);
}
Gördüğünüz gibi, rastgele bir arabellek oluşturuyoruz, boyutu komut satırından okunan x
megabayt x
. Daha sonra, arabellek üzerinde tekrarlar popcount
ve popcount gerçekleştirmek için x86 intrinsic bir unrolls sürümünü kullanırız. Daha kesin bir sonuç elde etmek için popcount'u 10.000 kez yapıyoruz. Popcount zamanlarını ölçüyoruz. Büyük harf, iç döngü değişkeni, unsigned
küçük harf iç döngü değişkendir uint64_t
. Bunun bir fark yaratmaması gerektiğini düşündüm, ama tam tersi.
(Kesinlikle çılgın) sonuçlar
Böyle derlemek (g ++ sürümü: Ubuntu 4.8.2-19ubuntu1):
g++ -O3 -march=native -std=c++11 test.cpp -o test
İşte Haswell Core i7-4770K CPU'mun @ 3.50 GHz'deki sonuçları test 1
(1 MB rasgele veri):
- imzasız 41959360000 0.401554 sn 26.113 GB / s
- uint64_t 41959360000 0.759822 sn 13.8003 GB / s
Gördüğünüz gibi, verimi uint64_t
sürümü olan sadece yarısı biri unsigned
sürümü! Sorun şu ki, farklı montaj üretiliyor, ama neden? İlk olarak, bir derleyici hata düşündüm, bu yüzden denedim clang++
(Ubuntu Clang sürüm 3.4-1ubuntu3):
clang++ -O3 -march=native -std=c++11 teest.cpp -o test
Sonuç: test 1
- imzasız 41959360000 0,3998293 sn 26,3267 GB / s
- uint64_t 41959360000 0.680954 saniye 15.3986 GB / s
Yani, neredeyse aynı sonuç ve hala garip. Ama şimdi çok garipleşiyor. Ben bir sabit girdi ile okunan arabellek boyutunu değiştirin 1
, bu yüzden değiştirmek:
uint64_t size = atol(argv[1]) << 20;
için
uint64_t size = 1 << 20;
Böylece, derleyici derleme zamanında tampon boyutunu bilir. Belki bazı optimizasyonlar ekleyebilir! İşte numaraları g++
:
- imzasız 41959360000 0.509156 sn 20.5944 GB / s
- uint64_t 41959360000 0.508673 sn 20.6139 GB / s
Şimdi, her iki versiyon da eşit derecede hızlı. Ancak, unsigned
daha da yavaş var ! Bu bırakılan 26
için 20 GB/s
böylece sabit bir değer kurşun olmayan bir sabit yerini deoptimization . Cidden, burada neler olduğuna dair hiçbir fikrim yok! Ama şimdi clang++
yeni sürümle:
- imzasız 41959360000 0.677009 sn 15.4884 GB / s
- uint64_t 41959360000 0.676909 saniye 15.4906 GB / s
Bir dakika ne? Şimdi, her iki sürüm de 15 GB / sn'lik yavaş sayıya düştü . Böylece, sabit olmayan bir değeri sabit bir değerle değiştirmek her iki durumda da Clang için yavaş koda yol açar !
Ivy Bridge CPU'lu bir meslektaşımdan karşılaştırmamı derlemesini istedim. Benzer sonuçlar aldı, bu yüzden Haswell gibi görünmüyor. İki derleyici burada garip sonuçlar ürettiğinden, aynı zamanda bir derleyici hatası gibi görünmüyor. Burada bir AMD CPU'muz yok, bu yüzden sadece Intel ile test yapabildik.
Daha fazla delilik, lütfen!
İlk örneği (ile olanı atol(argv[1])
) alın ve static
değişkenin önüne a koyun , yani:
static uint64_t size=atol(argv[1])<<20;
İşte g ++ sonuçları:
- imzasız 41959360000 0.396728 sn 26.4306 GB / s
- uint64_t 41959360000 0.509484 saniye 20.5811 GB / s
Yay, başka bir alternatif . Hala 26 GB / s'lik hızlı bir hıza sahibiz u32
, ancak u64
en azından 13 GB / s'den 20 GB / s sürümüne geçmeyi başardık ! Meslektaşımın bilgisayarında, u64
sürüm versiyondan daha hızlı hale geldi u32
ve en hızlı sonucu verdi. Ne yazık ki, bu sadece işe yarıyor g++
, clang++
umurumda değil gibi görünüyor static
.
Benim sorum
Bu sonuçları açıklayabilir misiniz? Özellikle:
- Nasıl arasında böyle bir fark var olabilir
u32
veu64
? - Sabit olmayan bir aralığın sabit bir arabellek boyutuyla değiştirilmesi daha az optimum kodu nasıl tetikleyebilir ?
static
Anahtar kelimenin eklenmesiu64
döngüyü nasıl daha hızlı hale getirebilir ? Meslektaşımın bilgisayarındaki orijinal koddan daha hızlı!
Optimizasyonun zor bir bölge olduğunu biliyorum, ancak böyle küçük değişikliklerin yürütme süresinde % 100 fark yaratabileceğini ve sabit bir tampon boyutu gibi küçük faktörlerin sonuçları tekrar tamamen karıştırabileceğini asla düşünmedim . Tabii ki, her zaman 26 GB / s popcount edebilen sürüme sahip olmak istiyorum. Düşünebildiğim tek güvenilir yol, bu kasa için kopyala yapıştırma tertibatını ve satır içi montajı kullanmaktır. Küçük değişikliklere deli gibi görünen derleyicilerden kurtulmanın tek yolu budur. Ne düşünüyorsun? Kodu en yüksek performansla güvenilir bir şekilde almanın başka bir yolu var mı?
Demontaj
İşte çeşitli sonuçlar için sökme:
G ++ / u32 / sabit olmayan bufsize'den 26 GB / s sürüm :
0x400af8:
lea 0x1(%rdx),%eax
popcnt (%rbx,%rax,8),%r9
lea 0x2(%rdx),%edi
popcnt (%rbx,%rcx,8),%rax
lea 0x3(%rdx),%esi
add %r9,%rax
popcnt (%rbx,%rdi,8),%rcx
add $0x4,%edx
add %rcx,%rax
popcnt (%rbx,%rsi,8),%rcx
add %rcx,%rax
mov %edx,%ecx
add %rax,%r14
cmp %rbp,%rcx
jb 0x400af8
G ++ / u64 / sabit olmayan bufsize'den 13 GB / s sürümü :
0x400c00:
popcnt 0x8(%rbx,%rdx,8),%rcx
popcnt (%rbx,%rdx,8),%rax
add %rcx,%rax
popcnt 0x10(%rbx,%rdx,8),%rcx
add %rcx,%rax
popcnt 0x18(%rbx,%rdx,8),%rcx
add $0x4,%rdx
add %rcx,%rax
add %rax,%r12
cmp %rbp,%rdx
jb 0x400c00
Clang ++ / u64 / sabit olmayan bufsize'den 15 GB / s sürüm :
0x400e50:
popcnt (%r15,%rcx,8),%rdx
add %rbx,%rdx
popcnt 0x8(%r15,%rcx,8),%rsi
add %rdx,%rsi
popcnt 0x10(%r15,%rcx,8),%rdx
add %rsi,%rdx
popcnt 0x18(%r15,%rcx,8),%rbx
add %rdx,%rbx
add $0x4,%rcx
cmp %rbp,%rcx
jb 0x400e50
G ++ / u32 & u64 / const bufsize'den 20 GB / s sürüm :
0x400a68:
popcnt (%rbx,%rdx,1),%rax
popcnt 0x8(%rbx,%rdx,1),%rcx
add %rax,%rcx
popcnt 0x10(%rbx,%rdx,1),%rax
add %rax,%rcx
popcnt 0x18(%rbx,%rdx,1),%rsi
add $0x20,%rdx
add %rsi,%rcx
add %rcx,%rbp
cmp $0x100000,%rdx
jne 0x400a68
Clang ++ / u32 & u64 / const bufsize sürümünden 15 GB / sn sürümü :
0x400dd0:
popcnt (%r14,%rcx,8),%rdx
add %rbx,%rdx
popcnt 0x8(%r14,%rcx,8),%rsi
add %rdx,%rsi
popcnt 0x10(%r14,%rcx,8),%rdx
add %rsi,%rdx
popcnt 0x18(%r14,%rcx,8),%rbx
add %rdx,%rbx
add $0x4,%rcx
cmp $0x20000,%rcx
jb 0x400dd0
İlginçtir, en hızlı (26 GB / s) sürüm de en uzundur! Kullanan tek çözüm gibi görünüyor lea
. Bazı sürümler jb
atlamak için, bazıları ise atlamak için kullanılır jne
. Ancak bunun dışında tüm sürümler karşılaştırılabilir gibi görünüyor. % 100 performans boşluğunun nereden kaynaklanabileceğini görmüyorum, ancak meclisi deşifre etme konusunda çok becerikli değilim. En yavaş (13 GB / s) sürüm çok kısa ve iyi görünüyor. Bunu açıklayan var mı?
Dersler öğrenildi
Bu sorunun cevabı ne olursa olsun; Gerçekten sıcak döngülerde her detayın önemli olduğunu öğrendim , hatta sıcak kodla herhangi bir ilişkisi yok gibi görünen detaylar bile . Bir döngü değişkeni için ne tür kullanacağımı hiç düşünmedim, ancak gördüğünüz gibi küçük bir değişiklik % 100 fark yaratabilir! static
Anahtar kelimenin boyut değişkeninin önüne eklenmesiyle gördüğümüz gibi, bir arabellek depolama türü bile büyük bir fark yaratabilir ! Gelecekte, sistem performansı için çok önemli olan çok sıkı ve sıcak döngüler yazarken her zaman çeşitli derleyiciler üzerinde çeşitli alternatifleri test edeceğim.
İlginç olan şey, döngüyü dört kez açmış olmama rağmen performans farkının hala çok yüksek olması. Böylece, kilidini açsanız bile, yine de büyük performans sapmalarından etkilenebilirsiniz. Oldukça ilginç.