C è più lento di Fortran nello shootout della norma spettrale (usando gcc, Intel e altri compilatori)?


13

La conclusione qui:

Quanto sono davvero migliori i compilatori Fortran?

è che gfortran e gcc sono veloci per un semplice codice. Quindi volevo provare qualcosa di più complicato. Ho preso l'esempio sparatutto della norma spettrale. Prima calcolo preliminare la matrice 2D A (:, :) e quindi calcolo la norma. (Questa soluzione non è consentita sulla sparatoria credo.) Ho implementato Fortran e la versione C. Ecco il codice:

https://github.com/certik/spectral_norm

Le versioni gfortran più veloci sono spectral_norm2.f90 e spectral_norm6.f90 (una utilizza il matmul e il dot_product integrati di Fortran, l'altra implementa queste due funzioni nel codice - senza differenze di velocità). Il codice C / C ++ più veloce che sono stato in grado di scrivere è spectral_norm7.cpp. I tempi a partire dalla versione 457d9d9 di git sul mio laptop sono:

$ time ./spectral_norm6 5500
1.274224153

real    0m2.675s
user    0m2.520s
sys 0m0.132s


$ time ./spectral_norm7 5500
1.274224153

real    0m2.871s
user    0m2.724s
sys 0m0.124s

Quindi la versione di gfortran è un po 'più veloce. Perché? Se invii una richiesta pull con un'implementazione C più veloce (o semplicemente incolli un codice), aggiornerò il repository.

In Fortran passo un array 2D in giro, mentre in CI uso un array 1D. Sentiti libero di usare un array 2D o qualsiasi altro modo che ritieni opportuno.

Per quanto riguarda i compilatori, confrontiamo gcc vs gfortran, icc vs ifort e così via. (A differenza della pagina di shootout, che confronta ifort vs gcc.)

Aggiornamento : usando la versione 179dae2, che migliora matmul3 () nella mia versione C, ora sono più veloci:

$ time ./spectral_norm6 5500
1.274224153

real    0m2.669s
user    0m2.500s
sys 0m0.144s

$ time ./spectral_norm7 5500
1.274224153

real    0m2.665s
user    0m2.472s
sys 0m0.168s

La versione vettoriale di Pedro qui sotto è più veloce:

$ time ./spectral_norm8 5500
1.274224153

real    0m2.523s
user    0m2.336s
sys 0m0.156s

Infine, come riporta lassxy di seguito per i compilatori Intel, non sembra esserci una grande differenza lì e anche il codice Fortran più semplice (spectral_norm1) è tra i più veloci.


5
Al momento non sono vicino a un compilatore, ma considera l'aggiunta della parola chiave restringente ai tuoi array. L'alias dei puntatori è in genere la differenza tra le chiamate di funzione Fortran e C sugli array. Inoltre, Fortran memorizza la memoria in ordine di colonna maggiore e C in ordine di riga maggiore.
Moyner,

1
-1 Il corpo di questa domanda parla di implementazioni, ma il titolo chiede quale lingua è più veloce? Come può una lingua avere un attributo di velocità? È necessario modificare il titolo della domanda in modo che rifletta il corpo della domanda.
milancurcica,

@ IRO-bot, l'ho risolto. Fammi sapere se sembra ok lo sai.
Ondřej Čertík,

1
In realtà le conclusioni su "Quanto sono davvero migliori i compilatori Fortran?" non sono del tutto corretti in quel thread. Avevo provato il benchmark su un Cray con compilatori GCC, PGI, CRAY e Intel e con 3 compilatori Fortran era più veloce di C (b / n 5-40%). I compilatori Cray hanno prodotto il codice Fortran / C più veloce ma il codice Fortran è stato del 40% più veloce. Pubblicherò risultati dettagliati quando avrò tempo. Tuttavia chiunque abbia accesso alle macchine Cray può verificare il benchmark. È una buona piattaforma perché sono disponibili 4-5 compilatori e i relativi flag vengono automaticamente attivati ​​dal wrapper ftn / cc.
Stali,

controllato anche con pgf95 / pgcc (11.10) su un sistema Opteron: # 1 e # 2 sono i più veloci (più veloci di ifort del ~ 20%), quindi # 6, # 8, # 7 (in questo ordine). pgf95 era più veloce di ifort per tutti i tuoi codici fortran e icpc era più veloce di pgcpp per tutti i C - Devo dire che per quanto mi riguarda, di solito trovo ifort più veloce, anche sullo stesso sistema AMD.
Lassxy,

Risposte:


12

Prima di tutto, grazie per aver pubblicato questa domanda / sfida! Come disclaimer, sono un programmatore C nativo con qualche esperienza Fortran e mi sento quasi a casa in C, quindi come tale, mi concentrerò solo sul miglioramento della versione C. Invito anche tutti gli hack di Fortran a provare!

Giusto per ricordare ai nuovi arrivati ​​di cosa si tratta: la premessa di base in questo thread era che gcc / fortran e icc / ifort dovrebbero, dato che hanno rispettivamente gli stessi back-end, produrre codice equivalente per lo stesso programma (semanticamente identico), indipendentemente di esso in C o Fortran. La qualità del risultato dipende solo dalla qualità delle rispettive implementazioni.

Ho giocato un po 'con il codice e sul mio computer (ThinkPad 201x, Intel Core i5 M560, 2,67 GHz), usando gcc4.6.1 e le seguenti bandiere del compilatore:

GCCFLAGS= -O3 -g -Wall -msse2 -march=native -funroll-loops -ffast-math -fomit-frame-pointer -fstrict-aliasing

Sono anche andato avanti e ho scritto una versione del codice C ++ in linguaggio C SIMD vettoriale spectral_norm_vec.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

/* Define the generic vector type macro. */  
#define vector(elcount, type)  __attribute__((vector_size((elcount)*sizeof(type)))) type

double Ac(int i, int j)
{
    return 1.0 / ((i+j) * (i+j+1)/2 + i+1);
}

double dot_product2(int n, double u[], double v[])
{
    double w;
    int i;
    union {
        vector(2,double) v;
        double d[2];
        } *vu = u, *vv = v, acc[2];

    /* Init some stuff. */
    acc[0].d[0] = 0.0; acc[0].d[1] = 0.0;
    acc[1].d[0] = 0.0; acc[1].d[1] = 0.0;

    /* Take in chunks of two by two doubles. */
    for ( i = 0 ; i < (n/2 & ~1) ; i += 2 ) {
        acc[0].v += vu[i].v * vv[i].v;
        acc[1].v += vu[i+1].v * vv[i+1].v;
        }
    w = acc[0].d[0] + acc[0].d[1] + acc[1].d[0] + acc[1].d[1];

    /* Catch leftovers (if any) */
    for ( i = n & ~3 ; i < n ; i++ )
        w += u[i] * v[i];

    return w;

}

void matmul2(int n, double v[], double A[], double u[])
{
    int i, j;
    union {
        vector(2,double) v;
        double d[2];
        } *vu = u, *vA, vi;

    bzero( u , sizeof(double) * n );

    for (i = 0; i < n; i++) {
        vi.d[0] = v[i];
        vi.d[1] = v[i];
        vA = &A[i*n];
        for ( j = 0 ; j < (n/2 & ~1) ; j += 2 ) {
            vu[j].v += vA[j].v * vi.v;
            vu[j+1].v += vA[j+1].v * vi.v;
            }
        for ( j = n & ~3 ; j < n ; j++ )
            u[j] += A[i*n+j] * v[i];
        }

}


void matmul3(int n, double A[], double v[], double u[])
{
    int i;

    for (i = 0; i < n; i++)
        u[i] = dot_product2( n , &A[i*n] , v );

}

void AvA(int n, double A[], double v[], double u[])
{
    double tmp[n] __attribute__ ((aligned (16)));
    matmul3(n, A, v, tmp);
    matmul2(n, tmp, A, u);
}


double spectral_game(int n)
{
    double *A;
    double u[n] __attribute__ ((aligned (16)));
    double v[n] __attribute__ ((aligned (16)));
    int i, j;

    /* Aligned allocation. */
    /* A = (double *)malloc(n*n*sizeof(double)); */
    if ( posix_memalign( (void **)&A , 4*sizeof(double) , sizeof(double) * n * n ) != 0 ) {
        printf( "spectral_game:%i: call to posix_memalign failed.\n" , __LINE__ );
        abort();
        }


    for (i = 0; i < n; i++) {
        for (j = 0; j < n; j++) {
            A[i*n+j] = Ac(i, j);
        }
    }


    for (i = 0; i < n; i++) {
        u[i] = 1.0;
    }
    for (i = 0; i < 10; i++) {
        AvA(n, A, u, v);
        AvA(n, A, v, u);
    }
    free(A);
    return sqrt(dot_product2(n, u, v) / dot_product2(n, v, v));
}

int main(int argc, char *argv[]) {
    int i, N = ((argc >= 2) ? atoi(argv[1]) : 2000);
    for ( i = 0 ; i < 10 ; i++ )
        printf("%.9f\n", spectral_game(N));
    return 0;
}

Tutte e tre le versioni sono state compilate con gli stessi flag e la stessa gccversione. Si noti che ho racchiuso la chiamata della funzione principale in un ciclo da 0 a 9 per ottenere tempi più precisi.

$ time ./spectral_norm6 5500
1.274224153
...
real    0m22.682s
user    0m21.113s
sys 0m1.500s

$ time ./spectral_norm7 5500
1.274224153
...
real    0m21.596s
user    0m20.373s
sys 0m1.132s

$ time ./spectral_norm_vec 5500
1.274224153
...
real    0m21.336s
user    0m19.821s
sys 0m1.444s

Quindi, con flag di compilatore "migliori", la versione C ++ supera la versione Fortran e i loop vettorizzati codificati a mano forniscono solo un miglioramento marginale. Una rapida occhiata all'assemblatore per la versione C ++ mostra che anche i loop principali sono stati vettorializzati, anche se srotolati in modo più aggressivo.

Ho anche dato un'occhiata all'assemblatore generato da gfortraned ecco la grande sorpresa: nessuna vettorializzazione. Attribuisco il fatto che è solo leggermente più lento al problema che la larghezza di banda è limitata, almeno sulla mia architettura. Per ciascuna delle moltiplicazioni di matrice, vengono attraversati 230 MB di dati, che praticamente sommergono tutti i livelli di cache. Se si utilizza un valore di input inferiore, ad esempio 100, le differenze di prestazioni aumentano considerevolmente.

Come nota a margine, invece di essere ossessionato dalla flag di vettorializzazione, allineamento e compilatore, l'ottimizzazione più ovvia sarebbe quella di calcolare le prime iterazioni in aritmetica a precisione singola, fino a quando non avremo ~ 8 cifre del risultato. Le istruzioni a precisione singola non sono solo più veloci, ma anche la quantità di memoria che deve essere spostata viene dimezzata.


Grazie mille per il tuo tempo! Speravo che avresti risposto. :) Quindi, per prima cosa ho aggiornato il Makefile per usare le tue bandiere. Quindi ho inserito il tuo codice C come spectral_norm8.c e ho aggiornato README. Ho aggiornato i tempi sulla mia macchina ( github.com/certik/spectral_norm/wiki/Timings ) e come puoi vedere, i flag del compilatore non hanno reso la versione C più veloce sulla mia macchina (cioè gfortran vince ancora), ma il tuo SIMD vettoriale la versione batte gfortran.
Ondřej Čertík,

@ OndřejČertík: Solo per curiosità, quale versione di gcc/ gfortranstai usando? Nei thread precedenti, versioni diverse hanno dato risultati significativamente diversi.
Pedro,

Uso 4.6.1-9ubuntu3. Hai accesso ai compilatori Intel? La mia esperienza con gfortran è che a volte non produce (ancora) un codice ottimale. Normalmente lo fa.
Ondřej Čertík,

1
@ OndřejČertík: ora i risultati hanno più senso! Avevo trascurato che matmul2nella versione Fortran è semanticamente equivalente alla matmul3mia versione C. Le due versioni sono davvero le stesse e quindi gcc/ gfortran dovrebbero produrre gli stessi risultati per entrambi, ad esempio in questo caso nessun front-end / linguaggio è migliore dell'altro. gccha solo il vantaggio di poter sfruttare le istruzioni vettoriali se dovessimo scegliere.
Pedro,

1
@ cjordan1: ho scelto di utilizzare l' vector_sizeattributo per rendere il codice indipendente dalla piattaforma, cioè usando questa sintassi, gccdovrei essere in grado di generare codice vettoriale per altre piattaforme, ad esempio usando AltiVec sull'architettura IBM Power.
Pedro,

7

La risposta di user389 è stata cancellata, ma lasciatemi dire che sono fermamente nel suo campo: non riesco a vedere cosa impariamo confrontando i micro-benchmark in diverse lingue. Non mi sorprende molto il fatto che C e Fortran ottengano praticamente le stesse prestazioni su questo benchmark dato che è breve. Ma il benchmark è anche noioso poiché può essere facilmente scritto in entrambe le lingue in una dozzina di righe. Dal punto di vista del software, questo non è un caso rappresentativo: dovremmo preoccuparci del software che ha 10.000 o 100.000 righe di codice e di come i compilatori lo fanno. Naturalmente, a quella scala, si scopriranno rapidamente altre cose: quella lingua A richiede 10.000 linee mentre la lingua B ne richiede 50.000. O viceversa, a seconda di cosa vuoi fare. E all'improvviso '

In altre parole, non importa molto per me che forse la mia applicazione potrebbe essere più veloce del 50% se la sviluppassi in Fortran 77 se invece mi ci vorrà solo 1 mese per farla funzionare correttamente mentre mi richiederebbe 3 mesi in F77. Il problema con la domanda qui è che si concentra su un aspetto (singoli kernel) che non è rilevante nella pratica a mio avviso.


Concordato. Per quello che vale, a parte modifiche molto, molto minori (-3 caratteri, +9 caratteri), ho concordato con il sentimento principale della sua risposta. Per quanto ne so, il dibattito sul compilatore C ++ / C / Fortran è importante solo quando uno ha esaurito ogni altra strada possibile per il miglioramento delle prestazioni, motivo per cui, per il 99,9% delle persone, questi confronti non contano. Non trovo la discussione particolarmente illuminante, ma conosco almeno una persona sul sito che può attestare di aver scelto Fortran su C e C ++ per motivi di prestazioni, motivo per cui non posso dire che sia totalmente inutile.
Geoff Oxberry,

4
Sono d'accordo con il tuo punto principale, ma penso ancora che questa discussione sia utile in quanto ci sono un certo numero di persone là fuori che ancora credono in qualche modo che ci sia qualche magia che rende una lingua "più veloce" dell'altra, nonostante l'uso di un compilatore identico backend. Contribuisco a questi dibattiti principalmente per cercare di dissipare questo mito. Per quanto riguarda la metodologia, non esiste un "caso rappresentativo" e, a mio avviso, prendere qualcosa di semplice come moltiplicare il vettore matrice è una buona cosa, in quanto offre ai compilatori abbastanza spazio per mostrare cosa possono fare o meno.
Pedro,

@GeoffOxberry: Certo, troverai sempre persone che usano una lingua piuttosto che un'altra per cause più o meno ben articolate e ragionate. La mia domanda, tuttavia, sarebbe quanto veloce sarebbe Fortran se si dovessero usare le strutture di dati che appaiono, per esempio, in maglie di elementi finiti adattivi non strutturati. A parte il fatto che questo sarebbe imbarazzante da implementare in Fortran (tutti quelli che lo implementano in C ++ usano pesantemente lo STL in tutto), Fortran sarebbe davvero più veloce per questo tipo di codice che non ha loop stretti, molte indirette, molti if?
Wolfgang Bangerth,

@WolfgangBangerth: Come ho detto nel mio primo commento, sono d'accordo con te e con l'utente389 (Jonathan Dursi), quindi farmi questa domanda è inutile. Detto questo, vorrei invitare chi non crede che la scelta della lingua (tra C ++ / C / Fortran) è importante per le prestazioni nella loro applicazione a rispondere alla tua domanda. Purtroppo, sospetto che si possa avere questo tipo di dibattito per le versioni del compilatore.
Geoff Oxberry,

@GeoffOxberry: Sì, e io ovviamente non voglio dire che si bisogno di rispondere a questa domanda.
Wolfgang Bangerth,

5

Si scopre che posso scrivere un codice Python (usando numpy per eseguire le operazioni BLAS) più velocemente del codice Fortran compilato con il compilatore gfortran del mio sistema.

$ gfortran -o sn6a sn6a.f90 -O3 -march=native
    
    $ ./sn6a 5500
1.274224153
1.274224153
1.274224153
   1.9640001      sec per iteration

$ python ./foo1.py
1.27422415279
1.27422415279
1.27422415279
1.20618661245 sec per iteration

foo1.py:

import numpy
import scipy.linalg
import timeit

def specNormDot(A,n):
    u = numpy.ones(n)
    v = numpy.zeros(n)

    for i in xrange(10):
        v  = numpy.dot(numpy.dot(A,u),A)
        u  = numpy.dot(numpy.dot(A,v),A)

    print numpy.sqrt(numpy.vdot(u,v)/numpy.vdot(v,v))

    return

n = 5500

ii, jj = numpy.meshgrid(numpy.arange(1,n+1), numpy.arange(1,n+1))
A  = (1./((ii+jj-2.)*(ii+jj-1.)/2. + ii))

t = timeit.Timer("specNormDot(A,n)", "from __main__ import specNormDot,A,n")
ntries = 3

print t.timeit(ntries)/ntries, "sec per iteration"

e sn6a.f90, uno spettrale_norm6.f90 leggermente modificato:

program spectral_norm6
! This uses spectral_norm3 as a starting point, but does not use the
! Fortrans
! builtin matmul and dotproduct (to make sure it does not call some
! optimized
! BLAS behind the scene).
implicit none

integer, parameter :: dp = kind(0d0)
real(dp), allocatable :: A(:, :), u(:), v(:)
integer :: i, j, n
character(len=6) :: argv
integer :: calc, iter
integer, parameter :: niters=3

call get_command_argument(1, argv)
read(argv, *) n

allocate(u(n), v(n), A(n, n))
do j = 1, n
    do i = 1, n
        A(i, j) = Ac(i, j)
    end do
end do

call tick(calc)

do iter=1,niters
    u = 1
    do i = 1, 10
        v = AvA(A, u)
        u = AvA(A, v)
    end do

    write(*, "(f0.9)") sqrt(dot_product2(u, v) / dot_product2(v, v))
enddo

print *, tock(calc)/niters, ' sec per iteration'

contains

pure real(dp) function Ac(i, j) result(r)
integer, intent(in) :: i, j
r = 1._dp / ((i+j-2) * (i+j-1)/2 + i)
end function

pure function matmul2(v, A) result(u)
! Calculates u = matmul(v, A), but much faster (in gfortran)
real(dp), intent(in) :: v(:), A(:, :)
real(dp) :: u(size(v))
integer :: i
do i = 1, size(v)
    u(i) = dot_product2(A(:, i), v)
end do
end function

pure real(dp) function dot_product2(u, v) result(w)
! Calculates w = dot_product(u, v)
real(dp), intent(in) :: u(:), v(:)
integer :: i
w = 0
do i = 1, size(u)
    w = w + u(i)*v(i)
end do
end function

pure function matmul3(A, v) result(u)
! Calculates u = matmul(v, A), but much faster (in gfortran)
real(dp), intent(in) :: v(:), A(:, :)
real(dp) :: u(size(v))
integer :: i, j
u = 0
do j = 1, size(v)
    do i = 1, size(v)
        u(i) = u(i) + A(i, j)*v(j)
    end do
end do
end function

pure function AvA(A, v) result(u)
! Calculates u = matmul2(matmul3(A, v), A)
! In gfortran, this function is sligthly faster than calling
! matmul2(matmul3(A, v), A) directly.
real(dp), intent(in) :: v(:), A(:, :)
real(dp) :: u(size(v))
u = matmul2(matmul3(A, v), A)
end function

subroutine tick(t)
    integer, intent(OUT) :: t

    call system_clock(t)
end subroutine tick

! returns time in seconds from now to time described by t 
real function tock(t)
    integer, intent(in) :: t
    integer :: now, clock_rate

    call system_clock(now,clock_rate)

    tock = real(now - t)/real(clock_rate)
end function tock
end program

1
Lingua nella guancia, presumo?
Robert Harvey,

-1 per non aver risposto alla domanda, ma penso che tu lo sappia già.
Pedro,

Interessante, quale versione di gfortran hai usato e hai testato il codice C disponibile nel repository con le bandiere di Pedro?
Aron Ahmadia,

1
In realtà, penso che ora sia più chiaro, supponendo che non saresti stato sarcastico.
Robert Harvey,

1
Dal momento che questo post, e nessuna delle altre domande o post, sono stati modificati da Aron in modo da abbinare meglio le sue opinioni, anche se il mio punto è che tutti i post dovrebbero essere etichettati esattamente con "tali risultati non hanno senso" avvertenze, lo sto solo cancellando.

3

Controllato con i compilatori Intel. Con 11.1 (-fast, implicando -O3) e con 12.0 (-O2) i più veloci sono 1,2,6,7 e 8 (ovvero i codici Fortran e C "più semplici" e la C vettoriale vettoriale) - sono indistinguibili l'uno dall'altro a ~ 1,5 s. I test 3 e 5 (con array come funzione) sono più lenti; # 4 Non ho potuto compilare.

In particolare, se si compila con 12.0 e -O3, anziché -O2, i primi 2 ("più semplici") codici Fortran rallentano MOLTO (1,5 -> 10,2 secondi) - questa non è la prima volta che vedo qualcosa di simile questo, ma questo potrebbe essere l'esempio più drammatico. Se questo è ancora il caso nella versione attuale, penso che sarebbe una buona idea segnalarlo a Intel, poiché c'è chiaramente qualcosa che non va nelle loro ottimizzazioni in questo caso piuttosto semplice.

Altrimenti concordo con Jonathan che questo non è un esercizio particolarmente istruttivo :)


Grazie per averlo verificato! Ciò conferma la mia esperienza, che gfortran non è ancora completamente maturo, perché per qualche ragione l'operazione matmul è lenta. Quindi la conclusione per me è semplicemente usare matmul e mantenere semplice il codice Fortran.
Ondřej Čertík,

D'altra parte, penso che gfortran abbia un'opzione da riga di comando per convertire automaticamente tutte le chiamate matmul () in chiamate BLAS (forse anche dot_product (), non sono sicuro). Non l'ho mai provato però.
Lassxy,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.