EXAMENSARBETE INOM TEKNIK, GRUNDNIVÅ, 15 HP STOCKHOLM, SVERIGE 2016 Optimering av blockbaserad Choleskyfaktorisering för moderna datorsystem En studie om vektorisering och dess effekt på en befintlig blockalgoritm JONATHAN ROSELL ANDREAS VESTERGREN KTH SKOLAN FÖR DATAVETENSKAP OCH KOMMUNIKATION Optimering av blockbaserad Choleskyfaktorisering för moderna datorsystem En studie om vektorisering och dess effekt på en befintlig blockalgoritm JONATHAN ROSELL ANDREAS VESTERGREN Degree Project in Computer Science, DD143X Handledare: Michael Schliephake Examinator: Örjan Ekeberg CSC, KTH 2016-05-11 Referat Linjära system och lösning av dessa är ett viktigt verktyg inom många vetenskapliga områden. Lösning av linjära system är en operation med hög komplexitet, samtidigt som det finns tillämpningar där system med tusentals variabler är vanligt. Därför är det viktigt att använda metoder och algoritmer som utnyttjar datorns prestanda maximalt. Genom att faktorisera en matris som representerar ett linjärt system kan det lösas snabbare. Är matrisen symmetrisk och positivt definit kan Choleskyfaktorisering användas. J. Chen et. al (2013) undersökte en blockbaserad algoritm som ger bättre prestanda genom förbättrad cachehantering. Förutsättningarna har dock förändrats sedan deras undersökning. Dels är processorns cacheminnen under ständig utveckling, och dels så har moderna processorer kraftigt förbättrade möjligheter att öka prestandan med hjälp av vektorisering. Denna rapport undersöker hur denna blockbaserade Choleskyfaktorisering kan optimeras för moderna Intelprocessorer. Genom att använda AVX2-instruktioner vektoriseras de delar av algoritmen där flest operationer utförs. Samtidigt undersöks hur valet av blockstorlek påverkas av, samt hur brytpunkten mellan en klassisk, naiv algoritm och den blockbaserade algoritmen förändras i takt med att hårdvaran utvecklas. Med hjälp av en relativt enkel implementation av vektorisering halveras tiden för att faktorisera matriser oavsett storlek. Brytpunkten mellan den naiva och de blockbaserade algoritmerna sker nu redan runt 100 × 100-matriser. Detta är extra intressant då utvecklingen tidigare gått mot allt större matriser. [2] Abstract Optimization of Block-based Cholesky Factorization for Modern Computer Systems Linear systems and the solving of those is an important tool in many areas of science. The solving of linear systems is an operation of high complexity, and there are applications where systems of thousands of variables are used. Therefore, it is important to use methods and algorithms that can take full advantage of the performance of modern computers. Factorizing a matrix that represents a linear system makes solving it faster. If the matrix is symmetrical and positive definite Cholesky factorization can be used. J. Chen et. al. (2013) studied a block-based algorithm that gives better performance by using the cache memory more efficently when the matrix size increases. Since then, the conditions have changed. The cache memory of modern processors are subject to constant change, and modern processors capability of improving performance by vectorization have been vastly improved. This report examine how this block-based Cholesky factorization can be optimized for modern Intel processors. By using AVX2 instructions, the parts of the algorithm where most of the arithmetic operations are performed are vectorized. The report also study how the optimal block size, as well as how the breaking point between the naive algorithm and the block-based algorithm changes as the hardware develops. Using a fairly simple implementation with vectorization, the time required to factorize matrices of all sizes are cut in half. The breaking point between the naive and the block-based algorithm is now at matrices of sizes as small as 100 × 100. This is an interesting fact as prior research showed a trend where the breaking point seemed to move towards bigger matrices as the hardware developed. Innehåll 1 Inledning 1.1 Problemformulering . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Syfte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Bakgrund 2.1 Matematisk bakgrund . . . . . . . . 2.1.1 Linjär algebra . . . . . . . . . 2.1.2 Matrisfaktorisering . . . . . . 2.1.3 Blockindelning . . . . . . . . 2.2 Algoritmer . . . . . . . . . . . . . . 2.2.1 Naiv Cholesky . . . . . . . . 2.2.2 Blockindelad Cholesky . . . . 2.3 Datastrukturer . . . . . . . . . . . . 2.3.1 Lagring av matriser . . . . . 2.4 Hårdvara . . . . . . . . . . . . . . . 2.4.1 CPU . . . . . . . . . . . . . . 2.4.2 Advanced Vector eXtensions 2.4.3 Fused Multiply-Add . . . . . 2.4.4 Minneshierarki . . . . . . . . 2.4.5 Intels Skylake 6700HQ . . . . 1 1 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 3 3 4 5 5 6 7 8 8 9 9 10 11 11 12 3 Implementation 3.1 Naiv Cholesky . . . . . . . . . . . . . . . 3.2 Blockindelad Cholesky . . . . . . . . . . . 3.2.1 (U T1,1 )−1 . . . . . . . . . . . . . . . 3.2.2 (U T1,1 )−1 × A1,2 . . . . . . . . . . . 3.2.3 A02,2 = A2,2 − U T1,2 U 1,2 = U T2,2 U 2,2 3.3 Blockindelad Cholesky med vektorisering 3.3.1 Komplexitetsanalys . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 13 13 13 14 14 14 15 4 Metod 4.1 Testmiljö . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2 Testdata . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 17 17 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 18 18 18 5 Resultat 5.1 Bestämning av blockstorlek . . . . . . . 5.1.1 Blockcholesky utan vektorisering 5.1.2 Blockcholesky med vektorisering 5.2 Prestanda . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 19 19 22 25 6 Diskussion 6.1 Begränsningar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.2 Framtida arbete . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.3 Slutsats . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 29 30 30 Litteraturförteckning 31 Bilagor 31 4.4 Mätningar . . . . . 4.3.1 Blockstorlek 4.3.2 Prestanda . Verifiering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A Psuedokod A.1 Naiv Choleskyfaktorisering . . . . . . . . . . . . . . . . . . . . . A.2 Invers av triangulär matris . . . . . . . . . . . . . . . . . . . . . . A.3 Triangulär/rektangulär matrismultiplikation . . . . . . . . . . . . A.4 Rektangulär matrismultiplikation mellan transponat . . . . . . . A.5 Vektoriserad triangulär/rektangulär matrismultiplikation . . . . . A.6 Vektoriserad rektangulär matrismultiplikation mellan transponat . . . . . . . . . . . . 33 33 34 34 35 36 37 Kapitel 1 Inledning Linjära system används inom många vetenskapliga fält och har ett brett spektrum av tillämpningar. Inom många av dessa tillämpningar är systemen extremt stora. Det är inte ovanligt med system beståendes av tusentals variabler och lika många ekvationer. Lösning av linjära system har, i likhet med många andra matrisoperationer, hög komplexitet. Detta har delvis kompenserats av att beräkningskraften i moderna processorer utvecklats snabbt. Utvecklingen av datorns minne har dock inte lyckats hålla jämna steg, vilket medför att en flaskhals bildas. För att komma runt problemet kan speciella algoritmer som delar upp matrisen i mindre delar användas. På så sätt kan delarna var för sig hanteras effektivt i datorns snabba cacheminne. Dessa algoritmer, så kallade blockalgoritmer, är mer komplicerade och kräver fler operationer än de klassiska algoritmerna. Är matrisen som behandlas tillräckligt stor kan problemet trots det lösas snabbare tack vare färre cachemissar. När utvecklingen av processorns prestanda började stagnera på grund av fysikaliska hinder började utvecklingen istället gå mot parallellisering. Inledningsvis främst genom flerkärninga processorer, men på senare år har det blivit möjligt med så kallad vektorisering, eller Single Instruction Multiple Data-operationer.[4] Moderna processorer kan idag utföra 16 operationer per cykel och kärna, och kommande generationer processorer förväntas dubbla detta. [5] Möjligheten till vektorisering påverkar blockalgoritmer på dubbla fronter. Dels så blir påverkan av minneshastigheten tydligare ju högre beräkningskapacitet processorn har. Dels så påverkas det faktum att blockalgoritmerna kräver fler operationer. Ifall en blockbaserad algoritm består av operationer som i hög grad kan vektoriseras förändras den brytpunkt som finns vad gäller matrisstorlek för att den blockbaserade algoritmen ska vara snabbare. 1.1 Problemformulering Den snabba utvecklingen av vektoriseringskapaciteten hos moderna processorer ställer allt högre krav på effektiva algoritmer för att utföra matrisoperationer. Den här 1 KAPITEL 1. INLEDNING undersökningen avser att undersöka hur en blockbaserad algoritm för Choleskyfaktorisering påverkas av vektorisering. Följande två nära relaterade frågeställningar kommer besvaras • Hur mycket snabbare blir en blockbaserad Choleskyalgortim av vektorisering? • Hur förändras brytpunkten mellan en naiv Choleskyalgoritm och en blockbaserad algoritm om den blockbaserade algoritmen vektoriseras? 1.2 Syfte Uppsatsens huvudsyfte är att undersöka hur befintliga algoritmer för numeriska metoder kan optimeras för moderna datorsystem. Även om frågeställningarna ovan är fokuserade på vektorisering, gör valet att optimera en blockbaserad algoritm att även effektiv minneshantering blir en vital del av undersökningen. På så sätt kan två nyckelkoncept inom algoritmoptimering behandlas. Choleskyfaktorisering har, på grund av de snäva kraven på matrisen, något begränsat användningsområde. Dess symmetriska egenskaper bidrar dock till att göra den till en högst intressant metod att undersöka då de möjliggör flertalet optimeringsmöjligheter som andra liknande metoder saknar.[4] Nyckelkoncepten, vektorisering och minneshantering, är generella och går att tillämpa även på andra algoritmer. 2 Kapitel 2 Bakgrund Här introduceras och förklaras de områden som är nödvändiga för läsaren för att tillgodogöra sig det som presenteras i rapporten. Kapitlet inleds med en kort förklaring av de matematiska begrepp som används, följt av en härledning av de algoritmer som presenteras i rapporten. Därefter förklaras de datastrukturer som valts för att lagra matriserna, innan kapitlet avslutas med nödvändiga detaljer kring hårdvaran gås igenom. 2.1 Matematisk bakgrund Här presenteras de matematiska koncept som ligger till grund för uppsatsen. Relevanta delar av grundläggande linjära algebra presenteras kort, vilket även fyller syftet att introducera läsaren till den notation som används i resterande delar av uppsatsen. Detta följs av en introduktion till matrisfaktorisering, och avslutningsvis en förklaring av blockindelning av matriser. 2.1.1 Linjär algebra Uppsatsen kretsar kring de delar av linjär algebra som berör vektor- och matrisoperationer samt linjära ekvationssystem. Vektorer kommer betecknas med gemener i fet stil och vektorelement anges med dess index som subskript (figur 2.1). Matriser betecknas med versaler i fet stil, och matriselement anges med radnummer följt av kolumnnummer som subskript (figur 2.2). a = (a1 , a2 , · · · , an ) Figur 2.1. Vektorn a med storlek n Matriser används i stor utsträckning inom många vetenskapliga områden. En vanlig tillämpning är representation av linjära ekvationssytem. Ett ekvationssystem med m variabler och n ekvationer representeras av en n × m-matris. Endast i de fall då n = m kan det finnas en entydig lösning av systemet. Lösning av linjära ekva3 KAPITEL 2. BAKGRUND a1,1 a2,1 A= .. . a1,2 a2,2 .. . ··· ··· .. . a1,n a2,n .. . am,1 am,2 · · · am,n Figur 2.2. n × m matrisen A tionssystem har en komplexitet på O(n3 ). Den höga komplexiteten och det faktum att n i många tillämpningar är väldigt stort, medför att lösningen blir tidskrävande. Systemet kan lösas flera gånger snabbare om en metod anpassad efter matrisens typ används, även om komplexiteten förblir densamma. Matriserna som berörs i uppsatsen är symmetriska och positivt definita. En symmetrisk matris är identisk med sitt transponat. Transponatet till en matris bildas genom att låta matrisens rader bilda transponatets kolumner. Transponatet till matris A betecknas AT . I en symmetrisk matris är alltså rad i identisk med kolumn i. Ekvivalent gäller även att alla element i matrisen uppfyller am,n = an,m . Matrisers teckenkaraktär bestäms av vilka värden uttrycket v T Av kan anta, då v är en godtycklig vektor. För en positivt definit matris A gäller att uttrycket alltid är större än noll. Om en matris har alla nollskilda element placerade på diagonalen samt över eller under diagonalen kallas den triangulär. Är den övre sidan nollskild kallas den övertriangulär, är den undre sidan nollskild kallas den undertriangulär. Denna egenskap gör att flera operationer, exempelvis invertering, går flera gånger snabbare. Addition och subtraktion av matriser sker elementvis. Således ges elementen i en matris C = A + B av ci,j = ai,j + bi,j . Multiplikation sker genom att först elementvis multiplicera rad i från den vänstra matrisen med kolumn j från den högra, för att sedan summera produkterna. Alltså ges elementen i C = A + B av ci,j = ai,1 b1,j + ai,2 b2,j + · · · + ai,n bn,j = n P ai,k bk,j , där n är antalet rader i k=1 kolumner i A och antalet rader i B. Multiplikation är endast definierat om detta krav på dimensionerna är uppfyllt. Inversen till en matris A betecknas A−1 . En matris A sägs alltså vara inverterbar om det finns en matris A−1 sådan att AA−1 = I där I är identitetsmatrisen. 2.1.2 Matrisfaktorisering Faktorisering av matriser innebär att en matris delas upp i flera matriser, som vid multiplikation bildar den ursprungliga matrisen. Det finns olika typer av faktoriseringar. Valet av faktorisering är beroende av både typ av matris och syfte med faktoriseringen. Är målet att lösa stora linjära system snabbare söks en faktorisering sådan att matrisen delas upp i triangulära matriser. Alla kvadratiska matriser kan faktoriseras med LU-metoden. Matrisen delas då upp i en undertriangulär matris L och en övertriangulär matris U . Ett specialfall av LU-faktorisering är Choleskyfaktorisering. Choleskyfaktorise4 2.2. ALGORITMER 1 0 0 1 −2 3 1 −1 3 1 7 = 1 1 0 0 2 4 A = LU = 1 −2 0 1 0 0 5 −2 2 −1 Figur 2.3. Exempel på LU-faktorisering ring är möjlig när matrisen är symmetrisk och positivt definit. Genom att utnyttja symmetrin kan antalet operationer för att faktorisera matrisen minskas med nästan hälften, då endast halva matrisen behöver beräknas. Choleskyfaktorisering delar upp en matris i en undertriangulär matris L och dess transponat LT . 2 0 0 2 6 −8 4 12 −16 37 −43 = 6 1 0 0 1 5 A = LLT = 12 −8 5 3 0 0 3 −16 −43 98 Figur 2.4. Exempel på Choleskyfaktorisering När matrisen är faktoriserad kan det linjära systemet lösas med framåtsubstitution följt av bakåtsubstitution (figur 2.5). Nyttan med faktoriseringar är inte uppenbar, då faktoriseringen i sig kräver många beräkningar. En stor fördel är att faktoriseringen kan återanvändas när den väl är utförd, varpå det är möjligt att byta ut högerledsvektorn i ekvationen. Lösning av ett redan faktoriserat system har en komplexitet på O(n2 ). Ax = b ⇔ LLT x = b ⇒ Ly = b ⇒ LT x = y Figur 2.5. Lösning av faktoriserat linjärt system 2.1.3 Blockindelning En matris kan partitioneras så att varje del bildar en matris, ett så kallat block. En blockmatris är den matris där varje element är ett block (figur 2.6). Detta görs för att utnyttja datorns cacheminne effektivare när operationer utförs på stora matriser. Processen medför många extra operationer, och kräver väldigt stora matriser samt noga vald blockstorlek anpassad till datorns cacheminne för att innebära förbättrad prestanda.[2] 2.2 Algoritmer Här nedan förklaras och härleds de båda algoritmerna som undersöks. Först behandlas en grundläggande naiv algoritm som följer direkt av den matematiska definitionen av Choleskyfaktorisering. Därefter härleds en algoritm som genom blockindelning av matrisen ger bättre resultat för stora matriser. 5 KAPITEL 2. BAKGRUND # " " # " r q q p p P = 1 2 Q= 1 2 R= 1 r3 q3 q4 p3 p4 p1 p2 " # p p P Q 1 2 A= = r1 r2 R S r3 r4 # " s s r2 S= 1 2 s3 s4 r4 q1 q2 q1 q2 s1 s2 s3 s4 # Figur 2.6. Exempel på en blockmatris 2.2.1 Naiv Cholesky För att härleda en naiv algoritm för Choleskyfaktorisering tas avstamp i en generaliserad form av figur 2.4 l1,1 0 0 l1,1 l2,1 l3,1 A = LLT ⇔ l2,1 l2,2 0 0 l2,2 l3,2 l3,1 l3,2 l3,3 0 0 l3,3 2 l1,1 (Symmetrisk) 2 + l2 l2,1 = l2,1 l1,1 2,2 2 + l2 + l2 l3,1 l1,1 l3,1 l1,1 + l3,2 l2,2 l3,1 3,2 3,3 (2.1) Här syns hur symmetrin utnyttjas för att minska antalet operationer. För stora matriser sker i princip en halvering av antalet. Av ekvation 2.1 kan nu följande uttryck för elementen på (ekvation 2.2), respektive under diagonalen (ekvation 2.3) härledas. v u j−1 u X 2 lj,j = tai,j − lj,k (2.2) k=1 li,j = 1 lj,j (ai,j − j−1 X li,k lj,k ), k=1 Av 2.2 och 2.3 följer algoritm 1 nedan: [1] 6 i>j (2.3) 2.2. ALGORITMER Data: En Matris av tal A[x][y] = [1, 2, .., n][1, 2, .., n] som representerar matrisen. Result: Matris A innehåller den nedre triangulära faktoriseringen med diagonal elementen sparade i matrisen P Function Cholesky(A) for i ← 1 to n do for j ← i to n do sum ← A[i][j] for k ← i − 1 to 1 do sum ← sum − A[i][k] ∗ A[j][k] end if i = j then √ P [i] ← sum else A[i][j] ← sum P [i] end end end Algorithm 1: Naiv Choleskyfaktorisering Den naiva algoritmen är effektiv för små och medelstora matriser, och är grundsteget även i den blockindelade algoritmen. Då storleken på matrisen ökar blir effekten av cachemissar allt större. Det är den effekten som den blockindelade algoritmen motverkar. 2.2.2 Blockindelad Cholesky Den blockindelade Choleskyalgoritmen använder datorns cacheminne effektivare, men är mer komplicerad och kräver fler operationer på grund av det extra arbete som blockindelningen medför. Detta extra arbete medför krav på både matrisens storlek samt väl vald blockstorlek. Den valda algoritmen presterade 20% bättre än den naiva på stora matriser i tester, men författarna poängterade då att resutatet är beroende av datorns hårdvara. Både optimal blockstorlek och brytpunkt mellan de båda algoritmerna är hårdvaruberoende. [2] Algoritmen utgår från en blockindelad matris A. Återigen kan symmetrin utnyttjas för att minska antalet operationer. " A1,1 # " U T1,1 A1,2 = A2,2 U T1,2 U T2,2 #" U 1,1 U 1,2 U 2,2 # (2.4) Givet att A är en n × n-matris och blockstorleken sätts till d, får blocken A1,1 och U 1,1 dimensionen d × d, blocken A1,2 och U 1,2 dimensionen d × (n − d), samt blocken A2,2 och U 2,2 dimensionen (n − d) × (n − d). A1,1 , A2,2 , U 1,1 och U 2,2 är övertriangulära matriser medan A1,2 och U 1,2 är rektangulära matriser. Från ekvation 2.4 härleds följande uttryck A1,1 = U T1,1 U 1,1 7 (2.5) KAPITEL 2. BAKGRUND A1,2 = U T1,1 U 1,2 (2.6) A2,2 = U T1,2 U 1,2 + U T2,2 U 2,2 (2.7) Ekvation 2.5 är Choleskyfaktoriseringen av A1,1 och beräknas med hjälp av den grundläggande algoritmen. Förutsatt väl vald blockstorlek får hela matrisen nu plats i cacheminnet och faktoriseringen kan utföras utan cachemissar. Efter faktoriseringen kan U 1,2 beräknas från 2.6. (U T1,1 )−1 (2.8) Låt A02,2 beteckna den uppdaterade A2,2 . Med hjälp av 2.7 ges A02,2 = A2,2 − U T1,2 U 1,2 = U T2,2 U 2,2 (2.9) Där U T2,2 U 2,2 är Choleskyfaktoriseringen av av A02,2 . Är A02,2 mindre än vald blockstorlek faktoriseras den direkt, annars får den iterera genom algoritmen ännu en gång. Algoritmen kan sammanfattningsvis delas upp i sju steg.[2] 1. Uppdelning av matrisen i block. 2. Choleskyfaktorisering,A1,1 = U T1,1 U 1,1 , med naiv algoritm. 3. Beräkning av invers av undertriangulär matris, (U T1,1 )−1 . 4. Matrismultiplikation av undertriangulära matrisen (U T1,1 )−1 och rektangulära matrisen A1,2 , (U T1,1 )−1 . 5. Matrismultiplikation av U T1,2 och U 1,2 6. Uppdatera A2,2 genom att subtrahera den resulterande matrisen från steg 5 från A2,2 , A02,2 = A2,2 − U T1,2 U 1,2 = U T2,2 U 2,2 . 7. Om A02,2 är mindre än vald blockstorlek skickas A02,2 till steg 1, faktorisera annars med den naiva algoritmen. Figur 2.7. De sju stegen i den blockbaserade algoritmen 2.3 Datastrukturer Här presenteras detaljer kring de datastrukturer för lagring av matriser som använts vid implementation av algoritmerna. 2.3.1 Lagring av matriser C och C++ har inbyggt stöd för tvådimensionionella arrayer. Dessa fungerar vanligtvis bra för att lagra matriser på ett naturligt sätt. En tvådimensionell array 8 2.4. HÅRDVARA lagras radvis i datorns linjära minne. Detta försvårar arbetet att optimera matrisoperationer med avseende på cachehantering, då dessa operationer sker både radoch kolumnvis. Även möjligheten att minneseffektivt spara speciella typer av matriser begränsas, exempelvis triangulära matriser där endast halva matrisen behöver sparas utan att information går till spillo. Detsamma gäller symmetriska matriser, som kan sparas som triangulära matriser. Minnesadressen till ett visst element Ax,y i en n × n-matris sparat i en tvådimensionell array ges av: M innesaddress : Ax,y = (x − 1) + y − 1 (2.10) För att komma runt begränsningarna med tvådimensionella arrayer kan matriser lagras direkt i en endimensionell array. Matriselementen kan då placeras i en ordning som lämpar sig för de operationer som ska utföras. Det finns två format som är aktuella för våra algoritmer, Column Major Form (CMF) och Row Major Form (RMF). Som namnen antyder sparas matriserna då kolumnvis respektive radvis. Sparas matriser i endimensionella arrayer är det möjligt att endast spara den del av matriserna som anses intressant för att spara minne. Detta format kallas packad form, och kombineras med antingen CMF eller RMF.[6] Matriser sparade i packad form kräver att algoritmerna anpassas något för att kompensera för det offset som uppstår. Minnesaddressen till ett visst element Ax,y i en övertriangulär n × n-matris sparad i packad form för RMF respektive CMF ges av: RM F − minnesadress : Ax,y = CM F − minnesadress : (x − 1) ∗ (2n − x) + (y − 1) 2 Ax,y = (x − 1) + (y − 1)y 2 (2.11) (2.12) [3] 2.4 Hårdvara Effektiv optimering av algoritmer kräver god kännedom om hårdvaran som ska exekvera koden. Först ges en kort introduktion till hur en modern processor är uppbyggd. Två nyckelkoncept för uppsatsen, Advanced Vector eXtensions och Fused Multiply-Add, presenteras i detalj. På detta följer en genomgång av minneshierarkins uppbyggnad på ett modernt system samt hur den påverkar implementationen av algoritmer. Avslutningsvis presenteras hur detta realiseras i Intels Skylakearkitektur. 2.4.1 CPU CPU:n är vanligtvis enheten som utför alla aritmetiska operationer i datorn. Dessa operationer utförs antingen i en aritmetisk logisk enhet (ALU från engelskans Arithmetic Logic Unit) eller flyttalsprocessor (FPU från engelskans Floating Point 9 KAPITEL 2. BAKGRUND Unit), beroende på om det är heltals- eller flyttalsoperationer som ska utföras. Historiskt sett har CPU-prestandan utvecklats genom att antalet ALU och FPU ökas, men på grund av fysikaliska svårigheter har tillverkarna tvingats till andra metoder. Ett sätt att öka datorns prestanda utan att öka klockfrekvensen är att utföra operationer parallellt. Detta görs idag på två olika sätt, antingen genom instruktionsnivå-parallellism genom flertråds- eller flerkärneteknik alternativt med datanivå-parallellism, så kallad Single Instruction Multiple Data (SIMD).[4] Med hjälp av SIMD-instruktioner kan en aritmetisk eller logisk operation utföras på flera tal på samma gång. De senaste åren har samtliga av både Intels processorer haft SIMD-instruktionsset i sin arkitektur. Intels första SIMD-instruktionsset var Streaming SIMD Extensions (SSE), som använde sig av 128-bitars register, vilket innebar att fyra flyttalsoperationer kunde utföras parallellt. Detta utökades med Advanced Vector eXtensions (AVX) till 256-bitars register, vilket innebär en teoretisk fördubbling av prestandan. Detta har sedan utökats ytterligare i och med AVX2, där även instruktioner för att utföra kombinerad multiplikation och addition under samma cykel, så kallad Fused-Multiply-Addition (FMA), infördes.[4][5] Processorns prestanda mäts ofta i antalet flyttalsoperationer som kan utföras på en sekund, FLOPs. Processorns prestanda har en teoretisk maxgräns som ges av uttrycket: F LOP smax = #kärnor × klockf rekvens × #operationer cykel (2.13) En processor med stöd för AVX2 och FMA kan utföra 16 FLOP per cykel, med en klockfrekvens på 2.8 GHz ger det en teoretisk maxgräns på 44.8 GFLOPs per kärna.[5] Detta mått kan med fördel användas som referenspunkt när effektiviteten hos algoritmer undersöks eller jämförs. 2.4.2 Advanced Vector eXtensions Alla Intels processorer efter Haswell har stöd för vektoroperationer genom AVX. För att komma åt dessa används speciella assamblerinstruktioner eller, för enklare implementering, så kallade intrinsics. En intrinsic motsvarar en eller ett fåtal assamblerinstruktioner och ökar läsbarheten. Dessa intrinsics kan användas tillsammans med antingen C/C++ eller Fortran, förutsatt att kompilatorn har stöd för dessa. AVX gör det möjligt att läsa in flera element i följd på en gång från minnet till speciella register, och på samma sätt spara flera element från register till minne. För AVX är dessa register 256 bitar stora och rymmer 8 single precision- eller 4 double precision-flyttal. När vektorer laddats in i dessa register kan aritmetiska och logiska operationer utföras på samtliga element på en gång. I praktiken innebär detta att antalet operationer kan minskas till en åttondel för många tillämpningar inom exempelvis linjär algebra.[4][5] 10 2.4. HÅRDVARA Figur 2.8. Illustration av vektoraddition, där vektorerna a och b adderas och resultatet sparas i en ny vektor. 2.4.3 Fused Multiply-Add Fused Multiply-Add är en extension till x86-instruktionssetet som möjliggör att en multiplikation och en additionsoperation av floats utförs under en och samma klockcykel. Intels processorer med stöd för AVX2, Haswell och senare, har även stöd för FMA3. FMA3 innebär att FMA-operationen använder sig av tre register. Det är samma register som används för AVX2.[5] 2.4.4 Minneshierarki När det kommer till datorns minne finns det två önskvärda egenskaper, kapacitet och hastighet. Tyvärr är dessa egenskaper svåra att kombinera, då snabba minnen är små och dyra medan stora minnen är långsamma. Algoritmer tenderar att vara konstruerade så att data och instruktioner används flera gånger inom kort tid, så kallad temporal lokalitet, och att sannolikheten för att närliggande data behövs är hög, så kallad spatial lokalitet. På grund av detta är det möjligt att utnyttja små, snabba minnen genom att ladda in block av data från större minnen.[5] Detta ger en hierarkisk minnesmodell som för moderna system kan illustreras med figuren nedan. Högst upp i hierakrin ligger processorns cacheminnen, som i figuren består av tre nivåer kallade L1, L2 och L3. Därefter kommer datorns RAM-minne och längst ned i hierarkin, datorns sekundärminne. Accesstiden för data i L1-cachen är generellt ett fåtal cykler. För varje steg nedåt i hierarkin kan accesstiden mångdubblas.[5] Detta medför att algoritmer, även om de är korrekt konstruerade, snabbt begränsas av minneshantering snarare än antalet operationer när datamängderna ökar. Detta faktum utnyttjas exempelvis av blockbaserade matrisalgoritmer, som delar upp en stor matris i block med storlek vald så att hela blocket får plats i cacheminnet. 11 KAPITEL 2. BAKGRUND Figur 2.9. TEMP: Förenklad illustration av minneshierarkin i ett modernt system 2.4.5 Intels Skylake 6700HQ Processor 6700HQ är en fyrkärnig processor byggd med Intels Skylakearkitektur. Processorn har 64-bitars instruktionsuppsättning med stöd för AVX2 och FMA3. Klockfrekvensen är 2.6 GHz med multicore, eller 3.5 GHz med en kärna.[5] Detta ger enligt 2.13 en maximal prestanda för en respektive flera kärnor enligt nedan. Singlecore : 1 × 3.5GHz × 16 M ulticore : 4 × 2.6 × 16 Op = 56 GF LOP s Cykel Op = 166 GF LOP s Cykel (2.14) (2.15) Många avvägningar och implementationsbeslut kommer bygga på hur accesstiderna ökar för de olika minnesnivåerna i systemet. Dessa presenteras i tabellen nedan. Nivå L1 L2 L3 Kapacitet 32 KB 256 KB 6 MB Associativitet 8 4 12 Radstorlek 64 byte 64 byte 64 byte Accesstid 4 cykler 12 cykler 64 Figur 2.10. Accesstider för olika minnesnivåer för Skylake-processorer. 12 Kapitel 3 Implementation Här presenteras implementationen av de tre algoritmerna i detalj, tillsammans med motiveringar för avgörande designbeslut och en kort komplexitetsanalys. Efterfrågas ytterliggare detaljer kring implementationen återfinns psuedokod till samtliga funktioner i bilaga A. 3.1 Naiv Cholesky I ekvation 2.2 och 2.3 syns att summeringen sker kolumnvis för både element på diagonalen och utanför diagonalen. Matriserna sparas därför på kolumnform för att i någon mån utnyttja spatial lokalitet och därmed minska antalet cachemissar. Beräkningen av element utanför diagonalen kräver enligt ekvation 2.2 division med diagonalelementet på samma rad, varför god spatial lokalitet inte är möjligt för stora matriser. 3.2 Blockindelad Cholesky Med utgångspunkt i listan i sektion 2.2.2 presenteras här nedan de metoder som representerar respektive steg, med undantag för blockindelnigen som sker implicit och den naiva choleskyalgoritmen som presenterades i föregående sektion. Steg fem och sex har slagits ihop i implementationen för att spara minne. 3.2.1 (U T1,1 )−1 Denna operation sker på ett block med storlek anpassat för cacheminnet. Därför behöver ingen hänsyn tas i algoritmen för att förbättra spatial lokalitet. Den resulterande matrisen måste sparas på egen plats då matrisen som skickas in innehåller en del av slutresultatet. Innan den resulterande matrisen returneras transponeras den. 13 KAPITEL 3. IMPLEMENTATION 3.2.2 (U T1,1 )−1 × A1,2 Här utnyttjas det faktum att (U T1,1 )−1 är triangulär. Genom att utföra beräkningarna radvis, nedifrån och upp kan resultatet sparas direkt i matris A1,2 , då föregående rader ej behövs för kommande beräkningar, i enlighet med definitionen av matrismultiplikation. På så sätt kan både minnesanvänding och antalet operationer minskas. 3.2.3 A02,2 = A2,2 − U T1,2 U 1,2 = U T2,2 U 2,2 Genom att utnyttja dels de symmetriska egenskaperna hos en matris och dess transponat, och dels väljer att se matrismultiplikation som en serie av skalärprodukter kan både minnesanvänding och antal operationer sänkas. Istället för att utföra multiplikationen rad-vektorvis som en klassisk matrismultiplikation, beräknas skalärprodukten mellan den första kolumnen i U 2,2 med övriga kolumner i matrisen. Resultatet subtraheras sedan direkt för att undvika att spara en stor matris i onödan. 3.3 Blockindelad Cholesky med vektorisering Den stora flaskhalsen sett till antalet operationer i den blockbaserade algoritmen är de två matrismultiplikationerna. Matrismultiplikation går att vektorisera med relativt enkla mått. Därför har denna del av blockalgoritmen valts ut för vektorisering. Beskrivning av implementation med hjälp av AVX blir snabbt väldigt tekniskt. För bättre överblick och förståelse kan psuedokoden i bilaga A med fördel användas som hjälpmedel när den här sektionen läses. De två matrismultiplikationerna, (U T1,1 )−1 A1,2 och U T1,2 U 1,2 fungerar på liknande sätt. Det som skiljer dem åt är var uträkningen påbörjas och var resultatet sparas. Funktionen använder sig av AVX- och FMA-instruktioner, dessa kräver den specifika typen __m256d som motsvarar specifika minnesplatser. I koden benäms dessa ymm0, ymm1, och ymm2. For-loopen där k går från 0 till d−3 ökar med 4 varje gång för att _mm256_loadu_pd(double∗ mem) läser in fyra doubles från minnet från pekaren mem. Koden läser in fyra värden från både matrisen a och lt till ymm0 och ymm1. Dessa multipliceras ihop elementvis och resultatet adderas elementvis till värdena i ymm2. Sedan läses följande fyra värden och fortsätter så till loopen är klar. Efter loopen summeras värdena lagrade i ymm2. Det sker genom att ymm2 adderas ihop med en permutation av sig själv, och sedan adderas varje par av värden i ymm2. Efter det så har alla element i ymm2 samma värde. ymm2 sparas till ett temporärt minne, holder, för att förhindra att värden på a skrivs över av misstag. Värdet tas sedan från holder och läggs på rätt plats i a. • _mm256_loadu_pd(doubleconst∗mem_addr), tar en pekare till en minnesadress som variabel och returnerar en vektor av typen __m256d som innehåller 14 3.3. BLOCKINDELAD CHOLESKY MED VEKTORISERING fyra doubles från adressen i mem_addrtillmem_addr + 3. • _mm256_f madd_pd(__m256da, __m256db, __m256dc), tar tre vektorer av typen __m256d, och multiplicerar a och b och adderar c och returnerar det som en vektor av typen __m256d. • _mm256_permute2f 128_pd(__m256da, __m256db, intimm8), tar två vektorer av typen __m256d, och blandar dem för att få ett utdata, i detta fall så blir indata samma vektor [a1, a2, a3, a4] och utdata blir [a3, a4, a1, a2]. • _mm256_add_pd(__m256da, __m256db) tar två vektorer av typen __m256d och adderar dem elementsvis och returnerar resultatet som en __m256d vektor. • _mm256_hadd_pd(__m256da, __m256db) tar och adderar elementen horisontellt inom en vektor, [a1 + a2, b1 + b2, a3 + a4, b3 + b4] och returnerar denna vektor. • _mm256_storeu_pd(doubleconst ∗ mem_addr, __m256db), tar elementen i b och sparar dem till minnet från mem_addrtillmem_addr + 3. 3.3.1 Komplexitetsanalys För att kunna tolka och dra slutsatser av testresultaten krävs åtminstonne en grov bild av hur de olika algoritmerna förväntas prestera. Därför presenteras här en analys av algoritmernas komplexitet. För stora matriser har den naiva algoritmen en aritmetisk komplexitet enligt ekvation 3.1 [1] n3 . (3.1) 6 I de blockbaserade algoritmerna sker blockindelning nd gånger. För varje blockindelning utförs en faktorisering med den naiva algoritmen på en d × d-matris samt beräkning av invers av en triangulär d×d-matris. Detta följs av två matrismultiplikationer, först mellan en triangulär d × d-matris och en rektangulär d × (n − d)-matris, och sedan mellan två rektangulära matriser med dimensionerna (n − d) × d och d × (n − d). Blockstorleken d är konstant, men matrisstorleken n minskar med d i varje iteration. Detta ger då en aritmetisk komplexitet enligt ekvation 3.2, som är parallell med ekvation 3.1 för stora matriser [2] n3 (n%d + 1)n2 ) + (3.2) 6 2 Vektorisering av den blockbaserade algoritmen minskar antalet operationer som behövs för att utföra samma mängd beräkningar. Denna minskning fungerar som en konstant faktor som minskar antalet operationer, men den påverkar inte komplexiteten för algoritmen. Med andra ord kan förhållandet mellan de två blockbaserade algoritmernas prestanda förväntas vara konstant, oberoende av storleken på matrisen. 15 Kapitel 4 Metod I detta kapitel beskrivs de förutsättningar och metoder som använts i undersökningen. Resultaten kan förväntas vara beroende på systemet testerna körs på, så kapitlet inleds med att presentera relevanta hård- och mjukvaruförutsättnignar, samt hur koden kompilerats. Vidare presenteras hur testdata har skapats. Därefter följer en genomgång av de tester som genomförts samt hur prestandan bedömts på dessa tester. 4.1 Testmiljö CPU: Intel Core i7 6700HQ (Skylake) Moderkort: Clevo P65_P67RGRERA RAM-minne:2x8GB Kingston HyperX impact 2133MHz SODIMM DDR4 C++ har använts genomgående för implementation av samtliga algoritmer. Koden kompileras med g++ version 5.3.1 med flaggorna −mavx − mf ma − O3 − std = c + +11. Operativsystem var Xubuntu 15.10. 4.2 Testdata Undersökningen kräver positivt definita, symmetriska matriser i varierande storlek. Om en matris A är diagonaliserbar samt alla dess egenvärden λ är positiva, är matrisen positivt definit. Detta kan tvingas fram genom att addera ett stort tal till diagonalelementen. En testmatris A genereras genom att addera en randomiserad symmetrisk matris A0 med en identitetsmatris multiplicerad med A:s dimension. A = A0 + λI (4.1) Med denna metod skapades matriser med storlek varierande från 50 × 50 till 10000 × 10000 17 KAPITEL 4. METOD 4.3 Mätningar Samtliga tidsmätningar genomförs genom att jämföra UNIX-klockan före respektive efter genomförd faktorisering. Tidtagning börjar först när matrisen är inläst i minnet, då vi endast är intresserade av hur algoritmerna presterar. Testerna som utförts kan delas upp i två faser. I den första fasen undersöks hur blockstorleken påverkar prestandan hos de båda blockbaserade algoritmerna. I den andra fasen jämförs de tre algoritmernas prestanda med blockstorlek vald utifrån resultatet i fas ett. 4.3.1 Blockstorlek Tidigare forskning visar att val av blockstorlek beror på matrisstorleken.[2] Därför utförs testerna på tre olika matrisstorlekar, 100 × 100, 500 × 500 och 2500 × 2500. Målet är att hitta den blockstorlek som ger bäst prestanda på systemet. Testerna utförs genom att upprepade gånger faktorisera matriser av vald storlek, med varierande blockstorlek. Blockstorleken valdes initialt till fyra, och ökade sedan i steg om fyra. 4.3.2 Prestanda Med utgångspunkt i resultatet i föregående tester kan prestandan för de olika algoritmerna mätas med lämplig blockstorlek. Tester körs på matriser med dimension mellan 50 och 10 000. I den lägre delen av intervallet är hoppen mellan matrisstorlekarna mindre, för att sedan bli större i takt med att matrisstorleken ökar. Mellan 50 × 50 och1000 × 1000 ökas storleken i steg om 50, mellan 1000 × 1000 och 2500 × 2500 i steg om 100, samt mellan 2500 × 2500 och 10000 × 10000 i steg om 2500. Utifrån dessa resultat kan vi se både vid vilken matrisstorlek de båda blockalgoritmerna ger bättre prestanda än den naiva algoritmen, samt hur prestandan utvecklas i förhållande till ökande matrisstorlek. Utifrån den uppmätta tidsåtgången och uttrycken för antalet operationer i kapitel tre kan sedan algoritmernas prestanda sättas i paritet med den teoretiska maxprestandan för systemet. 4.4 Verifiering För verifiera att faktoriseringen utförts korrekt kan faktorerna multipliceras ihop för att sedan jämföras med en instans av den ursprungliga matrisen. För god överblick kan subtraktion användas för att få en matris innehållandes det resulterande felet i varje element. En viss felmarginal får ses som acceptabel då avrundningsfel är oundvikligt i flyttalsoperationer. 18 Kapitel 5 Resultat I detta kapitel presenteras de resultat från testerna som specificerats i kapitel fyra. Först redovisas testerna för att hitta lämplig blockstorlek för de båda . Därefter följer prestandamätningarna blockstorlekarna som togs fram i föregående test, på matriser i varierande storlek. Även den naiva algoritmen testas på samtliga matrisstorlekar. 5.1 Bestämning av blockstorlek 5.1.1 Blockcholesky utan vektorisering Graferna i figur 5.1, 5.2 samt 5.3 visar hur lång tid det tog att faktorisera 100 × 100, 500 × 500- respektive 2500 × 2500-matriser för blockcholesky utan vektorisering, för blockstorlekar mellan 4 och 36. Enligt mätningarna presterar algoritmen bäst med en blockstorlek på 12, men de efterföljande storlekarna verkar prestera bättre i takt med att matrisstorleken ökar. Både 12 och 16 tas därför med i kommande prestandatester. 19 KAPITEL 5. RESULTAT Figur 5.1. Tidsåtgång för faktorisering av 100 × 100 med blockcholesky utan vektorisering, för blockstorlekar mellan 4 och 36. 20 5.1. BESTÄMNING AV BLOCKSTORLEK Figur 5.2. Tidsåtgång för faktorisering av m500 med blockcholesky utan vektorisering, för blockstorlekar mellan 4 och 36. Figur 5.3. Tidsåtgång för faktorisering av 2500 × 2500 med blockcholesky utan vektorisering, för blockstorlekar mellan 4 och 36. 21 KAPITEL 5. RESULTAT 5.1.2 Blockcholesky med vektorisering Graferna i figur 5.4, 5.5 samt 5.6 visar hur lång tid det tog att faktorisera 100 × 100, 500 × 500 respektive 2500 × 2500 för blockcholesky med vektorisering, för blockstorlekar mellan 4 och 80. Ett större intervall för blockstorlekar används då resultaten skiljer sig kraftigt från de utan vektorisering. Då 36 presterar bra på både 500 × 500 och 2500 × 2500 valdes denna ut för vidare tester. Blockstorlek 20 presterar förvisso bättre på 100 × 100, men skillnaden mellan 20 och 36 är blott 0.1 millisekunder. Anmärkningsvärt är att den vektoriserade algoritmen verkar gå snabbare ju större blockstorleken blir på 2500 × 2500. På liknande sett skiljer sig 500 × 500, då prestandan verkar hålla sig på liknande nivå för större blockstorlekar. 22 5.1. BESTÄMNING AV BLOCKSTORLEK Figur 5.4. Tidsåtgång för faktorisering av 100 × 100 med blockcholesky med vektorisering, för blockstorlekar mellan 4 och 36. 23 KAPITEL 5. RESULTAT Figur 5.5. Tidsåtgång för faktorisering av 500 × 500 med blockcholesky med vektorisering, för blockstorlekar mellan 4 och 36. Figur 5.6. Tidsåtgång för faktorisering av 2500 × 2500 med blockcholesky med vektorisering, för blockstorlekar mellan 4 och 36. 24 5.2. PRESTANDA 5.2 Prestanda Graferna i figur 5.7 och 5.8 visar hur lång tid det tar för de tre algoritmerna att faktorisera små till medelstora matriser, respektive stora matriser. De valda blockstorlekarna följer från föregående sektion, och är 12 samt 16 för blockcholesky utan vektorisering samt 36 för blockcholesky med vektorisering. Graferna visar att den vektoriserade algoritmen är ungefär dubbelt så snabb jämfört med den ej vektoriserade. Det blir också tydligt att valet av blockstorlek endast gör marginell skillnad så länge någorlunda rimliga värden används, då kurvorna för den ej vektoriserade algoritmen i princip ligger på varandra. För väldigt stora matriser syns dock att blockstorlek 16 blir marginellt bättre. Figur 5.7. Prestanda små till medelstora matriser för de tre algoritmerna. 25 KAPITEL 5. RESULTAT Figur 5.8. Prestanda på stora matriser för de tre algoritmerna. 26 5.2. PRESTANDA I figur 5.9 har området kring algoritmernas brytpunkt förstorats. Här syns att den vektoriserade algoritmen går snabbare för matriser med dimensions mellan 50 och 100, medan den ej vektoriserade passerar något senare kring dimension på 100 eller strax efter beroende på vald blockstorlek. Exakt brytpunkt kan inte anges då alla storlekar ej testats. Figur 5.9. Skärningspunkterna då de blockbaserade algoritmerna presterar bättre än den naiva algoritmen. 27 Kapitel 6 Diskussion Optimal blockstorlek är enligt graferna 12, men prestandan är relativt jämn mellan blockstorlekar i intervallet 12 till 32. Detta resultat ligger inom ramen för vad som förväntades, då större block inte kan hanteras effektivt av cacheminnet. Mindre blockstorlekar kan förvisso hanteras effektivt av minnet, men den ökade mängden operationer tar då ut nyttan av effektivare cachehantering. Både den vanliga blockalgoritmen och den vektoriserade fick likvärdiga resultat för blockstorlek. För test av prestandan användes utöver blockstorlek 12, även 16 och 32. Detta för att vidare undersöka hur de påverkade prestandan för ett bredare spektrum av matrisstorlekar. En del oväntade resultat uppstod för den vektoriserade algoritmen. För små matriser, i testet n = 100, var en blockstorlek på 8 snabbast. Så små matriser ligger på gränsen till brytpunkten mellan den naiva algoritmen och de blockbaserade, så detta valdes att bortses ifrån vid vidare undersökningar. För större matriser, n = 2500 blev den av okänd anledning snabbare för större block. Exekveringstiden minskade för blockstorlekar på uppemot 80. Detta behöver undersökas vidare, men sker inte i den här uppsatsen då det ligger utanför det intervall som kan anses vara intressant för frågeställningen. Även prestandatesterna gav förväntat resultat. De blockbaserade algoritmerna presterar allt bättre jämfört med den naiva algoritmen i takt med att matrisstorlekarna ökar. Jämförs prestandan sinsemellan de blockbaserade algoritmerna, syns att den vektoriserade algoritmen är ungefär dubbelt så så snabb som den enkla algoritmen. Detta gäller samtliga matrisstorlekar, vilket är rimligt då vektorisering inte förändrar algoritmens komplexitet utan endast minskar antalet operationer som behöver utföras. Skillnaden mellan de valda blockstorlekarna är i det närmaste försvinnande liten, bortsett från ett fåtal avvikande värden. 6.1 Begränsningar Det är viktigt att poängtera att uppsatsens huvudsyfte är att undersöka styrkan i vektorisering och effektiv cachehantering. På inget sätt hävdas att de olika delarna i blockalgoritmerna utförs med optimala metoder. Implementeringen har istället skett 29 KAPITEL 6. DISKUSSION med mål att på ett klart sätt visa hur vektorisering påverkar prestandan, och med hänsyn taget till effektiv hantering av cacheminne. Främst matrismultiplikationen utförs med tämligen naiva metoder, med potential till stor förbättring. Optimering av dessa är dock ett tillräckligt stort problem för att täcka upp separata uppsatser, och utelämnas därför helt. 6.2 Framtida arbete Det finns många intressanta vinklar för framtida undersökningar. Uppenbart från föregående sektion är att utföra liknande tester med algoritmer bestående av väl optimerade operationer. Vidare används här bara en bråkdel av processorns prestanda då endast en kärna används. Många matrisoperationer kan med fördel parallelliseras i mycket hög utsträckning, och med mycket goda resultat. Framtida generationer av processorer kommer erbjuda ännu bättre prestanda för vektorisering i och med att registerstorlekarna fortsätter att öka. Övriga förutsättningar, exempelvis minnesstorlekar och hierarkier, är under ständig utveckling vilket medför att även prestandan hos en algoritm varierar över tid. Här uppkommer ett naturligt behov av fortsatta undersökningar. 6.3 Slutsats Denna uppsats har undersökt hur en blockbaserad algoritm för Choleskyfaktorisering kan optimeras för moderna datorsystem med hjälp av vektorisering. Undersökningen visar tydligt att vektorisering förbättrar prestandan för blockbaserad Choleskyfaktorisering markant. Jämfört med den enkla blockalgoritmen halveras exekveringstiden för samtliga matrisstorlekar när flyttal med dubbel precision används. Vektoriseringen flyttar även brytpunkten mellan den naiva algoritmen och den blockbaserade. Med vektorisering blir blockalgoritmen snabbare än den naiva för mindre matriser. Det ska dock nämnas att den blockbaserade algoritmen även utan vektorisering passerade den naiva algoritmen väldigt tidigt jämfört med resultat från tidigare forskning. 30 Litteraturförteckning [1] W. Press, (1992),Numerical recipes in C : The art of scientific computing(2.nd ed.), Cambridge: Cambridge University Press. [2] Jianping Chen, Zhe Jin, Quan Shi, Jianling Qiu, and Weifu Liu, (2014) Block Algorithm and Its Implementation for Cholesky Factorization." ICCGI 2013 the Eighth International Multi-Conference on Computing in the Global Information Technology, July 21 - 26, 2013, Nice, France ; Red Hook, NY: Curran, 232-36, Print. [3] S. C. Echezona, (2013), Derivation of the Mapping Functions for the Triangular Matrices to Linear Memory Nsukka, Nigeria: University of Nigeria. Web. [4] A. Andreev, A. Nasonov, A. Novokschenov, A. Bochkarev, E. Kharkov, D. Zharikov, S. Kharchenko, A. Yuschenko. (2015), Vectorization Algorithms of Block Linear Algebra Operations Using SIMD Instructions, Communications in Computer and Information Science, Vol 525, pp 323-340. [5] Intel, (2016) Intel 64 and IA-32 Architectures Optimization Reference Manual, Web. [6] Intel, Intel Math Kernel Library, Developer Reference, MKL 11.3, Revision 005, Web. 31 Bilaga A Psuedokod A.1 Naiv Choleskyfaktorisering Data: Ett startvärde startI där algoritmen ska starta på matrisen A, Ett slutvärde endI där algortimen ska sluta på matrisen A och en Matris A av tal A[x][y] = [1, 2, .., n][1, 2, .., n] som representerar matrisen sparad på kolumnform. Result: Matris A innehåller den övre triangulära faktoriseringen Cholesky (startI, endI, A) for j ← startI to endI do for i ← startI to j do sum ← 0 for k ← startI to i do sum ← sum + A[k][i] ∗ A[k][j] end if i = j thenp A[i][j] ← A[i][i] − sum else 1 A[i][j] ← A[i][i]∗(A[i][j]−sum end end end Algorithm 2: Choleskyfaktorisering 33 BILAGA A. PSUEDOKOD A.2 Invers av triangulär matris Data: En Matris a som innehåller den del som inversen ska vara beräknad av, En Matris lt som ska innehålla inversen, dwight som är storleken på den del som ska inverteras, startI index på a där inversen ska börja beräknas, w är storleken på hela a Result: Matrisen lt kommer att innehålla en invers av den delen av a som är innanför startI och dwidth computeInverseOfTriangular ( a, lt, dwidth, startI, w) for i ← 0 to dwidth do 1 lt[i][i] ← a[startI+i][startI+i] end for i ← 0 to dwidth do for j ← i + 1 to dwidth do sum ← 0 for k ← i to j do sum ← a[startI + k][startI + j] ∗ lt[k][i] end lt[j][i] ← lt[j][j] ∗ (−lsum) end end Algorithm 3: Beräkna Inversen av en triangulär matris A.3 Triangulär/rektangulär matrismultiplikation Data: Ett värde d som är längen på den triangulära matrisen lt, ett värde w som är längden på en sida av den totala matrisen a, en matris a som är sparad på kolumn format, en matris lt som inehåller en triangulär matris, ett värde startI som är vilket index av a som algoritmen skall börja på. Result: Matrisen a updateras med värdena från multipliationen matrixMultiplicationLowerTriangularRectangular (d, w, a, lt, startI) width getsw − startI + d for j ← 0 to width do for i ← (d − 1) to 0 do sum ← 0 for k ← 0 to d do sum ← lt[i][k] ∗ a[k + startI][j + startI + d] end a[i + startI][d + startI + j] ← sum end end Algorithm 4: Multiplikation mellan triangulär och rektangulär matris 34 A.4. REKTANGULÄR MATRISMULTIPLIKATION MELLAN TRANSPONAT A.4 Rektangulär matrismultiplikation mellan transponat Data: Ett värde startI som är indexet som algoritmen börjar på, w är längen då en sida av a, d den andra dimensionen av de rektangulära delarna, Matrisen a som innehåller de delmatriser som algoritmen använder Result: Matrisen a uppdateras med nya värden beroende på matrismultiplikationen matrixMultiplicationRectangular ( startI, w, d, a) for j ← startI + d to w do for i ← startI + d to j + 1 do sum ← 0 for k ← 0 to d do sum ← a[startI + k][i] ∗ a[startI + k][j] end a[i][j] ← a[i][j] − sum end end Algorithm 5: Multiplikation mellan en matris och dess transponat 35 BILAGA A. PSUEDOKOD A.5 Vektoriserad triangulär/rektangulär matrismultiplikation Data: Ett värde d som är längen på den triangulära matrisen lt, ett värde w som är längden på en sida av den totala matrisen a, en matris a som är sparad på kolumn format, en matris lt som inehåller en triangulär matris, ett värde startI som är vilket index av a som algoritmen skall börja på. Result: Matrisen a updateras med värdena från multipliationen matrixMultiplicationLowerTriangularRectangular (d, w, a, lt, startI) width ← w − startI + d holder ← [0, 0, 0, 0] for j ← 0 to width do for i ← (d − 1) to 0 do ymm2 ← [0, 0, 0, 0] for k ← 0 to d − 3 do ymm0 ← _mm256_loadu_pd(&a[startI + k][j + startI + d]) ymm1 ← _mm256_loadu_pd(&lt[k][i]) ymm2 ← _mm256_f madd_pd(ymm0, ymm1, ymm2) k ←k+4 end ymm2 ← _mm256_add_pd(ymm2, mm256_permute2f 128_pd(ymm2, ymm2, 0x1)) ymm2 ← _mm256_hadd_pd(ymm2, ymm2) _mm256_storeu_pd(holder, ymm2) a[i + startI][d + startI + j] ← holder[0] end end Algorithm 6: Vektoriserad multiplikation mellan triangulär och rektangulär matris 36 A.6. VEKTORISERAD REKTANGULÄR MATRISMULTIPLIKATION MELLAN TRANSPONAT A.6 Vektoriserad rektangulär matrismultiplikation mellan transponat Data: Ett värde startI som är indexet som algoritmen börjar på, w är längen då en sida av a, d den andra dimensionen av de rektangulära delarna, Matrisen a som innehåller de delmatriser som algoritmen använder Result: Matrisen a uppdateras med nya värden beroende på matrismultiplikationen matrixMultiplicationRectangular ( startI, w, d, a) holder ← [0, 0, 0, 0] for j ← startI + d to w do for i ← startI + d to j + 1 do ymm2 ← [0, 0, 0, 0] for k ← 0 to d − 3 do ymm0 ← _mm256_loadu_pd(&a[startI + k][i]) ymm1 ← _mm256_loadu_pd(&a[startI + k][j]) ymm2 ← _mm256_f madd_pd(ymm0, ymm1, ymm2) k ←k+4 end ymm2 ← _mm256_add_pd(ymm2, _mm256_permute2f 128_pd(ymm2, ymm2, 0x1)) ymm2 ← _mm256_hadd_pd(ymm2, ymm2) _mm256_storeu_pd(holder, ymm2) a[i + startI][d + startI + j] ← holder[0] end end Algorithm 7: Vektoriserad multiplikation mellan en matris och dess transponat 37 www.kth.se