Це C повільніше, ніж Фортран, на перестрілці спектральної норми (використовуючи gcc, intel та інші компілятори)?


13

Висновок тут:

Наскільки краще насправді компілятори Fortran?

полягає в тому, що gfortran і gcc настільки швидкі для простого коду. Тому я хотів спробувати щось складніше. Я взяв приклад зйомки спектральної норми. Спочатку попередньо підраховую 2D матрицю A (:, :), а потім обчислюю норму. (Я думаю, що це рішення заборонено на перестрілці.) Я реалізував версію Fortran і C. Ось код:

https://github.com/certik/spectral_norm

Найшвидші версії gfortran - це spectral_norm2.f90 та spectral_norm6.f90 (одна використовує вбудований matmul та dot_product Fortran, інша реалізує ці дві функції в коді - без різниці у швидкості). Найшвидший код C / C ++, який мені вдалося написати, це spectral_norm7.cpp. Тим часом на моєму ноутбуці git версії 457d9d9 є:

$ 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

Тож версія gfortran трохи швидша. Чому так? Якщо ви надішлете запит на потяг із швидшою реалізацією на C (або просто вставити код), я оновлю сховище.

У Fortran я пропускаю 2D масив навколо, тоді як в CI використовую 1D масив. Сміливо використовуйте 2D-масив або будь-який інший спосіб, який вважаєте за потрібне.

Щодо компіляторів, давайте порівняємо gcc vs gfortran, icc vs ifort тощо. (На відміну від сторінки перестрілки, яка порівнює ifort проти gcc.)

Оновлення : використовуючи версію 179dae2, яка покращує matmul3 () у моїй версії C, вони зараз такі швидкі:

$ 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

Векторизована версія Педро, наведена нижче, швидша:

$ time ./spectral_norm8 5500
1.274224153

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

Нарешті, як наведені нижче звіти про лексичні описи для компіляторів Intel, там, схоже, немає великої різниці, і навіть найпростіший код Fortran (спектральний_норм1) є одним з найшвидших.


5
Зараз я ніде не знаходяться поблизу компілятора, але розглядаю можливість додавання ключового слова обмеження до масивів. Згладжування покажчиків зазвичай є різницею між викликами функцій Fortran та C на масивах. Крім того, Fortran зберігає пам’ять у порядку стовпців-мажорів та C у рядку-мажор.
Мойнер

1
-1 Основна частина цього питання розповідає про реалізацію, але заголовок запитує, яка мова швидша? Як мова може мати атрибут швидкості? Ви повинні відредагувати назву питання, щоб вона відображала суть питання.
milancurcic

@ IRO-бот, я це виправив. Дайте мені знати, якщо вам це здається нормальним.
Ondřej Čertík

1
Власне, висновки про "Наскільки краще насправді компілятори Fortran?" не зовсім коректні в цій темі. Я спробував орієнтир на Cray з компіляторами GCC, PGI, CRAY та Intel, а з 3 компіляторами Fortran був швидшим за C (б / в 5-40%). Cray компілятори створили найшвидший код Fortran / C, але код Fortran був на 40% швидшим. Я опублікую детальні результати, коли знайду час. Тим, хто має доступ до машин Cray, може перевірити показник. Це хороша платформа, тому що наявні 4-5 компіляторів, а відповідні прапори автоматично включаються обгорткою ftn / cc.
stali

також перевірено за допомогою pgf95 / pgcc (11.10) в системі Opteron: №1 і №2 є найшвидшими (швидше, ніж ifort на ~ 20%), тоді №6, №8, №7 (у такому порядку). pgf95 був швидшим за ifort для всіх ваших кодів fortran, а icpc - швидше, ніж pgcpp для всіх C - я мушу зазначити, що для своїх речей я зазвичай знаходжу ifort швидше навіть у тій же системі AMD.
laxxy

Відповіді:


12

Перш за все, дякую за опублікування цього питання / виклику! В якості відмови я є рідним програмістом на C, який має певний досвід Fortran, і відчуваю, що вдома найбільше відчуваю себе вдома, тому, як такий, я зосереджуся лише на вдосконаленні версії C. Я запрошую всіх хакків Fortran, щоб вони теж ходили!

Просто нагадуючи новачкам про що це: Основна передумова цього потоку полягала в тому, що gcc / fortran та icc / ifort повинні, оскільки вони мають однакові зворотні сторони відповідно, створювати еквівалентний код для тієї ж (семантично ідентичної) програми, незалежно. про те, що він знаходиться в С або Фортран. Якість результату залежить лише від якості відповідних реалізацій.

Я трохи пограв з кодом і на своєму комп’ютері (ThinkPad 201x, Intel Core i5 M560, 2.67 ГГц), використовуючи gcc4.6.1 та наступні прапорці компілятора:

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

Я також пішов вперед і написав СЕМ-векторизовану C-мовну версію коду C ++ 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;
}

Усі три версії були складені з однаковими прапорами та однаковою gccверсією. Зауважте, що я завернув виклик основної функції в циклі від 0..9, щоб отримати більш точні таймінги.

$ 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

Таким чином, з "кращими" прапорцями компілятора версія C ++ виконує версію Fortran, а кодовані вручну векторизовані петлі забезпечують лише незначне поліпшення. Швидкий погляд на асемблер для версії C ++ показує, що основні петлі також були векторизовані, хоча й розгорнуті більш агресивно.

Я також подивився на асемблер, що генерується, gfortranі ось тут великий сюрприз: відсутність векторизації. Я приписую той факт, що проблема лише обмежена, оскільки проблема обмежена пропускною здатністю, принаймні, в моїй архітектурі. Для кожного матричного множення проходить 230 Мб даних, що в значній мірі перекриває всі рівні кешу. Якщо ви використовуєте менші вхідні значення, наприклад 100, різниця в продуктивності значно зростає.

Як бічна примітка, замість того, щоб нав'язувати прапори векторизації, вирівнювання та компілятора, найбільш очевидною оптимізацією було б обчислити перші кілька ітерацій в одноточній арифметиці, поки ми не отримаємо ~ 8 цифр результату. Інструкції з одноточною швидкістю не тільки швидші, але і об'єм пам'яті, яку потрібно перемістити, також скорочується вдвічі.


Дякую за ваш час! Я сподівався, що ви відповісте. :) Тому спочатку я оновив Makefile, щоб використовувати ваші прапори. Тоді я поставив ваш код С у спектральний_норм.c та оновив README. Я оновив таймінги на своїй машині ( github.com/certik/spectral_norm/wiki/Timings ), і, як ви бачите, прапори компілятора не зробили версію С швидшою на моїй машині (тобто gfortran все ще перемагає), але ваш SIMD-векторний версія б'є gfortran.
Ondřej Čertík

@ OndřejČertík: Просто з цікавості, яку версію gcc/ gfortranви використовуєте? У попередніх потоках різні версії дали суттєво різні результати.
Педро

Я використовую 4.6.1-9ubuntu3. Чи маєте ви доступ до компіляторів Intel? Мій досвід роботи з gfortran полягає в тому, що іноді він (ще) не дає оптимального коду. IFort, як правило, робить.
Ondřej Čertík

1
@ OndřejČertík: Тепер результати мають більше сенсу! Я не помітив, що matmul2у версії Fortran семантично еквівалентна matmul3моїй версії C. Дві версії справді однакові і, таким чином, gcc/ gfortran повинні давати однакові результати для обох, наприклад, жодна передня мова / мова не краща за іншу в цьому випадку. gccякраз має ту перевагу, що ми можемо використовувати векторизовані інструкції, якби вибрати.
Педро

1
@ cjordan1: Я вирішив використовувати vector_sizeатрибут для того, щоб зробити платформу коду незалежною, тобто використовуючи цей синтаксис, gccповинен мати можливість генерувати векторизований код для інших платформ, наприклад, використовуючи AltiVec в архітектурі IBM Power.
Педро

7

Відповідь користувача389 видалено, але дозвольте мені зазначити, що я твердо перебуваю в його таборі: я не бачу того, що ми дізнаємось, порівнюючи мікро-показники на різних мовах. Мене не дивно, що C і Fortran отримують майже однакові показники за цим показником, враховуючи, наскільки він короткий. Але еталон також нудний, оскільки його можна легко записати двома мовами в кілька десятків рядків. З точки зору програмного забезпечення, це не є репрезентативним випадком: нам слід подбати про програмне забезпечення, яке містить 10 000 або 100 000 рядків коду, і про те, як це робити компілятори. Звичайно, в такому масштабі швидко з’ясуються інші речі: для цієї мови A потрібно 10 000 рядків, тоді як для мови B потрібно 50 000. Або навпаки, залежно від того, що ви хочете зробити. І раптом це '

Іншими словами, для мене це не має великого значення, що, можливо, моя програма може бути на 50% швидшою, якщо я розробив її у Fortran 77, якщо замість цього мені знадобиться лише 1 місяць, щоб змусити її запуститись правильно, тоді як це займе у мене 3 місяці у F77. Проблема тут полягає в тому, що він зосереджений на аспекті (окремих ядрах), який не є актуальним на моєму погляді.


Домовились. Що варто, окрім дуже-дуже незначних змін (-3 символи, +9 символів), я погодився з головними почуттями його відповіді. Наскільки я знаю, дискусія щодо компілятора C ++ / C / Fortran має значення лише тоді, коли вичерпали всі інші можливі шляхи для підвищення продуктивності, тому для 99,9% людей ці порівняння не мають значення. Я не вважаю дискусію особливо освічуючою, але я знаю щонайменше однієї людини на сайті, яка може засвідчити вибір Fortran над C та C ++ з міркувань продуктивності, тому я не можу сказати, що це абсолютно марно.
Джефф Оксберрі

4
Я погоджуюся з вашим основним моментом, але я все ж вважаю, що ця дискусія корисна, оскільки там є ряд людей, які все ще якось вважають, що є якась магія, яка робить одну мову "швидшою", ніж іншу, незважаючи на використання однакового компілятора мікстури. Я беру участь у цих дебатах головним чином, щоб спробувати розвіяти цей міф. Що стосується методології, то тут немає "репрезентативного випадку", і на мою думку, прийняття чогось такого простого, як множення матриць-векторів, є хорошою справою, оскільки дає компіляторам достатньо місця, щоб показати, що вони можуть робити чи ні.
Педро

@GeoffOxberry: Звичайно, ви завжди знайдете людей, які використовують одну мову, а не іншу, для більш-менш чітко сформульованих та аргументованих причин. Моє питання, однак, якою швидкістю буде Fortran, якби можна було використовувати структури даних, які з'являються, скажімо, в неструктурованих адаптивних сітках кінцевих елементів. Крім того, що це було б незручно реалізувати у Fortran (кожен, хто реалізує це в C ++, широко використовує STL на всьому протязі), чи дійсно Fortran був би швидшим для такого типу коду, який не має жорстких циклів, багато непрямих напрямків, багато ifs?
Вольфганг Бангерт

@WolfgangBangerth: Як я вже говорив у своєму першому коментарі, я згоден з вами та з користувачем389 (Джонатан Дурсі), тому задавати мені це питання безглуздо. Тим НЕ менше, я хотів би запросити всіх , хто робить вірити , що вибір мови ( в тому числі C ++ / C / Fortran) має важливе значення для виконання в їх застосуванні , щоб відповісти на ваше запитання. На жаль, я підозрюю, що подібні дебати можуть бути у версії компілятора.
Джефф Оксберрі

@GeoffOxberry: Так, і я, очевидно, не мав на увазі, що вам потрібно відповісти на це питання.
Вольфганг Бангерт

5

Виявляється, я можу записати код Python (використовуючи numpy для виконання операцій BLAS) швидше, ніж код Fortran, зібраний з компілятором gfortran моєї системи.

$ 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"

і sn6a.f90, дуже незначно модифікований спектральний_норм.f90:

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
Я припускаю, що мова в щоці?
Роберт Харві

-1 за те, що не відповів на питання, але я думаю, ви це вже знаєте.
Педро

Цікаво, яку версію gfortran ви використовували, і чи ви перевіряли код С, доступний у сховищі, зі прапорцями Педро?
Арон Ахмадія

1
Насправді, я думаю, зараз це зрозуміліше, якщо вважати, що ти не був саркастичним.
Роберт Харві

1
Оскільки цей пост, і жодне з інших запитань чи публікацій, Арон не редагує таким чином, щоб краще відповідати його думкам, хоча вся моя думка полягає в тому, що всі повідомлення повинні бути марковані саме такими "ці результати безглузді" застереження, я просто його видаляю.

3

Перевірив це за допомогою компіляторів Intel. З 11.1 (-швидкий, маючи на увазі -O3) і з 12.0 (-O2) найшвидшими є 1,2,6,7 і 8 (тобто "найпростіші" коди Фортран і С, і ручний вектор С) - вони не відрізняються один від одного за ~ 1,5 с. Тести 3 і 5 (з масивом як функцією) проходять повільніше; №4 я не зміг скласти.

Зовсім помітно, якщо компіляція з 12.0 і -O3, а не -O2, перші 2 ("найпростіші") коди Fortran сповільнюють багато (1,5 -> 10,2 сек.) - це не перший раз, коли я бачу щось подібне це, але це може бути найдраматичніший приклад. Якщо це все ще відбувається в поточному випуску, я думаю, було б непогано повідомити про це Intel, оскільки в їх досить простому випадку очевидно щось не так з їх оптимізаціями.

Інакше я погоджуюся з Джонатаном, що це не особливо інформативна вправа :)


Дякуємо за перевірку! Це підтверджує мій досвід, що gfortran ще не повністю дозрів, оскільки чомусь робота matmul відбувається повільно. Отже, для мене висновок - просто використовувати matmul і зберігати код Fortran просто.
Ondřej Čertík

З іншого боку, я думаю, що у gfortran є варіант командного рядка для автоматичного перетворення всіх викликів matmul () у виклики BLAS (можливо, також dot_product (), не впевнений). Ніколи не пробував, хоча.
laxxy
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.