simd nedir ve nasıl kullanılır

entry1 galeri0
    1.
  1. SIMD'nin açılımı, adından da anlaşılacağı gibi 'Tek Komutla Çoklu Veri'dir. Tek bir komutla birden fazla veriyi işlememizi sağlar.
    'işlem' derken, toplama, çıkarma, çarpma, bölme gibi işlemlerin yanı sıra 've', 'veya' ve 'xor' gibi mantıksal işlemleri de kastediyorum.

    Tipik bir programda şunu yaparız:

    int Add(int a, int b)
    {
    int c = a + b;
    return c;
    }

    Peki ya birden fazla dizi veya vektör eklemek istersek?

    void Add ( int a[ 4 ], int b[ 4 ], int c[ 4 ])
    {
    c[ 0 ] = a[ 0 ] + b[ 0 ];
    c[ 1 ] = a[ 1 ] + b[ 1 ];
    c[ 2 ] = a[ 2 ] + b[ 2 ];
    c[ 3 ] = a[ 3 ] + b[ 3 ];
    }

    ilk örnekte, 'a' ve 'b' sayıları kayıt defterlerinde saklanıyor ve bir cpu komutuyla toplanıp değer döndürülüyor, üretilen assembly kodu şu şekilde görünebilir:

    Add:
    add eax, ebx
    ret

    ikinci örnekte, Compiler Explorer'ı Optimizasyonlar etkinleştirilmiş halde derlediğimizde talimat sayısı 14'e çıkıyor -O3
    - Bu iyi görünmüyor.

    Add:
    mov eax, dword ptr [rdx]
    add eax, dword ptr [rcx]
    mov dword ptr [r8], eax
    mov eax, dword ptr [rdx + 4]
    add eax, dword ptr [rcx + 4]
    mov dword ptr [r8 + 4], eax
    mov eax, dword ptr [rdx + 8]
    add eax, dword ptr [rcx + 8]
    mov dword ptr [r8 + 8], eax
    mov eax, dword ptr [rdx + 12]
    add eax, dword ptr [rcx + 12]
    mov dword ptr [r8 + 12], eax
    ret

    Yirmi yıl önce Intel, Pentium III ile vektör işleme için yeni bir komut seti, kayıtlar ve özel donanım geliştirdi. Bu sayede vektör verilerini kayıtlarda depolayıp üzerinde hesaplamalar yapabiliyorduk. Intel SSE komutlarıyla dizi sayılarını şu şekilde toplayabiliriz:

    # include <immintrin.h> //< yalnızca sse istiyorsanız emmintrin.h'yi ekleyebilirsiniz
    __m128i Add (__m128i a, __m128i b)
    {
    return _mm_add_epi32(a, b);
    }

    Yaygın bir benzetme, mutfakta yaptığımız gibi tek tek doğramak yerine birden fazla sebzeyi aynı anda doğramaktır.

    Yukarıdaki kodla talimat sayısını ikiye düşürdük, işte üretilen assembly:

    Add:
    movaps xmm0, xmm1 ; Move b to xmm0
    paddq xmm0, xmm2 ; Add a and b
    ret

    Fonksiyon argümanları a ve b , her biri 4 tam sayı veya 8 kısa sayı veya 16 adet 8 bit sayı tutabilen iki vektör kaydıdır .

    _mm_add_epi32 fonksiyonu iki sayı dizisini birbirine ekler.
    _mm_ Intel'in içsel fonksiyonlarını çağırdığımızda kullandığımız bir önektir.
    _add_ yapmak istediğimiz şeydir (alt, çoğalt, böl…)
    _epi32 kullanmak istediğimiz veri türüdür (32 bit tam sayılar).

    8 bit tam sayılar için _epi8,
    16 bit tam sayılar için _epi16,
    kayan noktalı sayılar için _ps,
    çift sayılar için _pd

    Yani 16 bitlik sayılardan oluşan iki diziyi toplamak isteseydik şunu yazabilirdik:

    __m128i Add(__m128i a, __m128i b)
    {
    return _mm_add_epi16(a, b); // notice epi16 this time
    }

    Yukarıdaki vektörleştirilmiş kod, bu skaler koda eşdeğerdir:

    void Add(short a[8], short b[8], short c[8])
    {
    for (int i = 0; i < 8; i++)
    c[i] = a[i] + b[i];
    }

    SIMD Nedir?
    SIMD'nin ne olduğunu ve nasıl kullanılabileceğini derinlemesine incelemeden önce, özünü ele alalım. CPU'lar, toplama, çarpma, çıkarma, VE, XOR, VEYA, karşılaştırma gibi matematiksel ve mantıksal işlemleri gerçekleştirmek için tamsayı kayıtlarını hesaplamaktan sorumlu bir ALU (Aritmetik Mantık Birimi) içerir. Ayrıca, kayan nokta sayılarını işlemek için kullanılan bir FPU (Kayan Nokta Birimi) de vardır.

    Bir Vektör işleme Birimi (VPU), genellikle hem tam sayı hem de kayan nokta aritmetiğini gerçekleştirebildiği için, birleşik bir ALU/FPU gibi davranır. Bir VPU'yu diğerlerinden ayıran şey, giriş verisi vektörlerine aritmetik işlemler uygulayabilme yeteneğidir.

    Modern CPU'larda, ayrı bir FPU bileşeni yoktur. Bunun yerine, skaler kayan nokta değerleri içerenler de dahil olmak üzere tüm kayan nokta hesaplamaları VPU tarafından gerçekleştirilir. FPU'nun kaldırılması, CPU çekirdeğindeki transistör sayısını azaltır ve bu transistörlerin daha büyük önbellekler, daha karmaşık sırasız yürütme mantığı vb. için tahsis edilmesini sağlar.
    (Kaynak: Oyun Motoru Mimarisi Kitabı)

    'Streaming SIMD Extensions'ın kısaltması olan SSE, Intel tarafından Pentium III işlemciler için geliştirildi. Daha sonra AMD ve Intel gibi şirketler, SSE2, SSE3 ve SSE 4.2 gibi yeni sürümler çıkarmaya devam etti.

    Bu yeni sürümlerde nokta çarpımı ve dize işleme gibi daha fazla talimat eklendi.

    Ardından, 128 bit yerine 256 bit veriyi işlememizi sağlayan AVX geldi. Bu, tek seferde iki kat daha fazla veriyi işleyebileceğimiz anlamına geliyor.

    Ardından AVX512 geldi, ancak bu nispeten yeni bir donanım ve henüz birçok bilgisayar tarafından desteklenmiyor. Çoğunlukla sunucu bilgisayarlarda ve bulut bilişim kurulumlarında bulunsa da, en yeni üst düzey masaüstü işlemcilerin bazıları AVX512'yi destekliyor.

    Bu metin boyunca C kod örnekleri göstereceğim, ancak diğer programlama dillerinin de SIMD komutlarını desteklediğini belirtmekte fayda var. Örneğin, C#, Rust, Zig ve daha birçok dilde SIMD komut desteği bulunmaktadır.

    ipucu: C#’ta Vector4 yapısı varsayılan olarak vektörleştirilmiştir.

    Ayrıca, benzer işlevselliği biraz farklı bir kodlama yaklaşımıyla sağlayan ARM NEON komut seti de var.

    #include <arm_neon.h>
    float32x4 a = vdupq_n_f32(2.0f); // [2.0, 2.0, 2.0, 2.0] 4x float
    float32x4 b = vdupq_n_f32(PI); // [PI, PI, PI, PI] 4x float
    float32x4 res = vmulq_f32(a, b); // res: [TwoPI, TwoPI, TwoPI, TwoPI]

    // Same SSE
    #include <immintrin.h>
    __m128 a = _mm_set1_ps(2.0f);
    __m128 b = _mm_set1_ps(PI);
    __m128 res = _mm_mul_ps(a, b);
    // if you want to create vector by specifiying all elements in SSE
    __m128 all = _mm_setr_ps(1.0f, 2.0f, 3.0f, 4.0f); // [1.0, 2.0, 3.0, 4.0] 4x floats

    ARM NEON'da fonksiyonlar genellikle 'v' ile başlar,
    ardından işlem (örneğin, 'dup', 'add', 'mul'),
    ardından 4 eleman üzerinde işlem yapmak için quad'ı belirten 'q'
    ve son olarak 32 bit float kullanımını belirtmek için '_f32' gelir.
    Bu sözdizimini kullanarak birçok fonksiyon hayal edebiliriz,
    başka bir yerde aramamıza gerek yok.

    Örnek kullanım:
    vmulq_f32(a, b) -> iki float vektörünü çarp
    vsubq_u32(a, b) -> iki uint32 vektörünü çıkar // [ax-bx, ay-by…]

    SIMD Her Yerde
    Kullanıldığı Alanlar: video kodlayıcılar, ses işleme, görüntü işleme,
    oyun motorları, bulut bilişim (veritabanları), makine öğrenmesi, karma oluşturma, kriptografi… Liste uzayıp gidiyor.

    Avantajları
    Diyelim ki bir döngüyü birden fazla iş parçacığı kullanarak optimize etmeyi hedefliyoruz ve dört iş parçacığı örneği oluşturuyoruz. Ancak, iş parçacığı oluşturmak yavaş bir işlemdir ve işlemden sonra tüm iş parçacıklarının tamamlanmasını beklememiz gerekebilir. SIMD ile, iş parçacığı örneği oluşturmaya gerek kalmadan hızı 4 kattan fazla artırabiliriz. SIMD kullanarak optimizasyon yapmak genellikle daha kolay ve daha kullanışlıdır. Bununla birlikte, istenirse vektörleştirilmiş kodu çoklu iş parçacığına da dönüştürebiliriz.

    Bunun şu şekilde çalıştığını hayal edebiliriz:
    SIMD
    ||
    ...

    Çoklu iş
    Parçacığı
    ||
    ...

    Her CPU çekirdeğinin bir VPU'su olduğundan, çoklu iş parçacığı işlemeyi SIMD koduyla birleştirebilir ve kodumuzu önemli ölçüde optimize edebiliriz. Eş zamanlı olarak 4, 8 veya 16 görev gerçekleştirdiğimiz için, potansiyel olarak ThreadCount * SIMDLaneCount'a kadar performans elde edebiliriz ve dalları ortadan kaldırırsak daha da yüksek performans elde edebiliriz. Ayrıca, iki sayıyı çarpıp üçüncü bir sayıyı ekleyen birleşik Çarpma Toplama (FMADD) gibi birleşik komutlar da mevcuttur. Bu, toplama ve çarpma işlemlerini ayrı ayrı yapmaya kıyasla performansı artırabilir.

    SSE: _mm_fmadd_ps(a, b, c) // a * b + c
    NEON: vfmaq_f32(c, a, b) // a * b + c

    Bazı komutlar diğerlerinden daha yavaştır; örneğin, ters karekök alma işlemi karekök alma işleminden daha hızlıdır ve bölme işlemi çarpma işleminden daha yavaştır. Her komutun gecikme süresi ve işlem hızı hakkında ayrıntılı bilgiyi CPU satıcısının dokümanlarında bulabilirsiniz. Ayrıca, bu kısıtlamalar işlemci mimarileri arasında farklılık gösterir. Kodumuzu performans açısından optimize ederken bu faktörleri göz önünde bulundurmak önemlidir.

    Otomatik Vektörizasyon
    Derleyiciler bazı kalıpları tanıyabilir ve kodunuzu vektörleştirebilir, bazen programcıdan daha iyi vektörleştirilmiş kod üretebilir.

    Bu basit kod göz önüne alındığında, optimizasyon bayrakları etkinleştirildiğinde derleyici beklediğimizden çok daha fazla kod üretiyor.
    int Sum(int* arr, int n)
    {
    int res = 0;
    for (int i = 0; i < n; i++)
    res += arr[i];
    return res;
    }

    Üretilen kodu inceleyebiliriz ve eğer hoşumuza gitmezse kendi
    vektörleştirilmiş kodumuzu yazabiliriz.

    eğer istersek bu makroyu kullanarak otomatik vektörleştirmeyi devre dışı bırakabiliriz

    #ifndef AX_NO_UNROLL
    #if defined(__clang__)
    # define AX_NO_UNROLL _Pragma("clang loop unroll(disable)") _Pragma("clang loop vectorize(disable)")
    #elif defined(__GNUC__) >= 8
    # define AX_NO_UNROLL _Pragma("GCC unroll 0")
    #elif defined(_MSC_VER)
    # define AX_NO_UNROLL __pragma(loop(no_vector))
    #else
    # define AX_NO_UNROLL
    #endif
    #endif

    // usage:
    AX_NO_UNROLL while (i < 10)
    {
    // do things
    }

    // with for
    AX_NO_UNROLL for (size_t i = 0; i < 256; i += 32) {
    {
    // do things
    }

    Ayrıca clang'in vektör uzantısını kullanarak birden fazla mimari için otomatik olarak oldukça iyi simd kodu üretebildiğini bilmekte fayda var: https://godbolt.org/z/E1es9qW3f

    Temel Matematik
    işte skaler vektör lerp fonksiyonu:

    Vector3 Lerp(Vector3 a, Vector3 b, float t)
    {
    Vector3 v;
    v.x = a.x + (b.x - a.x) * t;
    v.y = a.y + (b.y - a.y) * t;
    v.z = a.z + (b.z - a.z) * t;
    return v;
    }

    Bu SIMD versiyonudur:

    __m128 VecLerp(__m128 a, __m128 b, float t)
    {
    __m128 aToB = _mm_sub_ps(b, a);
    __m128 progress = _mm_mul_ps(aToB, _mm_set1_ps(t));
    __m128 result = _mm_add_ps(a, progress);
    return result;
    }

    // we can optimize with fused multiply add function:
    __m128 VecLerp(__m128 a, __m128 b, float t)
    {
    return _mm_fmadd_ps(_mm_sub_ps(b, a), _mm_set1_ps(t), a);
    }

    // ARM Neon Version:
    float32x4 VecLerp(float32x4 a, float32x4 b, float t)
    {
    return vfmaq_f32(x, vsubq_f32(b, a), ARMVecSet1(t));
    }
    Ama her seferinde her iki sürümü de yazmak acı verici olabiliyor, bu yüzden bu amaçla makrolar kullandım, makroları kullanmamın bir diğer nedeni de içsel işlevleri soyutlamak için operatör geçersiz kılma veya işlevler kullanırsam, hata ayıklama modunda kod, yönergenin kendisinden daha yavaş olan çağırma yönergesine derlenir ve derleyicinin yapabileceği çok daha fazla iyileştirmeden kaçınır.
    Örnek vermek gerekirse, ARM'nin astc-encoder'ını kullanıyorum, operatör geçersiz kılma içsel işlevlerini yapıyor ve projemi hata ayıklama moduyla derlediğimde çok yavaş oluyor, sahnemdeki tüm dokuları sıkıştırmaya çalıştığımda yarım saatten fazla bekledim, bitmemişti, sonra beklemekten vazgeçtim.
    Ancak serbest bırakma moduyla derlediğimde tüm performans kaybı ortadan kalkıyor ve iki dakika sonra tüm dokular sıkıştırılmıştı. Bu yüzden tüm kodu satır içi hale getirmek için makrolar kullandım.

    ARM Neon ve SSE'yi şu şekilde soyutladım:

    #if defined(AX_SUPPORT_SSE) && !defined(AX_ARM)
    /*//////////////////////////////////////////////////////////////////////////*/
    /* SSE */
    /*//////////////////////////////////////////////////////////////////////////*/
    typedef __m128 vec_t;
    typedef __m128 veci_t;
    typedef __m128i vecu_t;

    #define VecZero() _mm_setzero_ps()
    #define VecOne() _mm_set1_ps(1.0f)
    #define VecSet1(x) _mm_set1_ps(x) /* {x, x, x, x} */
    #define VeciSet1(x) _mm_set1_epi32(x) /* {x, x, x, x} */
    #define VecSet(x, y, z, w) _mm_set_ps(x, y, z, w) /* {w, z, y, x} */
    #define VecSetR(x, y, z, w) _mm_setr_ps(x, y, z, w) /* {x, y, z, w} */
    #define VecLoad(x) _mm_loadu_ps(x) /* unaligned load from x pointer */
    #define VecLoadA(x) _mm_load_ps(x) /* load from x pointer */

    // Arithmetic
    #define VecAdd(a, b) _mm_add_ps(a, b) /* {a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w} */
    #define VecSub(a, b) _mm_sub_ps(a, b) /* {a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w} */
    #define VecMul(a, b) _mm_mul_ps(a, b) /* {a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w} */
    #define VecDiv(a, b) _mm_div_ps(a, b) /* {a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w} */

    #define VecFmadd(a, b, c) _mm_fmadd_ps(a, b, c) /* a * b + c */
    #define VecFmsub(a, b, c) _mm_fmsub_ps(a, b, c) /* a * b - c */
    #define VecHadd(a, b) _mm_hadd_ps(a, b) /* pairwise add (aw+bz, ay+bx, aw+bz, ay+bx) */

    #define VecNeg(a) _mm_sub_ps(_mm_setzero_ps(), a) /* -a */
    #define VecRcp(a) _mm_rcp_ps(a) /* 1.0f / a */
    #define VecSqrt(a) _mm_sqrt_ps(a)

    // Vector Math
    #define VecDot(a, b) _mm_dp_ps(a, b, 0xff) /* SSE4.2 required */
    #define VecDotf(a, b) _mm_cvtss_f32(_mm_dp_ps(a, b, 0xff))
    #define VecNorm(v) _mm_div_ps(v, _mm_sqrt_ps(_mm_dp_ps(v, v, 0xff)))
    #define VecNormEst(v) _mm_mul_ps(_mm_rsqrt_ps(_mm_dp_ps(v, v, 0xff)), v)
    #define VecLenf(v) _mm_cvtss_f32(_mm_sqrt_ss(_mm_dp_ps(v, v, 0xff)))
    #define VecLen(v) _mm_sqrt_ps(_mm_dp_ps(v, v, 0xff))

    // Logical
    #define VecMax(a, b) _mm_max_ps(a, b) /* [max(a.x, b.x), max(a.x, b.x)...] */
    #define VecMin(a, b) _mm_min_ps(a, b) /* [min(a.x, b.x), min(a.x, b.x)...] */

    #define VecCmpGt(a, b) _mm_cmpgt_ps(a, b) /* [a.x > b.x, a.y > b.y...] */
    #define VecCmpGe(a, b) _mm_cmpge_ps(a, b) /* [a.x >= b.x, a.y >= b.y...] */
    #define VecCmpLt(a, b) _mm_cmplt_ps(a, b) /* [a.x < b.x, a.y < b.y...] */
    #define VecCmpLe(a, b) _mm_cmple_ps(a, b) /* [a.x <= b.x, a.y <= b.y...] */
    #define VecMovemask(a) _mm_movemask_ps(a)

    #define VecSelect(V1, V2, Control) _mm_blendv_ps(V1, V2, Control);
    #define VecBlend(a, b, c) _mm_blendv_ps(a, b, c)
    #define VeciBlend(a, b, c) _mm_blendv_ps(a, b, _mm_cvtepi32_ps(c))

    #elif defined(AX_ARM)
    /*//////////////////////////////////////////////////////////////////////////*/
    /* NEON */
    /*//////////////////////////////////////////////////////////////////////////*/

    typedef float32x4_t vec_t;
    typedef uint32x4_t veci_t;
    typedef uint32x4_t vecu_t;

    #define VecZero() vdupq_n_f32(0.0f)
    #define VecOne() vdupq_n_f32(1.0f)
    #define VecNegativeOne() vdupq_n_f32(-1.0f)
    #define VecSet1(x) vdupq_n_f32(x)
    #define VeciSet1(x) vdupq_n_u32(x)
    #define VecSet(x, y, z, w) ARMCreateVec(w, z, y, x) /* -> {w, z, y, x} */
    #define VecSetR(x, y, z, w) ARMCreateVec(x, y, z, w) /* -> {x, y, z, w} */
    #define VecLoad(x) vld1q_f32(x)
    #define VecLoadA(x) vld1q_f32(x)
    #define Vec3Load(x) ARMVector3Load(x)

    // Arithmetic
    #define VecAdd(a, b) vaddq_f32(a, b)
    #define VecSub(a, b) vsubq_f32(a, b)
    #define VecMul(a, b) vmulq_f32(a, b)
    #define VecDiv(a, b) ARMVectorDevide(a, b)

    #define VecFmadd(a, b, c) vfmaq_f32(c, a, b) /* a * b + c */
    #define VecFmsub(a, b, c) vfmsq_f32(a, b, c) /* a * b -c */
    #define VecHadd(a, b) vpaddq_f32(a, b) /* pairwise add (aw+bz, ay+bx, aw+bz, ay+bx) */
    #define VecSqrt(a) vsqrtq_f32(a)
    #define VecRcp(a) vrecpeq_f32(a)
    #define VecNeg(a) vnegq_f32(a)

    // Logical
    #define VecMax(a, b) vmaxq_f32(a, b) /* [max(a.x, b.x), max(a.x, b.x)...] */
    #define VecMin(a, b) vminq_f32(a, b) /* [min(a.x, b.x), min(a.x, b.x)...] */

    #define VecCmpGt(a, b) vcgtq_f32(a, b) /* greater or equal */
    #define VecCmpGe(a, b) vcgeq_f32(a, b) /* greater or equal */
    #define VecCmpLt(a, b) vcltq_f32(a, b) /* less than */
    #define VecCmpLe(a, b) vcleq_f32(a, b) /* less or equal */
    #define VecMovemask(a) ARMVecMovemask(a)

    #define VecSelect(V1, V2, Control) vbslq_f32(Control, V2, V1)
    #define VecBlend(a, b, Control) vbslq_f32(Control, b, a)
    #elif NON_SIMD
    // not showed in this article
    #endif
    0 ...
© 2025 uludağ sözlük