Uživatel:Voldamic

Z HPM wiki
Přejít na: navigace, hledání

Obsah

Michal Voldán

Navrh tematu semestralni prace: Altivec

Altivec

logo

Úvod

AltiVec jinak známý také jako velocity engine, je novější druh výpočetní jednotky přidávaný do počítačů odděleně od integer jednotky a FPU (skalární jednotky).

Rozdíl mezi vektorovou (Altivec) a skalární jednotkou je ten, že vektorová jednotka ovládá více kusů dat zároveň paralelně jednou instrukcí. Tento formát se nazývá SIMD (Single Instruction Multiple Data).

Procesory x86 obsahují také SIMD jednotku, která se chová podobně ( MMX, SSE a SSE2 ).

Rozdíl mezi skalární metodou řešení problémů a vektorovou cestou se dá demonstrovat na operaci sčítání:

V integer jednotce se dá napsat 1 + 1 = 2, v FPU 1.0 + 1.0 = 2.0 a ve vektorové jednotce:

resultVector = vec_add(!vector1, vector2)
//Přičti vector1 k vector2 a výsledek umísti do resultVector

Rozdíl je tedy v tom, že každý 128-bitový vektor je schopen udržet až 16 různých čísel najednou.

Sčítání pak může vypadat takto:

vektor1
vektor1
plus
vektor2
vektor2
rovnase
výsledný vektor
resultvektor


Každý element z vektoru 1 se sečte s korespondujícím elementem z vektoru 2 a výsledek je uložen v „result“ vektoru. Vektorové sčítání reálných čísel tedy spotřebuje pouze jeden cyklus, tedy stejné množství času jako předešlé výpočty u skalárních jednotek a proto je AltiVec jednotka tak rychlá. S paralelním přístupem k datům toho jednoduše uděláte více.


Rozhraní pro práci s AltiVec jednotkou

AltiVec jednotka se dá programovat s využitím jazyka C nebo assembleru. Rozhraní C je prakticky stejné jako rozhraní assembleru, kromě toho, že jazyk C bude předstírat, že používáte funkce jazyka C namísto assemblerovských, a kompilátor bude schopný upravit vaše instrukce tak, aby došlo k co největší optimalizaci. Skutečné C “funkce” zhruba v poměru 1:1 korespondují s AltiVec instrukcemi, takže se skutečný „slovník“ mezi těmito dvěma jazyky moc neliší.

Takže lze programovat v assembleru, C nebo v C++ s použitím CodeWarrior, a MrC/MrCpp na MacOS 9 a novějších. Lze také programovat v  C, C++ nebo objektovém C na MacOS X, s použitím CodeWarrioru a GCC (nebo Project Builderu). GCC A Project builder jsou zdarma při získání MacOS X. 

Některé další jazyky a kompilátory mají také podporu AltiVec (například Lightsoft Fantasm pro asm.) a Absoft pro FORTRAN. Ve zbytku případů často stačí použít AltiVec knihovny v C jako sdílené knihovny a konvertovat je do jiného prostředí, jak to jde udělat např. pro RealBasic.AltiVec nefunguje na žádném „pre-G4“ počítači. Pokud na takovém počítači zavoláte AltiVec instrukci, pravděpodobně to pochopí jako neplatnou instrukci. Apple si krátce „hrál s některými emulátorovými knihovnami v MacOS 8.1 ale ty nejsou vhodné pro sestavení konečného kódu. Některé AltiVec emulátory pak nakonec extrémně zpomalují, místo aby zrychlovaly. Pokud je třeba napsat program jak pro před-G4 a G4, budete muset napsat dvě verze každé funkce, kterou AltiVec využije.

Datové typy

Vektory AltiVec jsou všechny o délce 16 bytů (128 bitů). To je velikost jednoho registru, kterých je v procesoru 32. Data v registru vektorů mohou být interpretovány a zpracovány jako 128 bitové, 16 znaků o délce 8, 8 16-bitových pixelů, 4 integery, nebo 4 samostatná IEEE-754 čísla s plovoucí čárkou. Integery mohou být signed nebo unsigned. 32-bitové pixely mohou být interpretovány buď jako 4 unsigned integery, nebo 16 unsigned znaky, podle toho, jestli je chcete ovládat na úrovni pixelů, nebo kanálů. Vektorová jednotka neurčuje plovoucí čárku dvakrát. Tím nedochází k takové ztrátě, jakou bychom mohli očekávat při paralelním přenosu 2:1.

Pro zmírnění ztrát, se používá FPU jednotka, která zdvojnásobuje přesnost výpočtu se kterou výpočet navíc trvá stejně dlouho, jako výpočet u „single precision“ verzí, tedy kromě dělení. V FPU zpravidla probíhá největší optimalizace našeho kódu

FPU = floating point unit

V rozhraní C je každý z těchto nových prvků pojmenován tak, že se do vektoru nejdříve uloží „vector keyword“ a až následně proběhne samotný zápis. Každý z nich si lze představit jako malé pole hodnot nacpané do jediného kontejneru:

datovetypy

V C se dají napsat “spojky” pro převod mezi těmito dvěma zápisy. Na příkladu je vidět spojka řady 8 shortů do paměti pomocí jediného short vektoru: //Spojky umožňují přistupovat k datům jako k jednotlivým elementům, nebo celým vektorům

typedef union
{
vector short vec;
short elements[8];
}ShortVector;

Spojky jsou jednoduchý způsob, jak transportovat data z vektorové jednotky do FPU/IU pomocí stacku (zásobník).

Altivec operace

AltiVec přináší řadu odlišných operací, které by jste jinak s vektory provádět nemohli. Jsou rozděleny mezi několik rozdílných tříd: konverze typu, inicializace konstant, matematické operace, Booleanovské operace, komparátory, paměťové operace a permutační operace. Rozmanité instrukce AltiVecu jsou všechny abecedně rozděleny a detailně popsány v AltiVec Programmers Instructions manuálu.

Konverze typu

Vektorový typ se dá zaměnit bez velké námahy, přičemž se nezmění bitová struktura. Například pokud je výsledek instrukce (vec_splat_u8) vektor unsigned char, a potřebujete vektor float se stejnou bitovou délkou, můžete jednoduše zaměnit jeden typ za druhý. Konverze mezi klasickým floating point a integer typem, se provádí automaticky:

vector float zero = (vector float) vec_splat_u8(0);
//Konverze vektoru unsigned char na vektor float

Ani jedna z konverzí nezmění žádný bit. 

vec_ctf = “vector convert to float”
vec_ctu = “vector convert to unsigned fixed word saturated”
vec_cts = “vector convert to signed fixed point word saturated”

Protože tyto operace nepracují se zásobníkem, jsou mnohem rychlejší, než skalární konverze dělající to samé. V některých případech byla pozorována až 30ti násobná rychlost. Pokud chcete vector float plný hodnot 1.0, musíte jej generovat z korespondující integer verze takto:

vector float one = vec_ctf( vec_splat_u32(1), 0 );

Konverze mezi rozdílnými integer typy se dá udělat pomocí vec_pack(), nebo vec_unpackh() a vec_unpackl().

Vec_packs() a vec_packsu() - konverze z objemnějších typů do menších.
(Vec_packsu() je pro unsigned typy.)
Vec_packpx () - 32  16-bit pixel konverze.
Vec_unpackh() a vec_unpackl() - konvertují 16 bit pixely zpět do 32 bit pixelů.

Viz tabulka:

konverze

Inicializátory konstant

Zavádění konstant do vektorové jednotky je docela častý úkol. Nejjednosušším způsobem je načtení konstanty z globálního úložiště:

//Načti {0.0, 1.0, 2.0, 10.0} do myVector
vector float myVector = (vector float) ( 0.0, 1.0, 2.0, 10.0 );<br />
//Pro Red Hat / FSF compiler se místo klasických závorek používají vlnkované
vector float myVector = (vector float) { 0.0, 1.0, 2.0, 10.0 };

Bohužel je to poměrně drahý způsob. Pokud paměť, ve které konstanty „žijí“ nevyužívá vyrovnávací paměť, můžete být zasaženi až 35–250 paměťovými cykly. Pokud vaše funkce zabere méně než 200 cyklů, pak vám bude výrazně ku prospěchu, pokud se vyhnete načítání konstant tímto způsobem. Bude to lepší i pro vyrovnávací paměť.

Naštěstí je zde řada vec_splat_X#() funkcí, schopných vám pomoci s generováním konstant. Mohou generovat vektory naplněné nějakými hodnotami v rozsahu –16…15, buď jako vektor char, short, nebo int.

Např. vec_splat_u8(1) vytvoří vektor plný 0x01, zatímco vec_splat_s32(1) vytvoří vektor plný 0x00000001. Pro více informací jak vytvořit objemnější typy vektorů lze také navštívit tento odkaz.

Další typ inicializátorů konstant je dvojice instrukcí vec_lvsl a vec_lvsr. Vytváří vektor unsigned char, který počítá směrem nahoru od jednoho prvku k druhému. Díky svému návrhu jsou určeny pro rychlé generování konstant v souladu s přiřazováním paměti.
Umí toho i více:
Docela často potřebujete vektor konstant s různými hodnotami uloženými v jednotlivých elementech. Ten nejde vygenerovat pomocí vec_splat_XX() funkcí. Překvapivě často, toho ale lze dosáhnout pomocí funkce vec_lvsl nebo vec_lvsr. Navíc jsou vec_lvsl a vec_lvsr jediné dvě vektorové instrukce, které vám umožňují přemístit integer z integer unit do vektorové jednotky přímo bez jakéhokoliv načítání, nebo ukládání okolo. Pokud potřebujeme přesunout 4 bity z integer jednotky do vektorové rychle, tyto funkce to dokážou za 2-3 cykly. Pokud to potřebujete provést skrze místo v zásobníku (stack), pravděpodobně to zabere 7-10 cyklů pro plný 32 bitový vzorek skrze vektorový registr. Plných 128 bitů pravděpodobně zabere minimálně dvojnásobnou dobu.

Matematické operace

Sčítání a odečítání

Sčítání a odečítání je ovládáno skrze funkce vec_add() a vec_sub(). Pokud chcete zabránit přetečení, jsou zde speciální varianty, vec_adds() a vec_subs() které provádějí saturovaný součet nebo rozdíl mezi odpovídajícími elementy dvou vektorů. Tyto funkce jsou obzvlášť užitečné pro pixelově transparentní operace např. tam, kde by přetečení způsobilo viditelně tmavé pixely tam, kde by měla být světlá oblast. Dalšího využití mohou mít i v oblasti zvuku, kde by přetečení a tedy nesprávné oříznutí působilo trhliny, nebo praskání. Navíc jsou zde i funkce vec_sum4s() a vec_sum2s() které sčítají v celém vektoru pouze určité datové typy čísel.

Násobení

Existuje mnoho druhů násobení: vec_madd(), vec_madds(), vec_mladd(), vec_mradds(), vec_msum(), vec_msums(), vec_mule(), vec_mulo(), a vec_nmsub(). Typycky se zaměřují na vektorové typy — většina operací se vztahuje pouze na konkrétní datové typy. Ve většině případů ve skutečnosti provádějí operaci „nasobné sčítání“ (výsledek = A * B + C) spíše než jednoduché násobění. Pokud prostě postřebujete násobit, C bude nulový vektor. Pro operace s plovoucí čárkou, je výhodnější místo toho použít vektor plný „negativních nul“.

Dělení

Přímé dělení je možné pouze pomocí vectoru typu float. To provedeme pomocí vec_re(). Vec_re() má poloviční přesnost odhadu výsledku. Plnou přesnost získáme pomocí jednoduchého Newton-Raphsonova uhlazovacího kroku:

//Použití Newton Raphsonova vyhlazování
inline vector float vec_reciprocal( vector float v )
{
vector float reciprocal = vec_re( v );
return vec_madd( reciprocal,
vec_nmsub( reciprocal, v, vec_float_one()),
reciprocal );
}
//Generuje (vector float)(1.0)
inline vector float vec_float_one( void )
{
return vec_ctf( vec_splat_u32(1), 0);
}

Dělení integerů lze provádět pomocí vec_mradds().

//Signed 16 bit dělení integerů. Pozn.: s přesností 12 bitů. Pokud potřebujete více,
//použijte Newton-Raphsonovo vyhlazování
vector signed short Divide16( vector signed short numerator,
vector signed short denominator)
{
vector signed short zero = vec_splat_s16(0);

Kvůli řetězení problémů v VFPU (vector floating point unit) by bylo rychlejší, pokud by se provádělo dělení souboru rozděleného do dvou short vektorů současně. Vec_reciprocal() by mohl být rychlejší způsob pokud při použití čtyř float vektorů.

Odmocniny a vzájemné mocniny

Odmocňování se dá často provádět pouze u typu float.

Instrukce vec_rsqrte(), “vector reciprocal square root estimate” vrátí výsledek s poloviční přesností.
Je tedy nutné následně použít Newton-Raphsonovo vyhlazování pro plnou přesnost výpočtu odmocniny:

//Výpočet odmocniny o úplné přesnosti
inline vector float vec_reciprocal_sqrt( vector float v )
{
const vector float kMinusZero = vec_neg_zero();
const vector float kOne = vec_ctf( vec_splat_u32( 1 ), 0 );
const vector float kOneHalf = vec_ctf( vec_splat_u32( 1 ), 1 );
//výpočet 1/denomenator s použitím vyhlazení''
vector float sqrtReciprocalEstimate = vec_rsqrte( v );
vector float reciprocalEstimate = vec_madd( sqrtReciprocalEstimate,
sqrtReciprocalEstimate,
kMinusZero );
vector float halfSqrtReciprocalEst =
vec_madd( sqrtReciprocalEstimate, kOneHalf, kMinusZero );
vector float term1 = vec_nmsub( v, reciprocalEstimate, kOne );
return vec_madd( term1, halfSqrtReciprocalEst,
sqrtReciprocalEstimate );
}
//Generování vektoru plného –0.0.
inline vector float vec_neg_zero( void )
{
vector unsigned result = vec_splat_u32(-1);
return (vector float ) vec_sl( result, result );
}

Smíšené floating point operace

Exponenciální odhadovač, vec_expte() odhaduje hodnotu pro 2x množství proměnných. Podobně existuje odhadovač pro ln(x) množství proměnných vec_loge(). Vec_ceil () a vec_floor () provádějí stejné operace jako analogické Std funkce v knihovnách C.

Smíšené integer operace

Vec_avg() vrací průměr dvou integerů (zaokrouhluje nahoru).
Vec_abs() a vec_abss() se používá k získání absolutní hodnoty integeru.


Booleanovské operace

Všechny očekávatelné Booleanovské operace se dají provést pomocí vec_and(), vec_or(), a vec_xor(). Navíc jsou zde funkce AND, OR a XOR s doplňkem.
(vec_andc, A!&!~B)
(vec_nor, ~( A | B) )


Komparátory

Existuje mnoho způsobů, jak zjistit vzájemný vztah 2 vektorů.

Je zde řada vec_cmpXX() instrukcí, které porovnávají elementy 2 vektorů, jako menší než, větší nebo rovno, rovná se, atd.
Dále existuje řada vec_cmpb() funkcí, které kontrolují, které elementy spadají mezi hranice dvou dalších vektorů.
Výsledkem těchto funkcí je –1 pro všechny elementy s hodnotou true. Dobře toho dosáhneme s vec_sel() i pro jiné úkoly.
Je zde také řada komparátorů, která ukládá výsledky do místa v registru v integer jednotce. Lze je rozdělit na 2 druhy:

1) Řada funkcí vec_any_xx(), která vrací hodnotu 1 pokud nějaký element splní podmínku.
2) Řada funkcí vec_all_xx(), která vrací hodnotu 1 pokud všechny elementy vektoru splní podmínku.

Pokud ne, výsledek je 0. Testy nejsou omezeny na počet nerovností. Dají se testovat i hodnoty NaN (not a number), s plovoucí desetinnou čárkou i bez ní, nebo mimo meze (out of bounds).

Můžete provádět komparace elementů float vektorů s využitím vlastností NaN.
Každé porovnání s NaN vrací hodnotu false. Chceme-li například vědět jestli je první element float vektoru větší, než 0, lze použít funkci vec_any_gt():

// NaN v IEEE-754 float:

#define QNaN 0x7fc00000UL //Quiet
#define SNaN 0x7f800000UL //Throws an exception (signalling)
//Vrací hodnotu '''true''', pokud je první float ve vektoru "v" větší než 0.0
Boolean IsFirstElementPositive( vector float v )
{
vector unsigned int compare = (vector unsigned int)( 0, QNaN, QNaN, QNaN);
return vec_any_gt( v, (vector float) compare );
}


Je nutná konverze unsigned int na vector float typ, protože QNaN není standardní C hodnota.
Načítá se jako unsigned int pro bitové ovládání vstupních hodnot.
0xFFFFFFFFUL je také QNaN.
Takže porovnávání konstant může být také prováděno "za letu" bez používání paměti:

vector signed char compare = vec_sld( vec_splat_s8(0), vec_splat_s8(-1), 12 );

Znegované compare funkce (vec_aXX_nXX()) mají opačný smysl
— když se NaN objeví, výsledek je vždy true.
Pokud chceme vědět, jestli je druhý a třetí element větší než nula, použijeme vec_all_nle():

// true, pokud je 2. a 3. float ve "v" větší než 0.0
Boolean AreSecondAndThirdElementsPositive( vector float v )
{
vector unsigned int compare = (vector unsigned int)( QNaN, 0, 0, QNaN);
return vec_all_nle( v, (vector float) compare );
}

Zpracování Výsledných nul a jedniček z výstupu vec_cmp* funkce: Většinu času porovnáváme obě strany vzniklé větve výsledků a následně použijeme vec_sel() na správné výsledky.
Vyhodnocujeme obě strany, protože některé části vektoru by mohly jít jedním směrem a některé opačným.
Zde je příklad. "Zvektorizujme" funkci MAX:

//Maximum dvou integerů
int Max( int a, int b )
{
int result;
if( a < b )
result = b;
else
result = a;
return result;
}
//Maximum dvou vektorů typu integer 
vector signed int Max(vector signed int a, vector signed int b )
{
vector bool int mask = vec_cmplt( a, b ); //If ( a < b)...
vector signed int result = vec_sel( a, b, mask ); //Select a or b
return result;
}

Zde není zdržování kvůli větvení, což zvyšuje rychlost.
Vec_sel() dokáže provádět následující logické operace:

result = (a & ~mask) | (b & mask);

Permutační operace

Často nastává případ, kdy potřebujeme prohodit prvky vektoru, nebo mezi dvěma vektory.

Pokud použijeme moc permutací, náš kód může mít špatnou linearitu průběhu rychlosti.
V těchto případech je lepší zkusit jiné algoritmy, které vyžadují menší reorganizaci dat.
Pro reorganizaci a přesouvání dat většinou existuje samostatný kód.
Toto upozornění platí hlavně pro ty funkce, pro něž jejich skalární verze nebude provádět velké množství kopírování dat.

Nejběžnější permutační funkce je vec_perm().
- Může vzít libovolnou sbírku bytů ze 2 zdrojových vektorů a nakombinovat je do třetího.
- Používá "permutační vektor", který si udržuje hodnoty v rozmezí 0 až 31, aby věděl, který byt ze dvou zdrojových vektorů použít pro cílovou buňku.

Zatímco vec_perm() je docela účinná funkce sama o sobě, bývá docela nákladné vytvořit permutační vektor, kterým by se dala nahradit. Proto jsou zde některé další permutační operace, které přehazují pevně dané typy prvků v běžných operacích.

permutace1

Vec_mergeh() a vec_mergel() jsou podobné funkce.
- Dají se použít k prokládání obsahu dvou vektorů nějakým dalším vektorem (něco jako míchání karet).

To se hodí pro rozšiřování unsigned integerů na větší velikost, při použití nulového vektoru k prokládání.
Je to také velmi efektivní způsob, jak transponovat matici.

permutace2

Lze použít mnoho různých vektorových funkcí pro "left shift" a "right shift" operace.
Pro plný 128 bitový posuvný registr používáme kombinaci vec_slo() a vec_sll (left shifting), nebo vec_sro and vec_srl (right shifting).

//Posun vektoru vlevo podle počtu zaznamenaných bitů
vector unsigned char vec_shift_left( vector unsigned char v, Uint8 bitCount )
{
//Načtení bitového součtu do jednoho z bytů ve vektoru
vector unsigned char shiftValue = vec_lde( 0, &bitCount );
vector unsigned char splatByteAcrossVector;
//Načtení bitového součtu do všech elementů ve vektoru
splatByteAcrossVector = vec_splat( vec_lvsl( 0, &bitCount ), 0 );
s = vec_perm( shiftValue, shiftValue, splatByteAcrossVector );
//Posun vlevo
return vec_sll( vec_slo( v, s), s);
}

Jsou zde i některé funkce, které otáčí, nebo posouvají každý prvek podle čísla bitu korespondujícího s hodnotou uloženou na odpovídající pozici dalšího vektoru.

Jsou to funkce vec_sl(), vec_sra() a vec_sr().

Nakonec je zde i funkce vec_sld() používaná k otáčení nebo posunu vlevo u vektorů s pevným počtem bytů.
To je velmi užitečné pro řetězení vektorů a otáčení vektoru o známou hodnotu.

Paměťové operace

Načítání a ukládání:

Pokud používáte rozšíření C pro AltiVec a programujete v C/C++/Obj C, lze použít standardní C zápis pro načítání a ukládání jako pro obyčejná čísla, s tím, že se aplikují pravidla zarovnávání vektorů.
(adresa je tiše zaokrouhlena dolu na násobek velikosti vektoru):

//Načti nějaká data z paměti s pomocí standardního zápisu C
vector float *dataPtr = someAddress;
vector float theData = *dataPtr; //tiše zaokrouhleno jako (dataPtr & ~15)
vector float moreData = dataPtr[1]; //zaokrouhleno ((dataPtr+16 bytes) & ~15)

Avšak to není dostatečně flexibilní pro všechny způsoby načítání a ukládání dat z vektorových registrů. Proto je zde pár "AltiVec C extension" funkcí pro přímé načítání a ukládání do paměti.
Nejjednodušší jsou vec_ld() a vec_st().
Jednoduše načítají a ukládají vektory lépe než předchozí příklad.

Ukládá se do nejbližšího 16 bytového zarovnaného bloku — vámi zadaná adresa je v hardwaru interně zkrácena na násobek 16ti než se provede samotné načtení nebo uložení.
Pokud není vektor zarovnán, budete ho muset softwarově zarovnat ručně.

Vec_lvsl() a vec_lvsr() se dají použít pro generování permutačního vektoru, který potřebujete ke generování "unaligned vektoru" ze dvou sousedních zarovnaných vektorů:

//Načti vektor z nezarovnané oblasti v paměti
vector unsigned char LoadUnaligned( vector unsigned char *v )
{
vector unsigned char permuteVector = vec_lvsl( 0, (int*) v );
vector unsigned char low = vec_ld( 0, v );
vector unsigned char high = vec_ld( 16, v );
return vec_perm( low, high, permuteVector );
}
//Ulož vektor do nezarovnané oblasti v paměti
void StoreUnaligned( vector unsigned char v, vector unsigned char *where)
{
//Načti přilehlou oblast
vector unsigned char low = vec_ld( 0, where );
vector unsigned char high = vec_ld( 16, where );
//Připrav potřebné konstanty
vector unsigned char permuteVector = vec_lvsr( 0, (int*) where );

vector signed char oxFF = vec_splat_s8( -1 );
vector signed char ox00 = vec_splat_s8( 0 );
//Vytvoř masku "které části vektoru vyměňovat"
vector unsigned char mask = vec_perm( ox00, oxFF, permuteVector );
//Rotace vstupních dat vpravo
v = vec_perm( v, v, permuteVector );
//Vlož data do spodního a horního vektoru
low = vec_sel( v, low, mask );
high = vec_sel( high, v, mask );
//Ulož dva výsledné zarovnané vektory
vec_st( low, 0, where );
vec_st( high, 16, where );
}

StoreUnaligned() je velmi náročná funkce.
Musíte se ujišťovat, že jsou data správně zarovnána.
Pokud nemůžete, tak se často dostanete do smyčky při práci s velkými poli jen kvůli potřebě zarovnat okraje pole.

Někdy je lepší použít vec_ldl() a vec_stl() místo vec_ld() a vec_st().
Načítají a ukládají způsobem, který je šetrnější k datům, která již jsou ve vyrovnávací paměti.
Vec_ldl() a vec_stl() v ní označují nové bloky jako ty naposledy nejméně použité.
To znamená, že budou vyprázdněné jako první, až bude potřeba více místa.
Navíc, vec_ldl() a vec_stl() označují bloky jako přechodné, tzn. že budou vyprázdněny přímo do paměti, než aby zabíraly místo v L2.
Pokud neoznačíte přechodový blok, vyprázdní se do vyrovnávací paměti L2, která slouží jako "obětní" vyrovnávací paměť procesoru.
To může způsobit, že data, která potřebujete v L2 vyrovn. pam. budou vyprázdněna do RAM, aby bylo místo.
(Jediný způsob jak dostat data do L2 u G4, je přemístit je z L1)
Pokud v L2 nepotřebujete skladovat žádná data, použití vec_ldl() a vec_stl() u ostatních dat, která zde také nemusí být, může být dobrý způsob, jak se ujistit, že vaše důležitá data nebyla přemístěna do RAM.


Existují i verze pro načítání a ukládání samostatných prvků vektoru zvané vec_lde() a vec_ste().
V jednu chvíli tedy pracují pouze s jedním prvkem a né s celým vektorem.
To, s kterým prvkem se bude pracovat, záleží na zarovnání cíle.
Tyto varianty funkcí vec_ld() a vec_st() nejsou moc efektivní, ale občas se hodí.
Příklad přesunu prvku do FPU:

inline float DotProduct( const FVec v1, const FVec v2 )
{
float result;
vector unsigned temp = vec_splat_u32(-1);
vector float minusZero = (vector float ) vec_sl( temp, temp );
// najdi prvek jako výsledek operace s dvěma vektory
vector float length2 = vec_madd( v1, v2, minusZero );
//Suma všech prvků
length2 = vec_add(length2, vec_sld(length2, length2, 4 ) );
length2 = vec_add(length2, vec_sld(length2, length2, 8 ) );
//Všechny prvky v length2 jsou nynní stejné. Ulož výsledek do stacku
//a načti je do FPU registru
vec_ste( length2, 0, &result );
return result;
}



Streamované instrukce vyrovnávací paměti:

Většina toho, co budete v AltiVecu dělat bude omezeno přetečením paměti, to znamená, rychlostí paměťové sběrnice (Rychlost vaší funkce neurčuje CPU).

Streamované cache instrukce se dají použít k přímému ovládání toho, jak a kdy jsou data vyrovnávací paměti použita.
Je to efektivnější než např. funkce dcbt, protože jednoduchá SCI může "zajít pro" velké množství dat. Navíc je zde trošku větší kontrola toho, co se s daty děje po tom, co je již nepoužíváte.

Lze nastavit od 1 až 4 "toky" pomocí vec_dst (load) nebo vec_dstst (load + store).
Existují i přechodné varianty vec_dstt() a vec_dststt(), které označují své bloky k přesunu přímo do RAM hned jak se vyčerpají, namísto odesílání do L2 a L3 vyrovnávací paměti.

Tento nástroj vám může pomoci zachovat cenná data uložená ve vyrovnávací paměti L2, která bychom mohli potřebovat později.

Použijte vec_dst() a vec_dstt(), když máte v úmyslu přečíst jen kus dat.
vec_dstst() a vec_dststt() použijte, chcete-li přečíst a upravit kus dat.

Když se 2 sousední vektory ukládají do stejné řádky vyrovnávací paměti, zvyšuje se výkonnost.
To umožňuje, aby hardware zabránil načítání pouze přepisovatelných dat z paměti.


Všechny SCI konfigurovány pomocí konstantního řízení datového toku, který nastavuje velikost každého prvku vektoru podle toho, kolik je zde bloků a jaká je mezi nimi vzdálenost v bytech.

I zde ale existují limity kvůli velikosti bloků, kterou je lepší znát, než aby jste nakonec získali něco jiného, než jste chtěli:

//Inicializace konstanty pro použití vec_dst(), vec_dstt(), vec_dstst
//nebo vec_dststt
inline UInt32 GetPrefetchConstant( int blockSizeInVectors,
int blockCount,
int blockStride )
{
ASSERT( blockSizeInVectors > 0 && blockSizeInVectors <= 32 );
ASSERT( blockCount > 0 && blockCount <= 256 );
ASSERT( blockStride > MIN_SHRT && blockStride <= MAX_SHRT );
return ((blockSizeInVectors << 24) & 0x1F000000) |
((blockCount << 16) && 0x00FF0000) |
(blockStride & 0xFFFF);
}

blockSizeInVectors musí být číslo v rozmezí 1 až 32 (16 ař 512 bytů).
blockCount musí být 1 až 256. BlockStride je v bytech bytes a může být v rozsahu:
-32768 až 32767. Lze použít i záporné hodnoty pro vyjádření opačného směru.

Při použití čísla 0 až 3 je možno využít až 4 datové toky.
Pokud začne nějaký nový proces na stejném "vlákně", které používá váš, váš proces se zastaví.
(BlockMoveData() je příklad bezpečné přerušovací MacOS funkce, která používá tok č. 3.
Je často volána v Sound Manageru.)

blok


Používání SCI:
Protože tok může být z různých důvodů tiše ukončen, posílání všech dat najednou pomocí jednoduché funkce vec_dst*() je většinou špatný nápad.
Můžete ale posílat data po malých blocích (o velikosti 64 až 512 bytů).
Překrytí 80 až 90% mezi bloky není běžné, ani moc efektivní.
Každý blok může začít od prvního bytu dat, která máte v úmyslu okamžitě číst a rozšířit tak počet bytů, které máte v úmyslu použít v budoucnu.
Odhadnout, kolik bytů je nejlepší na načítání použít, je velmi obtížné a zpravidla se to musí zjistit experimentálně.

Když je datový tok dokončen, vyvoláme funkci vec_dss() pro zastavení toku.
Lze také zavolat funkci vec_dssall() k zastavení všech toků.

Hardwarové doplňky ovlivňující rychlost

Abychom mohli co nejlépe využít G4 (4. generaci 32 bitových procesorů), je nutné pochopit některé jejich části.

Kompletní dokumentace k implementacím PPC 7400, 7410, 7450 and 7455:
MPC7400UM MPC7410UM MPC7410UMAD MPC7450UM MPC7450UMAD
Rozdíly mezi těmito procesory jsou popsány zde:
G4 procesory

Vyrovnávací paměť pro instrukce

Pokud použijeme funkci, instrukce, které tuto funkci tvoří se načtou z paměti.
G4 má přidruženou 8mi cestnou 32 kB sadu vyrovnávací paměti pro jejich ukládání.
Je logicky nemožné, aby se sem vešel celý program, pokud nemá triviální velikost.
Proto není neobvyklé, že funkce načítaná z hlavní paměti bývá velmi, velmi pomalá.
To má za následek, že funkce, ve kterých je smyčka mohou být podstatně pomalejší, než funkce s vypsanými příkazy pod sebe.

S tímto vědomím může být užitečné umisťovat často vyvolávané bloky kódu do paměti těsně blízko následujícího bloku.
Je také dobré, zavolat si nějaký tok dat z tohoto místa v paměti ještě před tím, i když ho nepotřebujeme.
To by mělo mít za následek zefektivnění funkce.

Řetězení

Většina funkcí zabírá okolo 1 až 5 cyklů, v závislosti na prvcích vektoru.
Hodně věcí zabere jeden cyklus, kromě operací ve "Vector Complex Integer" jednotce (VCIU)
a "Vector Floating Point" jednotce (VFPU).
Vektorová permutační jednotka (VPERM) potřebuje jeden cyklus u PPC 7400 a 7410,
ale 2 cykly u PPC 7450.
VCIU má 3 úrovně (4 u PPC 7450) a VFPU má 4 až 5 podle toho, jestli se pracuje v Java módu, nebo ne.
Jak VCIU, tak VFPU jsou zřetězené, což znamená, že v každém můžete provádět více operací najednou.
V každém cyklu může být do "řetězu" přidána nová instrukce a jiná se může dokončit, nebo opustit řetěz.
To umožňuje propustnost jedné instrukce na cyklus, i když by správně měla každá instrukce zabrat několik cyklů.

pocetcyklu


Abychom mohli řetěz plně využít, musíme se ujistit, že je k dispozici dostatek nezávislých dat.
Může se totiž stát, že bude instrukce čekat na výsledek jiné takže se řetěz nespustí.
Např. skalární součin dvou dlouhých vektorů.:

//Jednoduchá funkce pro skalární součin
float SlowDotProduct( vector float *v1, vector float *v2, int length )
{
vector float temp = (vector float) vec_splat_s8(0);
float result;
//Smyčka násobení a sčítání přes celý vektor
for( int i = 0; i < length; i++)
temp = vec_madd( v1[i], v2[i], temp);
//Výsledek přičteme k celé délce vektoru
temp = vec_add( temp, vec_sld( temp, temp, 4 ));
temp = vec_add( result, vec_sld( temp, temp, 8 ));
vec_ste( temp, 0, &result );
return result;
}

Problémem zde je fakt, že každé vyvolání funkce vec_madd záleží na výsledku předchozí, a tak zde nedochází k žádnému řetězení.
Prostě uděláme vec_madd() každé 4 nebo 5 cyklů.
Rychlejší by bylo načíst 64 bytů z každého vektoru každou smyčku.
Takový řetěz se pak dá realizovat takto:

//Vynásobíme v1 a v2 rychleji. Pouze zde se ujistíme, že je řetěz plný
float FasterDotProduct( vector float *v1, vector float *v2, int length )
{
vector float temp = (vector float) vec_splat_s8(0);
vector float temp2 = temp;
vector float temp3 = temp;
vector float temp4 = temp;
vector float result;

//Smyčka přes délku vektoru - zde se pracuje paralelně se 4mi vektory --> řetěz
for( int i = 0; i < length; i += 4)
{
temp = vec_madd( v1[i], v2[i], temp);
temp2 = vec_madd( v1[i+1], v2[i+1], temp2);
temp3 = vec_madd( v1[i+2], v2[i+2], temp3);
temp4 = vec_madd( v1[i+3], v2[i+3], temp4);
}
//Suma dočasných vektorů
temp = vec_add( temp, temp2 );
temp3 = vec_add( temp3, temp4 );
temp = vec_add( temp, temp3 );
//Suma přes celý vektor
temp = vec_add( temp, vec_sld( temp, temp, 4 ));
temp = vec_add( result, vec_sld( temp, temp, 8 ));
//Zkopírujeme výsledek do stacku protože si jej pak můžeme vytáhnout zpět skrze FPU
vec_ste( temp, 0, &result );
return result;
}

Tato funkce umí navíc i urychlit SCI.

Vyrovnávací paměť dat

Stejně jako instrukční cache, i zde má procesor několik vyrovnávacích pamětí pro ukládání nejčastěji používaných dat.
Všechna data načtená do procesoru vytvářejí 32 bytový cache blok ("cache line") přidružený k datům, která směřují do L1.
L1 je osmicestná sada přidružených vyrovnávacích pamětí o velikostech 32 kB.
tzn. že každý kousek adresovatelné paměti přímo koresponduje se sadou osmi cachelines.
Je zde 128 takových sad.
Když je 32 bytový blok načten do cache, bity 20 až 26 z adresy určují, která sada vpustí tato data dovnitř.
Ze sady osmi cachelines se vybere ta přepsaná pomocí pesudoalgoritmu "naposledy použitý" (LRU).
Ve vzácných případech tak mohou nastávat problémy.

L1 je extrémně rychlá, tzn. většina načtení z ní zabere okolo 2 cyklů (3 u PPC 7450).
Pro porovnání, čtení z RAM trvá průměrně okolo 250 cyklů, 30 až 50 je ale častější počet, v závislosti na počtu přeskoků mezi jednotlivými bloky v cache.

Většinou je jeden z osmi bloků v sadě udržován v L1, v závislosti na počtu vyvolávání.
Je také možné jeden blok využít jako globální úložiště.

Když data vytáhneme z L1, skončí v L2, jiný způsob jak tam dostat data není.
L2 je tzv. ‘victim cache’ právě z tohoto důvodu. L2 je mnohem větší než L1.

Zarovnávání a rozložení dat

AltiVec neobsahuje hardware podporující načítání a ukládání nezarovnaných vektorů.
To platí zejména pro ukládání (vec_st a vec_stl).
Pokud se musíte rozhodnout mezi nezarovnaným načítáním a ukládáním, zvolte načítání.
Nezarovnané uložení může přepsat i sousední data v cílovém místě.

Nejlepší možností je ale jednoduše zarovnat data.

Přejmenovávací registry

Přejmenovávací registry a dokončování fronty často překvapí nové programátory AltiVecu, kteří se pokoušejí ručně o agresivní plánování instrukcí.

Nedostatek přejmenovaných bloků paměti by totiž mohlo způsobit zastavení fronty úkolů a následnému čekání fronty na "novou" volnou paměť.

Přejmenování:

Přejmenovávací registry pro vektory jsou dočasné buffery, ukládající výsledky dokončených nekompletních instrukcí.

Celkově je zde:
- 6 vector rename registrů - 6 integer rename registrů - 6 FPU rename registrů

Aby byla instrukce úspěšně odeslána a provedena, rename registr mísí být dostupný pro každé místo operandu specifikovaného instrukcí.
Pokud je instrukce jednou provedena, výsledek se zapíše do rename registru.
Během zpětného zápisu instrukce se z něj pak data zkopírují do cílového registru.
Pokud pak nějaká další instrukce potřebuje tento výsledek jako zdrojový operand, zpřístupní se pro cílovou jednotku. To umožňuje datově nezávislým instrukcím dekódování a odesílání bez čekání na čtení dat z registru.

Dokončovací fronta instrukcí:

U PowerPC 7400 a 7410 až 8 instrukcí předáno "za letu".
Aktivní instrukce se umisťují do dokončovací fronty.
Instrukce ve frontě mohou operovat s libovolnými datovými typy.
Dokončovací jednotka navrací instrukci, pokud jsou dokončeny všechny instrukce před ní a instrukce byla dokončena.
U PPC 7400 a 7410 mohou být z fronty uvolněny pouze 2 instrukce za cyklus a tím se zvyšuje rychlost

Větvení a predikce větvení

Příklad větvení:

if( test )
value++;

Příklad zvukové konverze ilustruje problémy větvení.
Často, když procesor narazí na rozvětvení, nemá dost času vyhodnotit, kterou cestou se vydat.
Jediné co mu pak zbývá, je hádat.
Pokud je odhad špatný, musí se zastavit všechny probíhající operace a začít se znovu od stejného místa.

Pokud větev míří rovně, (if statement) procesor jí ignoruje a pokračuje se dál.
Pokud větev vede zpět (smyčka) instrukce se přijme.
Tzn. pokud do vašeho kódu musíte umístit if, je lepší umístit častější výsledek "ifu" za if a méně častý výsledek za else.
Říká se tomu "statická pravidla předpovídání větvení".
U novějších G4 se používá tabulka s historií větvení.

If … else ... se obvykle týkají pouze jednoho datového toku.
(Výjimky: vec_all_* a vec_any_* instrukce)
Tím se stává zřetězení nemožné stejně jako paralelní operace.
Výsledkem je to, že kód s velkým množstvím větvením bude pracovat mnohem pomaleji, než kódy bez větvení dělající to samé.

Nejlepší je pak najít způsob, jak získat kód, ve kterém se větve nevyskytují.
Dokonce i kdyby se tak měl kód 3x prodloužit, bude stále rychlejší.
Vzorkovací kód “Sound Sample Conversion 2”, který to názorně ukazuje.
Větvení je zde jediným problémem.
U vec_packs() funkcí to ale jinak nejde.
Způsob jak zmírnit větvení:

//Změň pole 32 bitových integerů na pole 16 bitových integerů
void Convert( SInt32 *src, Sint16 *dest, UInt32 sampleCount )
{
SInt32 value;
while( sampleCount-- )
{
value = src[0];
if( value > SHRT_MAX )
value = SHRT_MAX;
else
if( value < SHRT_MIN )
value = SHRT_MIN;
dest[0] = value;
src++;
dest++;
}
}

Zdroje

ALTIVEC - PROGRAMMING INTERFACE MANUAL
ALTIVEC - PROGRAMMING ENVIRONMENT MANUAL
ALTIVEC A.K.A. VELOCITY ENGINE

--Voldamic 12. 11. 2011, 21:50 (CET)

Osobní nástroje
Jmenné prostory
Varianty
Akce
Navigace
Nástroje