C és C++ esetén a függvények egy speciális fajtája az intrinsic, magyarul belsÅ‘ függvények. Ezek olyan függvények, amelyek implementációja magában a fordÃtóban van és nem valami C könyvtárban. Ilyen függvényekkel valósÃtják meg a processzorokban található, speciális utasÃtáskészletek támogatását.
Az ilyen utasÃtáskészletek gyűjtÅ‘neve a SIMD, ami a Single Instruction, Multiple Data rövidÃtése. Ezen utasÃtások lényege, hogy a processzor egy utasÃtásban több adaton végez műveletet. Például két nagy méretű regiszterbe betöltünk 6 db lebegÅ‘pontos számot, amit egy utasÃtással ezek után vektorokként össze tudunk adni.
AlapvetÅ‘en ezen utasÃtáskészletek multimédiás célokra (MMX, 3DNow!, SSE, SSE2) lettek kifejlesztve, azonban az évek alatt egyre általánosabb célokra alkalmazhatóak jelentek meg és a regiszter méretek is bÅ‘vültek. Az AVX utasÃtáskészletben 256 bites regiszterek állnak rendelkezésre, amivel egyszerre 256 bitnyi adatot tudunk feldolgozni adattÃpustól függetlenül.
Ezen utasÃtáskészletek kihasználása Assembly-bÅ‘l volt lehetséges, ami nem a legkényelmesebb, tekintve, hogy ezen utasÃtások nevei kimondhatatlan mozaik szavak, illetve a sok adat miatt komolyabb elÅ‘készületet vesz igénybe ezek meghÃvása úgy, hogy a C programunk állapotát ne kompromittáljuk.
Az intrinsic függvények ebben segÃtenek. Mivel ezek implementációi magában a fordÃtóban vannak, ezért mikor egy ilyen függvényt hÃvunk, akkor a fordÃtó a speciális Assembly utasÃtást illeszti be megfelelÅ‘ módon és nem kell Assembly-ben ezeket külön meghÃvnunk.
Az intrinsic függvényeket onnan lehet felismerni, hogy általában dupla aláhúzás (__) karakterekkel szokott a nevük kezdődni.
Ilyen függvénybÅ‘l több száz van, mivel az évek alatt az x86/x64 architektúra kapott egy pár speciális utasÃtáskészletet. Ezek kronológiai sorrendben: MMX, SSE, SSE2, SSE3, SSE4, AVX, AVX2 és AVX-512. A történetet tovább bonyolÃtja, hogy ezen utasÃtáskészleteknek több különbözÅ‘ változata is létezik és értelemszerűen nem minden CPU támogatja ezeket.
Éppen ezért egy speciális utasÃtással (CPUID) lekérdezhetÅ‘ egy processzorról, hogy milyen utasÃtásokat támogat. A CPUID meghÃvásához külön intrinsic függvények érhetÅ‘ek el, ami alapján tudunk olyan kódot Ãrni, ami ezeket figyelembe véve alkalmazza ezeket.
Az alábbi kódrészlet két 3D vektor összeadását mutatja be AVX utasÃtáskészlettel:
#include <stdio.h>
#include <immintrin.h> /*compiler intrinsics header*/
typedef struct
{
double x;
double y;
double z;
} Vector3D;
Vector3D AvxAdd(Vector3D vec1, Vector3D vec2)
{
/*AVX esetén az adatokat rendezni kell 256 bitre
azonban 3d vektorounk van, a 4. érték mindig 0 a helykitöltés miatt*/
__m256d ymm_vec1 = _mm256_set_pd(0, vec1.z, vec1.y, vec1.x);
__m256d ymm_vec2 = _mm256_set_pd(0, vec2.z, vec2.y, vec2.x);
/*AVX összeadás*/
__m256d ymm_result = _mm256_add_pd(ymm_vec1, ymm_vec2);
Vector3D result;
_mm256_store_pd((double *)&result, ymm_result);
return result;
}
/*Normál kód, ha nincs hardver támogatás*/
Vector3D Add(Vector3D vec1, Vector3D vec2)
{
Vector3D result;
result.x = vec1.x + vec2.x;
result.y = vec2.y + vec2.y;
result.z = vec2.z + vec2.z;
return result;
}
int main()
{
Vector3D vec1 = {1.0, 2.0, 3.0};
Vector3D vec2 = {4.0, 5.0, 6.0};
Vector3D result;
/*CPU detektálás futtatása*/
__builtin_cpu_init();
/*AVX meglétének ellenőrzése*/
if (__builtin_cpu_supports("avx"))
{
printf("AVX supported\r\n");
result = AvxAdd(vec1, vec2);
}
else
{
printf("AVX not supported\r\n");
result = Add(vec1, vec2);
}
printf("Result: x: %f y: %f z: %f", result.x, result.y, result.z);
}
A GCC által támogatott intrinsic függvények listája a https://gcc.gnu.org/onlinedocs/gcc-4.8.5/gcc/X86-Built-in-Functions.html cÃmen található meg. Az egyes utasÃtáskészletekben elérhetÅ‘ függvényekrÅ‘l és azok működésérÅ‘l az Intel weblapja nyújt segÃtséget: https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html