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