Vad är det som gör så BLAS biblioteket för matemaik är så optimerat?

Permalänk

Vad är det som gör så BLAS biblioteket för matemaik är så optimerat?

Jag har alltid undrat varför BLAS är så optimerat? Det finns olika nivåer av BLAS. Nivå 1 som handlar om vektorer * vektorer. Nivå 2 som handlar om matriser * vektorer. Nivå 3 som handlar om matriser * matriser. Som jag har tolkat det som.

BLAS är alltså Basic Linear Algebra Subprograms, dvs multiplikation, subbstraktion, addition, division, invertering, transponat osv. Alltså absolut lägsta nivån inom den linjära algebran. LAPACK finns också, men då handlar det mer om LU, SVD, EIG, QR osv.

När jag läser koden så verkar det inget annat vara än C-kod tidgare än C89-standard, dsv K&R C-standard från forna tider som C-folket körde innan det ens fanns en officiel standard. Koden ser ut som man har rullat en apelsin på ett tangentbord och inget "make sense". Men koden fungerar än idag och körs aktivt inom stora bibliotek. Senaste updatering hos BLAS för C-kod var 2017. Alltså 2 år sedan. Inte illa, med tanke på att BLAS härstammar från 70-talet.

Men då är frågorna:

1. Vad är det som gör BLAS så effektivt?

2. Varför är inte denna kod effektiv? För beräkning av A*x = b, där A och x är kända men inte b.

// Enligt C99 så fungerar det att ha 2D arrayer som argument, så länge dimensionerna nämns före som argument void mat(int n, int m, A[n][m], x[n], b[n]){ for(int i = 0; i < n; i++){ for(int j = 0; j < m; j++){ b[i] = A[i][j]*x[i]; // Nu är detta indexeringsfel, men hoppas ni förstår vad jag är ute efter. } } }

Här är ett utdrag på hur kod i BLAS ser ut

#include "cblas.h" #include "cblas_f77.h" void cblas_cher2k(const enum CBLAS_ORDER Order, const enum CBLAS_UPLO Uplo, const enum CBLAS_TRANSPOSE Trans, const int N, const int K, const void *alpha, const void *A, const int lda, const void *B, const int ldb, const float beta, void *C, const int ldc) { char UL, TR; #ifdef F77_CHAR F77_CHAR F77_TR, F77_UL; #else #define F77_TR &TR #define F77_UL &UL #endif #ifdef F77_INT F77_INT F77_N=N, F77_K=K, F77_lda=lda, F77_ldb=ldb; F77_INT F77_ldc=ldc; #else #define F77_N N #define F77_K K #define F77_lda lda #define F77_ldb ldb #define F77_ldc ldc #endif extern int CBLAS_CallFromC; extern int RowMajorStrg; float ALPHA[2]; const float *alp=(float *)alpha; CBLAS_CallFromC = 1; RowMajorStrg = 0; if( Order == CblasColMajor ) { if( Uplo == CblasUpper) UL='U'; else if ( Uplo == CblasLower ) UL='L'; else { cblas_xerbla(2, "cblas_cher2k", "Illegal Uplo setting, %d\n", Uplo); CBLAS_CallFromC = 0; RowMajorStrg = 0; return; } if( Trans == CblasTrans) TR ='T'; else if ( Trans == CblasConjTrans ) TR='C'; else if ( Trans == CblasNoTrans ) TR='N'; else { cblas_xerbla(3, "cblas_cher2k", "Illegal Trans setting, %d\n", Trans); CBLAS_CallFromC = 0; RowMajorStrg = 0; return; } #ifdef F77_CHAR F77_UL = C2F_CHAR(&UL); F77_TR = C2F_CHAR(&TR); #endif F77_cher2k(F77_UL, F77_TR, &F77_N, &F77_K, alpha, A, &F77_lda, B, &F77_ldb, &beta, C, &F77_ldc); } else if (Order == CblasRowMajor) { RowMajorStrg = 1; if( Uplo == CblasUpper) UL='L'; else if ( Uplo == CblasLower ) UL='U'; else { cblas_xerbla(2, "cblas_cher2k", "Illegal Uplo setting, %d\n", Uplo); CBLAS_CallFromC = 0; RowMajorStrg = 0; return; } if( Trans == CblasTrans) TR ='N'; else if ( Trans == CblasConjTrans ) TR='N'; else if ( Trans == CblasNoTrans ) TR='C'; else { cblas_xerbla(3, "cblas_cher2k", "Illegal Trans setting, %d\n", Trans); CBLAS_CallFromC = 0; RowMajorStrg = 0; return; } #ifdef F77_CHAR F77_UL = C2F_CHAR(&UL); F77_TR = C2F_CHAR(&TR); #endif ALPHA[0]= *alp; ALPHA[1]= -alp[1]; F77_cher2k(F77_UL,F77_TR, &F77_N, &F77_K, ALPHA, A, &F77_lda, B, &F77_ldb, &beta, C, &F77_ldc); } else cblas_xerbla(1, "cblas_cher2k", "Illegal Order setting, %d\n", Order); CBLAS_CallFromC = 0; RowMajorStrg = 0; return; }

Permalänk
Vila i frid

Förenklat handlar det om optimerad inline assembler. För t.ex. Turbo Pascal finns direktivet "inline" som tillät assemblerkod i pascal-koden. Andra exempel @ https://sv.wikipedia.org/wiki/Inline_assembler

Permalänk
Medlem

Det finns mer än en implementation av BLAS.

Referensimplementationen är skriven i FORTRAN 77. Den kod du citerar är bara ett översättningslager för att anropa F77 rutinerna från C.
Referensimplementationen av BLAS är inte hårdoptimerad för hastighet, men det finns många andra implementationer av BLAS som är hårt optimerade -i en del fall till och med optimerade för en specifik hårdvara.

Permalänk
Skrivet av hasenfrasen:

Förenklat handlar det om optimerad inline assembler. För t.ex. Turbo Pascal finns direktivet "inline" som tillät assemblerkod i pascal-koden. Andra exempel @ https://sv.wikipedia.org/wiki/Inline_assembler

Skrivet av Erik_T:

Det finns mer än en implementation av BLAS.

Referensimplementationen är skriven i FORTRAN 77. Den kod du citerar är bara ett översättningslager för att anropa F77 rutinerna från C.
Referensimplementationen av BLAS är inte hårdoptimerad för hastighet, men det finns många andra implementationer av BLAS som är hårt optimerade -i en del fall till och med optimerade för en specifik hårdvara.

Så om jag vill ha ett enkelt bibliotek för att lösa matriser och det ska passa alla system. Vart vänder jag mig då?

Permalänk
Medlem
Skrivet av heretic16:

Så om jag vill ha ett enkelt bibliotek för att lösa matriser och det ska passa alla system. Vart vänder jag mig då?

Du kan börja med att titta igenom den här listan och se om du hittar något som passar dig: https://en.wikipedia.org/wiki/List_of_numerical_libraries

Permalänk
Skrivet av Erik_T:

Du kan börja med att titta igenom den här listan och se om du hittar något som passar dig: https://en.wikipedia.org/wiki/List_of_numerical_libraries

Denna länk har jag aldrig sett! Tack!

Jag tror detta passar mig bäst.
https://en.wikipedia.org/wiki/Portable,_Extensible_Toolkit_fo...

Jag är behov utav portibilitet.

Edit:
Såg att biblioteket krävde rätt mycket minne. Passar inte mig. Skulle behöva ett enkelt bibliotek. Tror ni att två for-loopar passar tillräckligt bra, eller är det kasst sätt att göra beräkningar på?

Permalänk
Medlem

Vad jag kunde förstå av koden du visade så var det ingen beräkning utan bara kod för att förbereda inför ett anrop av ett underliggande bibliotek skrivet i Fortran77. Det är där du hittar den effektiva koden.

Vad som är tillräckligt snabbt kan bara du svara på. Om man ställer krav på portabilitet så försvårar man möjligheter att dra fördel av den lokala hårdvaran. Att optimera loopar och minnesåtkomster till den lokala CPU-cachen, exempelvis. Så att så mycket som möjligt sker i cachen.

Moderna CPU:s har idag 8 eller 16 eller ändå fler trådar som kan dela på arbetet. Om du utnyttjar detta så kan du öka prestandan på din kod rejält. Eller om du använder bibliotek som utnyttjar detta. Datorer arbetar ofta i nätverk där det finns många andra kraftfulla datorer som kan dela på arbetet. Grafikkort kan idag vara många, många gånger mer kraftfulla än CPU för massivt parallella beräkningar. Tänk bitcoin mining-riggar med många grafikkort.

Det finns bibliotek som utnyttjar opencl och/eller Cuda som gör att du kan skriva kod som använder dessa resurser.

Visa signatur

Linux och Android

Permalänk
Skrivet av Adoby:

Vad jag kunde förstå av koden du visade så var det ingen beräkning utan bara kod för att förbereda inför ett anrop av ett underliggande bibliotek skrivet i Fortran77. Det är där du hittar den effektiva koden.

Vad som är tillräckligt snabbt kan bara du svara på. Om man ställer krav på portabilitet så försvårar man möjligheter att dra fördel av den lokala hårdvaran. Att optimera loopar och minnesåtkomster till den lokala CPU-cachen, exempelvis. Så att så mycket som möjligt sker i cachen.

Moderna CPU:s har idag 8 eller 16 eller ändå fler trådar som kan dela på arbetet. Om du utnyttjar detta så kan du öka prestandan på din kod rejält. Eller om du använder bibliotek som utnyttjar detta. Datorer arbetar ofta i nätverk där det finns många andra kraftfulla datorer som kan dela på arbetet. Grafikkort kan idag vara många, många gånger mer kraftfulla än CPU för massivt parallella beräkningar. Tänk bitcoin mining-riggar med många grafikkort.

Det finns bibliotek som utnyttjar opencl och/eller Cuda som gör att du kan skriva kod som använder dessa resurser.

Okej. För mig är det viktigt att det är litet samt portabelt Men då får jag väll använda egna for-loopar antar jag?

Permalänk
Medlem

Ingen som förbjuder dig, vad jag ser.

Men sedan kan det kanske vara intressant att kunna köra 8 eller 16 loopar samtidigt? C++ har det som standard i språket nu. Det borde vara portabelt nog. Jag har tittat på det, men det ligger inte i verktygslådan. Inte än.

Eller att rulla ut looparna, men inte mer än att de får plats i cachen. Ett vanligt knep för att maxa prestandan.

https://en.m.wikipedia.org/wiki/Loop_unrolling

Skickades från m.sweclockers.com

Visa signatur

Linux och Android

Permalänk
Skrivet av Adoby:

Ingen som förbjuder dig, vad jag ser.

Men sedan kan det kanske vara intressant att kunna köra 8 eller 16 loopar samtidigt? C++ har det som standard i språket nu. Det borde vara portabelt nog. Jag har tittat på det, men det ligger inte i verktygslådan. Inte än.

Eller att rulla ut looparna, men inte mer än att de får plats i cachen. Ett vanligt knep för att maxa prestandan.

https://en.m.wikipedia.org/wiki/Loop_unrolling

Skickades från m.sweclockers.com

Exakt! Jag har tänkt så där!

typ om jag får demonstrera!

void calculate(int n, int m, float* A, float* x, float* b){ // n = rader, m = kolumner for(int i = 0; i < n; i++){ for(int j = 0; j < m; j++){ *(b+i) += (*(A+i*m+ j++) * *(x+ j++)) + (*(A+i*m+ j++) * *(x+ j++)); } } }

Permalänk
Hedersmedlem
Skrivet av heretic16:

Exakt! Jag har tänkt så där!

typ om jag får demonstrera!

void calculate(int n, int m, float* A, float* x, float* b){ // n = rader, m = kolumner for(int i = 0; i < n; i++){ for(int j = 0; j < m; j++){ *(b+i) += (*(A+i*m+ j++) * *(x+ j++)) + (*(A+i*m+ j++) * *(x+ j++)); } } }

Ett litet tips. Om du behöver en kommentar som förklarar vad variabler betyder. Då bör du fundera på att istället döpa om variablerna.

Permalänk
Skrivet av Shimonu:

Ett litet tips. Om du behöver en kommentar som förklarar vad variabler betyder. Då bör du fundera på att istället döpa om variablerna.

I detta fall betyder n = antal rader, m = antal kollumer.

En C-array kan uttryckas som *(A + i) där A är arrayen och i är själva indexeringen. När man säger *(A + i*m + j) så betyder det att om i = 0 så är vi på rad 0 och då är det j som styr kollumindex. När i = 1 så är vi på nästa rad, för i*m så placeras vi på exakt nästa rad.

När jag skrev denna kod

*(b+i) += (*(A+i*m+ j++) * *(x+ j++)) + (*(A+i*m+ j++) * *(x+ j++));

Så betyder det att första (*(A+i*m+ j++) * *(x+ j++)) räknar ut första elementet om j = 0. Där efter blir j = 1. Så koden ovan kör alltså två operationer i matematiken på en rad.

Permalänk
Medlem
Skrivet av heretic16:

Exakt! Jag har tänkt så där!

typ om jag får demonstrera!

void calculate(int n, int m, float* A, float* x, float* b){ // n = rader, m = kolumner for(int i = 0; i < n; i++){ for(int j = 0; j < m; j++){ *(b+i) += (*(A+i*m+ j++) * *(x+ j++)) + (*(A+i*m+ j++) * *(x+ j++)); } } }

Att modifiera en variabel (t.ex. med j++) flera gånger i samma uttryck gör inte bara koden onödigt svårläst, det är också "undefined behavior" - vilket i korthet betyder att koden är buggig och kompilatorn får göra precis vad den vill när den stöter på sådan kod.

Jag rekommenderar dig att använda ett färdigt bibliotek trots allt. De innehåller vanligtvis korrekt kod.

Permalänk
Skrivet av Erik_T:

Att modifiera en variabel (t.ex. med j++) flera gånger i samma uttryck gör inte bara koden onödigt svårläst, det är också "undefined behavior" - vilket i korthet betyder att koden är buggig och kompilatorn får göra precis vad den vill när den stöter på sådan kod.

Jag rekommenderar dig att använda ett färdigt bibliotek trots allt. De innehåller vanligtvis korrekt kod.

Hittar dock inget bibliotek som passar mig. Hittar man ett bibliotek så är det enormt stort. Får inte vara större än 5 kB.

Permalänk
Medlem
Skrivet av heretic16:

Hittar dock inget bibliotek som passar mig. Hittar man ett bibliotek så är det enormt stort. Får inte vara större än 5 kB.

Så vad är det du behöver från ett sådant bibliotek?

Permalänk
Skrivet av Erik_T:

Så vad är det du behöver från ett sådant bibliotek?

Addition av vektorer och multiplikation med matriser och vektorer endast.
Typ b = A*x+c

Edit:
http://www.netlib.org/lapack/explore-html/d6/d30/group__singl...

Permalänk
Hedersmedlem
Skrivet av heretic16:

I detta fall betyder n = antal rader, m = antal kollumer.

En C-array kan uttryckas som *(A + i) där A är arrayen och i är själva indexeringen. När man säger *(A + i*m + j) så betyder det att om i = 0 så är vi på rad 0 och då är det j som styr kollumindex. När i = 1 så är vi på nästa rad, för i*m så placeras vi på exakt nästa rad.

När jag skrev denna kod

*(b+i) += (*(A+i*m+ j++) * *(x+ j++)) + (*(A+i*m+ j++) * *(x+ j++));

Så betyder det att första (*(A+i*m+ j++) * *(x+ j++)) räknar ut första elementet om j = 0. Där efter blir j = 1. Så koden ovan kör alltså två operationer i matematiken på en rad.

Jag vet hur matriser och tillhörande beräkningar fungerar. Det behöver du inte förklara. Jag försökte bara ge ett litet tips för att snygga till din kod.

Permalänk
Medlem

Det finns en del på github. Även sådana som bara är rena template/header-filer. Det fina med det är att ingen kompilerad kod behöver länkas in. Den faktiskt använda koden kompileras bara. (borde)

Prova att sök på "github c++ template matrix library"

Visa signatur

Linux och Android

Permalänk
Skrivet av Adoby:

Det finns en del på github. Även sådana som bara är rena template/header-filer. Det fina med det är att ingen kompilerad kod behöver länkas in. Den faktiskt använda koden kompileras bara. (borde)

Prova att sök på "github c++ template matrix library"

Nu har jag kokat ihop ett BLAS exempel

int main(){ real A[3*4]={ -0.0941, 0.4098, 0.1868, 0.6364, 0.5298, -0.1581, -0.0455, -0.0308, 0.1350, -0.2768, -0.4475, 0.0176}; real b[3*1]={42.1, 21.4, 53.1}; real x[4*1] ={4, 6, 53, -4}; integer m = 4; integer n = 3; real alpha = 1; real beta = 1; integer incx = 1; integer incy = 1; char trans = 'T'; sgemv_(&trans, &m, &n, &alpha, A, &m, x, &incx, &beta, b, &incy); printf("b[%i] = %f\n", 0, b[0]); printf("b[%i] = %f\n", 1, b[1]); printf("b[%i] = %f\n", 2, b[2]); }

Ja. Den fungerar! Har klippt endast sgemv_ och någon c-funktion till. 25 kb! Litet och smidigt. Koden skrev år 1986!!!

För den som beräknar transponatmatriser

#include <stdio.h> #include "Blas/f2c.h" #include <time.h> int main(){ real A[4*3]={ -0.0941, 0.5298, 0.1350, 0.4098, -0.1581, -0.2768, 0.1868, -0.0455, -0.4475, 0.6364, -0.0308, 0.0176}; real b[1*3]={ 50, 20, 10}; real x[1*4] ={4, 6, 53, -4}; integer m = 3; integer n = 4; real alpha = 1; real beta = 1; integer incx = 1; integer incy = 1; char trans = 'N'; clock_t start, end; float cpu_time_used; start = clock(); sgemv_(&trans, &m, &n, &alpha, A, &m, x, &incx, &beta, b, &incy); end = clock(); cpu_time_used = ((float) (end - start)) / CLOCKS_PER_SEC; printf("\nTotal speed was %f,", cpu_time_used); printf("b[%i] = %f\n", 0, b[0]); printf("b[%i] = %f\n", 1, b[1]); printf("b[%i] = %f\n", 2, b[2]); }

6e-5 sekunder tar det att göra denna beräkning. Är det snabbt?

b = A*x + b är formeln jag beräknar.

Permalänk
Vila i frid
Skrivet av heretic16:

Ja. Den fungerar! Har klippt endast sgemv_ och någon c-funktion till. 25 kb! Litet och smidigt. Koden skrev år 1986!!!

Matematik är inget nytt, inte heller programmering. https://sv.wikipedia.org/wiki/Antikytheramekanismen

Permalänk
Skrivet av hasenfrasen:

Matematik är inget nytt, inte heller programmering. https://sv.wikipedia.org/wiki/Antikytheramekanismen

Vad tror du? Koden från 1986 bör väll vara lika optimal idag som senaste BLAS versionen?

Permalänk
Datavetare
Skrivet av heretic16:

Vad tror du? Koden från 1986 bör väll vara lika optimal idag som senaste BLAS versionen?

Knappast. 1986 fanns inte CPU-finesser som SSE/AVX/NEON och FMA, dessa gör brutal skillnad i just matrismultiplikationer om de används rätt.

Vilken/vilka CPU-architektur(er) och vilket/vilka OS behöver du en snabb BLAS för?

OpenBLAS fungerar på väldigt många CPU-arkitekturer och den är SIMD/FMA optimerad för de populära CPU-arkitekturerna. Intels MKL är det snabbaste du hittar för x86.

MKL är en av då få bibliotek som fortfarande innehåller handoptimerande funktioner anpassade till specifika CPU-modeller (lite olika vägar beroende på saker som cache-storlek och version av SSE/AVX som stöds). Det är möjligt dels då Intel har resurserna, men också då matrismultiplikation är betydligt enklare att handoptimera än t.ex. en spelmotor eller en OS-kärna.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem
Skrivet av heretic16:

6e-5 sekunder tar det att göra denna beräkning. Är det snabbt?

Hur långt är ett snöre? Matriserna och vektorerna i dina exempel är extremt små, så du mäter troligtvis mer hur mycket overhead BLAS har snarare än hur lång tid själva beräkningen tar. Sätt upp ett test där du använder din riktiga data istället, och bestäm dig sen för om BLAS lever upp till dina behov eller ej.

Permalänk
Skrivet av Yoshman:

Knappast. 1986 fanns inte CPU-finesser som SSE/AVX/NEON och FMA, dessa gör brutal skillnad i just matrismultiplikationer om de används rätt.

Vilken/vilka CPU-architektur(er) och vilket/vilka OS behöver du en snabb BLAS för?

OpenBLAS fungerar på väldigt många CPU-arkitekturer och den är SIMD/FMA optimerad för de populära CPU-arkitekturerna. Intels MKL är det snabbaste du hittar för x86.

MKL är en av då få bibliotek som fortfarande innehåller handoptimerande funktioner anpassade till specifika CPU-modeller (lite olika vägar beroende på saker som cache-storlek och version av SSE/AVX som stöds). Det är möjligt dels då Intel har resurserna, men också då matrismultiplikation är betydligt enklare att handoptimera än t.ex. en spelmotor eller en OS-kärna.

Tackar! Om man säger så här då! en for-loop gjort av mig är väll sämre än denna blas rutin från 1986 ? hehe

Permalänk
Skrivet av perost:

Hur långt är ett snöre? Matriserna och vektorerna i dina exempel är extremt små, så du mäter troligtvis mer hur mycket overhead BLAS har snarare än hur lång tid själva beräkningen tar. Sätt upp ett test där du använder din riktiga data istället, och bestäm dig sen för om BLAS lever upp till dina behov eller ej.

Jag ska göra detta! Orsaken varför jag behöver ett BLAS som drar liten plats och är ej beroende av hårdvara, har med att jag behöver portabilitet samt kommer implementera det på inbyggda system för eget bruk.