2013-05-16 10 views
9

Tek duyarlıklı yüzdelerini bellekten okuması gereken bazı kodları en iyi duruma getirmeye çalışıyorum ve onlarda aritmetiği iki kat hassasiyetle gerçekleştiriyorum. Bu, bellekte verileri tek bir hassasiyet olarak saklayan kod, verileri bellekte çift duyarlık olarak depolayan eşdeğer koddan önemli ölçüde yavaş olduğu için önemli bir performans darboğazı haline geliyor.GCC ve Clang neden cvtss2sd [memory] kullanıyor?

#include <cstdio> 

// noinline to force main() to actually read the value from memory. 
__attributes__ ((noinline)) float* GetFloat() { 
    float* f = new float; 
    *f = 3.14; 
    return f; 
} 

int main() { 
    float* f = GetFloat(); 
    double d = *f; 
    printf("%f\n", d); // Use the value so it isn't optimized out of existence. 
} 

Hem GCC ve clang cvtss2sd talimat kaynak argüman olarak bellek desteklediği halde iki ayrı talimatlar olarak çift duyarlılığa *f yüklenmesini ve dönüştürme işlemi: Aşağıda benim konunun özünü yakalayan bir oyuncak C++ programıdır . Agner Fog, cvtss2sd r, m, çoğu mimaride movss r, m kadar hızlı yürütür ve sonradan cvtss2sd r, r sonradan yürütme gereksinimi vardır. Bununla birlikte, Clang main() için aşağıdaki kodu oluşturur:

main PROC 
     push rbp          ; 
     mov  rbp, rsp        ; 
     call _Z8GetFloatv       ; 
     movss xmm0, dword ptr [rax]     ; 
     cvtss2sd xmm0, xmm0        ; 
     mov  edi, offset ?_001      ; 
     mov  al, 1         ; 
     call printf         ; 
     xor  eax, eax        ; 
     pop  rbp          ; 
     ret            ; 
main ENDP 

GCC benzer verimsiz kodu oluşturur. Neden bu derleyicilerden hiçbiri cvtss2sd xmm0, dword ptr [rax] gibi bir şey üretmiyor?

EDIT: Harika yanıt, Stephen Canon! Gerçek kullanım durumum için Clang'ın assembly dili çıktısını aldım, onu bir satır içi ASM olarak bir kaynak dosyaya yapıştırdım, karşılaştırdım, sonra burada tartışılan değişiklikleri yaptım ve tekrar karşılaştırdım. cvtss2sd [memory]'un aslında daha yavaş olduğuna inanamadım.

cevap

13

Bu aslında bir optimizasyon. CVTSS2SD bellekten, hedef yazmacının yüksek 64 bitini değiştirmeden bırakır. Bu, önemli bir duraklamaya neden olabilecek ve çoğu durumda ILP'yi büyük ölçüde azaltabilen kısmi kayıt güncellemesinin gerçekleştiği anlamına gelir. Öte yandan, MOVSS, bağımlılık kırması olan ve kullanılmayan riskleri ortadan kaldıran ve kullanılmayan bitleri sıfırlar.

Dönüştürmede çift tıklamaya yönelik bir darboğaz olabilir, ancak bu değil.


Ben kısmi kayıt güncelleme bir performans tehlikesi olduğunu tam olarak neden üzerinde biraz genişletmek gerekir.

Aslında gerçekleştirilen ediliyor ne hesaplama hiçbir fikrim yok, ama en çok bu çok basit örnekte benziyor varsayalım:

:

double accumulator, x; 
float y[n]; 
for (size_t i=0; i<n; ++i) { 
    accumulator += x*(double)y[i]; 
} 

döngü "bariz" codegen şuna benzer

loop_begin: 
    cvtss2sd xmm0, [y + 4*i] 
    mulsd xmm0, x 
    addsd accumulator, xmm0 
    // some loop arithmetic that I'll ignore; it isn't important. 

Yalnızca döngü tarafından taşınan bağımlılık toplayıcı güncelleştirmesinde bulunur, dolayısıyla asimptotik olarak döngü, geçerli "tipik" x86 çekirdeklerinde döngü yinelemesi başına 3 döngü olan 1/(addsd gecikme) hızında çalışmalıdır (Agner Fog's tabloları veya In Daha fazla bilgi için tel Optimizasyon Kılavuzu). biz aslında bu talimatların çalışması bakarsak

Ancak, biz xmm0 yüksek 64 bit, onlar ilgilenen sonucu üzerinde hiçbir etkisi yoktur rağmen ikinci döngü taşınan bağımlılık oluşturmak görüyoruz Zincir.Her bir cvtss2sd yönergesi, önceki döngü yinelemesinin mulsd sonucuna ulaşılana kadar başlayamaz; Bu, döngüdeki gerçek hızı 1/(cvtss2sd gecikme süresi + mulsd gecikme süresi) veya normal x86 çekirdeklerinde döngü yinelemesi başına 7 döngü sınırlar (iyi haber, yalnızca dönüşüm gecikme gecikmesini ödemenizdir, çünkü dönüştürme işlemi iki µops içine kırılmış ve yük opop xmm0 üzerinde bir bağımlılığa sahiptir, bu nedenle kaldırılabilir).

Bu döngü işlemini biraz daha açık hale getirmek için aşağıdaki gibi yazabiliriz (cvtss2sd yükünün yarısını görmezden geliyorum, çünkü bu değerler neredeyse sınırsızdır ve her ne zaman daha fazla veya daha az olabilir):

cycle iteration 1 iteration 2 iteration 3 
------------------------------------------------ 
0  cvtss2sd 
1  . 
2  mulsd 
3  . 
4  . 
5  . 
6  . --- xmm0[64:127]--> 
7  addsd   cvtss2sd(*) 
8  .    . 
9  .-- accum -+ mulsd 
10    | . 
11    | . 
12    | . 
13    | . --- xmm0[64:127]--> 
14    +-> addsd   cvtss2sd 
15     .    . 

(*) Aslında işleri basitleştiriyorum; Bunu doğru yapmak için sadece gecikmeyi değil aynı zamanda liman kullanımını da dikkate almalıyız. Ancak, söz konusu durakları göstermek için sadece gecikme yeterliğini göz önünde bulundurarak, bunu basit tutuyorum. Sonsuz ILP kaynakları olan bir makinede çalıştığımızı varsayın. Şimdi

yerine böyle döngü yazma varsayalım:

loop_begin: 
    movss xmm0, [y + 4*i] 
    cvtss2sd xmm0, xmm0 
    mulsd xmm0, x 
    addsd accumulator, xmm0 
    // some loop arithmetic that I'll ignore; it isn't important. 

hafıza bitleri sıfırlar dan movss Çünkü [32: 127] xmm0 arasında orada artık xmm0 bir döngü taşınan bağımlılık, bu yüzden beklendiği gibi birikim gecikme ile bağlıdır; Benim oyuncak örnekte, orada çok daha fazla hala kısmi kayıt güncelleme durak ortadan kaldırarak sonra söz konusu kod optimize için yapılması gereken o

cycle iteration i iteration i+1 iteration i+2 
------------------------------------------------ 
0  cvtss2sd  . 
1  .    . 
2  mulsd   .    movss 
3  .    cvtss2sd  . 
4  .    .    . 
5  .    mulsd   . 
6  .    .    cvtss2sd 
7  addsd   .    . 
8  .    .    mulsd 
9  .    .    . 
10  . -- accum --> addsd   . 
11     .    . 
12     .    . 
13     . -- accum --> addsd 

Not: Kararlı durumda yürütme şuna benzer. Döngü-taşınan birikme-biriktirme gecikmesinin etkisini en aza indirgemek için vektörleştirilebilir ve çoklu akümülatörler kullanılabilir (meydana gelen belirli yuvarlanmaların değişme pahasına).

+0

İlginç, ancak iki soruya yol açar: 1. Neden yüksek bitler sıfırlanmıyor? Muhtemelen bu talimatı kullanıyorsanız, amacınız vektörel olmayan kod yazmaktır. 2. GCC ve Clang, xmm kaydının yüksek bitleri kullanılmadığında bile, örneğin, daha sonra sadece vektörel olmayan talimatlar kullanıldığında bile bunu yapmakta gibi görünmektedir. Bu neden? – dsimcha

+0

1. Intel bunu bu şekilde yapmayı seçti; neden çok önemli değil. Bazen yararlıdır, ama muhtemelen değerinden daha fazla belaya neden olur. 2. Kısmi kayıt güncelleme tehlikesi, XMM kayıtlarının yüksek kısmı hiç kullanılmamış olsa bile mevcuttur. Onu bu kadar sinsice yapan şey budur. –

+0

Düzenlemenizdeki daha ayrıntılı açıklama müthiş! Tek sorum şu ki, neden CPU'nun mantıksal mantığı üst quadword bağımlılıkları bağımsız olarak düşük quadword bağımlılıklarını takip etmiyor ve xxxsd komutlarının sadece registerın düşük quadword'undan okuduğunu/yazdığını anlıyor musunuz? – dsimcha