Optimering av blockbaserad Choleskyfaktorisering för

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