2017-02-11 15 views
6

Başlık saçmalık gibi görünebilir ama açıklamama izin ver. Ben şu montaj kodu karşılaştı zaman Geçen gün bir program inceliyordu:Shufps bellek erişiminden daha yavaş mı?

movaps xmm3, xmmword ptr [rbp-30h] 
lea  rdx, [rdi+1320h] 
movaps xmm5, xmm3 
movaps xmm6, xmm3 
movaps xmm0, xmm3 
movss dword ptr [rdx], xmm3 
shufps xmm5, xmm3, 55h 
shufps xmm6, xmm3, 0AAh 
shufps xmm0, xmm3, 0FFh 
movaps xmm4, xmm3 
movss dword ptr [rdx+4], xmm5 
movss dword ptr [rdx+8], xmm6 
movss dword ptr [rdx+0Ch], xmm0 
mulss xmm4, xmm3 

ve dört yüzer [rdx] ile [RBP-30 saat] çoğunlukla sadece kopya gibi görünüyor. Bu shufps s, sadece (örneğin shufps xmm5, xmm3, 55hxmm5 ikinci şamandıra ve yerleştirir seçer) xmm3 dört şamandıraların birini seçmek için kullanılır. Bu shufps aslında (movss xmm0, dword ptr [rbp-30h], movss dword ptr [rdx], xmm0 gibi bir şey) bellek erişimi daha hızlıdır çünkü derleyici öyle olduysa beni meraklandırıyor

.

Yani bu iki yaklaşımı karşılaştırmak için bazı testler yazdım ve çoklu bellek erişen her zaman daha yavaş shufps bulundu. Şimdi düşünüyorum belki de shufps'un kullanımının performans ile ilgisi yoktur. Sadece kodları gizlemek için orada olabilir, böylelikle dekomponentler kolayca temiz kod üretemez (IDA pro ile denenmiş ve gerçekten çok karmaşıktır). Muhtemelen derleyici büyük olasılıkla daha akıllı benden daha olduğu gibi herhangi bir pratik programlarında (örneğin _mm_shuffle_ps kullanarak) açıkça zaten shufps kullanmak asla iken programı derlenmiş derleyici böyle bir kod oluşturulan neden

, hala bilmek istiyorum . Ne daha hızlı ne de daha küçük. Hiç bir anlamı yok.

Neyse ben aşağıda yazdım testler vereceğiz. testte

#include <Windows.h> 
#include <iostream> 

using namespace std; 

__declspec(noinline) DWORD profile_routine(void (*routine)(void *), void *arg, int iterations = 1) 
{ 
    DWORD startTime = GetTickCount(); 
    while (iterations--) 
    { 
     routine(arg); 
    } 
    DWORD timeElapsed = GetTickCount() - startTime; 
    return timeElapsed; 
} 


struct Struct 
{ 
    float x, y, z, w; 
}; 

__declspec(noinline) Struct shuffle1(float *arr) 
{ 
    float x = arr[3]; 
    float y = arr[2]; 
    float z = arr[0]; 
    float w = arr[1]; 

    return {x, y, z, w}; 
} 


#define SS0  (0x00) 
#define SS1  (0x55) 
#define SS2  (0xAA) 
#define SS3  (0xFF) 
__declspec(noinline) Struct shuffle2(float *arr) 
{ 
    Struct r; 
    __m128 packed = *reinterpret_cast<__m128 *>(arr); 

    __m128 x = _mm_shuffle_ps(packed, packed, SS3); 
    __m128 y = _mm_shuffle_ps(packed, packed, SS2); 
    __m128 z = _mm_shuffle_ps(packed, packed, SS0); 
    __m128 w = _mm_shuffle_ps(packed, packed, SS1); 

    _mm_store_ss(&r.x, x); 
    _mm_store_ss(&r.y, y); 
    _mm_store_ss(&r.z, z); 
    _mm_store_ss(&r.w, w); 

    return r; 
} 



void profile_shuffle_r1(void *arg) 
{ 
    float *arr = static_cast<float *>(arg); 
    Struct q = shuffle1(arr); 
    arr[0] += q.w; 
    arr[1] += q.z; 
    arr[2] += q.y; 
    arr[3] += q.x; 
} 
void profile_shuffle_r2(void *arg) 
{ 
    float *arr = static_cast<float *>(arg); 
    Struct q = shuffle2(arr); 
    arr[0] += q.w; 
    arr[1] += q.z; 
    arr[2] += q.y; 
    arr[3] += q.x; 
} 

int main(int argc, char **argv) 
{ 
    int n = argc + 3; 
    float arr1[4], arr2[4]; 
    for (int i = 0; i < 4; i++) 
    { 
     arr1[i] = static_cast<float>(n + i); 
     arr2[i] = static_cast<float>(n + i); 
    } 

    int iterations = 20000000; 
    DWORD time1 = profile_routine(profile_shuffle_r1, arr1, iterations); 
    cout << "time1 = " << time1 << endl; 
    DWORD time2 = profile_routine(profile_shuffle_r2, arr2, iterations); 
    cout << "time2 = " << time2 << endl; 

    return 0; 
} 

yukarıda, iki karıştır yöntemleri shuffle1 ve aynı şeyi yapmak shuffle2 var. MSVC -o2 ile derlenmiş, bu aşağıdaki kodu üretir:

shuffle1: 
mov   eax,dword ptr [rdx+0Ch] 
mov   dword ptr [rcx],eax 
mov   eax,dword ptr [rdx+8] 
mov   dword ptr [rcx+4],eax 
mov   eax,dword ptr [rdx] 
mov   dword ptr [rcx+8],eax 
mov   eax,dword ptr [rdx+4] 
mov   dword ptr [rcx+0Ch],eax 
mov   rax,rcx 
ret 
shuffle2: 
movaps  xmm2,xmmword ptr [rdx] 
mov   rax,rcx 
movaps  xmm0,xmm2 
shufps  xmm0,xmm2,0FFh 
movss  dword ptr [rcx],xmm0 
movaps  xmm0,xmm2 
shufps  xmm0,xmm2,0AAh 
movss  dword ptr [rcx+4],xmm0 
movss  dword ptr [rcx+8],xmm2 
shufps  xmm2,xmm2,55h 
movss  dword ptr [rcx+0Ch],xmm2 
ret 

shuffle1 benim makinede shuffle2 daha her zaman en az% 30 daha hızlıdır. Ben ihbar shuffle2 iki talimatlar bulunur ve shuffle1 aslında eax yerine xmm0 kullanır yaptım bu yüzden ben biraz önemsiz aritmetik işlemleri eklerseniz, sonuç farklı olacağını düşündük.

__declspec(noinline) Struct shuffle1(float *arr) 
{ 
    float x0 = arr[3]; 
    float y0 = arr[2]; 
    float z0 = arr[0]; 
    float w0 = arr[1]; 

    float x = x0 + y0 + z0; 
    float y = y0 + z0 + w0; 
    float z = z0 + w0 + x0; 
    float w = w0 + x0 + y0; 

    return {x, y, z, w}; 
} 


#define SS0  (0x00) 
#define SS1  (0x55) 
#define SS2  (0xAA) 
#define SS3  (0xFF) 
__declspec(noinline) Struct shuffle2(float *arr) 
{ 
    Struct r; 
    __m128 packed = *reinterpret_cast<__m128 *>(arr); 

    __m128 x0 = _mm_shuffle_ps(packed, packed, SS3); 
    __m128 y0 = _mm_shuffle_ps(packed, packed, SS2); 
    __m128 z0 = _mm_shuffle_ps(packed, packed, SS0); 
    __m128 w0 = _mm_shuffle_ps(packed, packed, SS1); 

    __m128 yz = _mm_add_ss(y0, z0); 
    __m128 x = _mm_add_ss(x0, yz); 
    __m128 y = _mm_add_ss(w0, yz); 

    __m128 wx = _mm_add_ss(w0, x0); 
    __m128 z = _mm_add_ss(z0, wx); 
    __m128 w = _mm_add_ss(y0, wx); 

    _mm_store_ss(&r.x, x); 
    _mm_store_ss(&r.y, y); 
    _mm_store_ss(&r.z, z); 
    _mm_store_ss(&r.w, w); 

    return r; 
} 

ve talimatların aynı sayıda ve her iki xmm kayıtlarını kullanmak gerekir olarak artık montaj biraz daha adil görünüyor:

yüzden şu şekilde değiştirilmiş. ancak önemli değil. shuffle1 hala% 30 daha hızlı! geniş bağlamda olmadan

+0

Pek olası olmasa da, el yapımı bir montaj olabilir. – tambre

+0

@tambre evet Bunu düşündüm ama bunu yapmak için iyi bir neden düşünemiyorum. Bu, muhtemelen yüz milyonlarca kod satırına sahip büyük bir programdan. Karmaşıklığa rağmen programın belirli kısımlarını optimize etmek istiyorlarsa. Neden aslında optimizasyon olduğundan ve tam tersi olmadığından emin değiller? Bu yüzden derleyiciyi suçluyorum :) – MegaStupidMonkeys

+0

Belki de hizalanmış bellek erişimleri eski işlemcilerde çok daha hızlıydı. Bu yüzden derleyici, dört 4 bayt hizalanmamış yük yerine 16 bayt hizalanmış bir yük yapmayı tercih etti. Ayrıca belki de derleyici kayan nokta verileri için 'eax 'gibi yazmaçları kullanamadı. Son olarak, bellek yükü ve shuffle komutlarının hızını karşılaştırmanın akıllıca olmadığını unutmayın. CPU içinde ayrı yürütme birimleri kullandığından, bu iki komut türü paralel olarak çalışabilir. Gerçek performans, burada darboğaz olan şey tarafından tanımlanır ... – stgatilov

cevap

2

, yeni işlemciler için optimize ederken, farklı portlar kullanımını düşünmek zorundayız ... kesin söylemek zordur, ama. Burada Agners bakınız: Bu durumda http://www.agner.org/optimize/instruction_tables.pdf

, bu ihtimal gibi olabilir, ancak size aslında, optimize, montaj olduğunu varsayarak eğer bana dışarı atlamak birkaç olasılık vardır.

  1. Bu Out-Of-Order zamanlayıcı (örnek olarak Haswell kullanarak tekrar) portlar 2 ve 3'ten (örneğin, Haswell üzerine) port 5 fazlasına sahip olur kod streç görünebilir mevcut.
  2. # 1 ile benzerdir, ancak hiper-yayma ile aynı etki gözlenebilir. Bu kod, kardeş hiper-banttan okuma işlemlerini çalmamak üzere tasarlanabilir.
  3. Son olarak, bu tür bir optimizasyona ve benzer bir şeyi kullandığım yere özgüdür. Çalışma zamanında% 100 öngörülebilir, ancak derleme sırasında olmayan bir şubeniz olduğunu varsayalım. Hayal edelim, varsayımsal olarak, daldan hemen sonra bir önbellek özlemesi olan bir okuma var. En kısa zamanda okumak istersiniz. Sipariş Dışı Zamanlayıcı, okuyacak ve okuma bağlantı noktalarını kullanmadığınız takdirde okumaya başlayacaktır. Bu, shufps komutlarını esas olarak yürütmek için "serbest" hale getirebilir.

    MOV ecx, [some computed, mostly constant at run-time global] 
    label loop: 
        ADD rdi, 16 
        ADD rbp, 16 
        CALL shuffle 
        SUB ecx, 1 
        JNE loop 
    
    MOV rax, [rdi] 
    
    ;do a read that could be "predicted" properly 
    MOV rbx, [rax] 
    

Dürüst olsa, sadece kötü yazılmış montaj veya kötü oluşturulan makine koduna benziyor, bu yüzden çok içine düşünce koymak olmaz: İşte o örnek. Verdiğim örnek oldukça dar bir ihtimal.

İlgili konular