스펙트럼 규범 총격 (gcc, 인텔 및 기타 컴파일러 사용)에서 C가 Fortran보다 느립니까?


13

결론은 다음과 같습니다.

포트란 컴파일러는 실제로 얼마나 나을까?

gfortran과 gcc는 간단한 코드만큼 빠릅니다. 그래서 더 복잡한 것을 시도하고 싶었습니다. 나는 스펙트럼 규범 총격 사건의 예를 들었습니다. 먼저 2D 행렬 A (:, :)를 미리 계산 한 다음 표준을 계산합니다. (이 솔루션은 제 생각에는 총격전에서는 허용되지 않습니다.) Fortran 및 C 버전을 구현했습니다. 코드는 다음과 같습니다.

https://github.com/certik/spectral_norm

가장 빠른 gfortran 버전은 spectral_norm2.f90 및 spectral_norm6.f90입니다 (하나는 Fortran의 내장 matmul 및 dot_product를 사용하고 다른 하나는 코드에서이 두 기능을 속도 차이없이 구현 함). 내가 작성할 수있는 가장 빠른 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 대 gfortran, icc 대 ifort 등을 비교해 봅시다. ifort와 gcc를 비교하는 총격전 페이지와 달리

업데이트 : 내 C 버전에서 matmul3 ()을 향상시키는 버전 179dae2를 사용하면 속도가 빨라집니다.

$ 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

마지막으로, 인텔 컴파일러에 대한 laxxy가 아래에보고 한 것처럼 큰 차이는 없으며 가장 간단한 포트란 코드 (spectral_norm1)도 가장 빠릅니다.


5
나는 지금 컴파일러 근처에 있지 않지만 배열에 restrict 키워드를 추가하는 것을 고려하십시오. 포인터의 별칭은 일반적으로 배열에서 Fortran과 C 함수 호출의 차이입니다. 또한 포트란은 열 주요 순서로 메모리를 저장하고 C는 행 주요 순서로 저장합니다.
moyner

1
-1이 질문의 본문은 구현에 대해 이야기하지만 제목은 어떤 언어가 더 빠른지 묻습니다. 언어는 어떻게 속도 속성을 가질 수 있습니까? 질문 제목을 편집하여 질문 본문을 반영해야합니다.
milancurcic

@ IRO-bot, 나는 고쳤다. 괜찮아 보이면 알려주십시오.
Ondřej Čertík 2016 년

1
실제로 "Fortran 컴파일러가 실제로 얼마나 우수합니까?"에 대한 결론 해당 스레드에서 정확하지 않습니다. 나는 GCC, PGI, CRAY 및 Intel 컴파일러를 사용하여 Cray에서 벤치 마크를 시도했으며 3 개의 컴파일러를 사용하여 Fortran이 C보다 빠릅니다 (b / w 5-40 %). Cray 컴파일러는 가장 빠른 Fortran / C 코드를 생성했지만 Fortran 코드는 40 % 더 빠릅니다. 시간이되면 자세한 결과를 게시 할 것입니다. Cray 머신에 액세스 할 수있는 모든 사용자는 벤치 마크를 확인할 수 있습니다. 4-5 개의 컴파일러를 사용할 수 있고 ftn / cc 래퍼가 관련 플래그를 자동으로 적용하기 때문에 좋은 플랫폼입니다.
stali

또한 Opteron 시스템에서 pgf95 / pgcc (11.10)로 검사되었습니다. # 1과 # 2가 가장 빠르며 (~ 20 %만큼 ifort보다 빠름) # 6, # 8, # 7 (순서대로)입니다. pgf95는 모든 포트란 코드에 대해 ifort보다 빠르며, icpc는 모든 C에 대해 pgcpp보다 빠릅니다. 필자는 일반적으로 동일한 AMD 시스템에서도 ifort가 더 빠릅니다.
laxxy

답변:


12

우선,이 질문 / 도전을 게시 해 주셔서 감사합니다! 면책 조항으로, 나는 약간의 포트란 경험을 가진 네이티브 C 프로그래머이며, C에서 가장 집처럼 느끼기 때문에 C 버전 개선에만 중점을 둘 것입니다. 나는 모든 포트란 해킹도 초대했습니다!

다만이 약이 무엇인지에 대한 이민자를 생각 나게 :의 기본 전제 스레드가 GCC / 포트란 및 ICC / ifort가 각각 같은 백엔드이 있기 때문에, 같은 (의미와 동일) 프로그램에 해당하는 코드를 생성한다 상관없이했다 그것의 C 또는 포트란에있는. 결과의 품질은 각 구현의 품질에만 의존합니다.

gcc4.6.1 및 다음 컴파일러 플래그를 사용하여 코드와 컴퓨터 (ThinkPad 201x, Intel Core i5 M560, 2.67 GHz)에서 약간의 장난을 쳤습니다 .

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

또한 C ++ 코드의 SIMD 벡터화 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벡터에 의해 생성 된 어셈블러를 살펴 보았으며 여기에 큰 놀라움이 있습니다. 나는 적어도 내 아키텍처에서 대역폭 제한 문제가 조금 느리다는 사실에 기인한다. 각 행렬 곱셈에 대해 230MB의 데이터가 순회되며, 이는 모든 수준의 캐시를 상당히 늪으로 만듭니다. 예를 들어 더 작은 입력 값을 사용 100하면 성능 차이가 상당히 커집니다.

부수적으로 벡터화, 정렬 및 컴파일러 플래그에 집착하는 대신 가장 정확한 최적화는 ~ 8 자리의 결과를 가질 때까지 단 정밀도 산술의 처음 몇 번의 반복을 계산하는 것입니다. 단 정밀도 명령어는 더 빠를뿐만 아니라 이동해야하는 메모리 양도 절반으로 줄어 듭니다.


시간 내 주셔서 감사합니다! 답장하기를 바랐습니다. :) 그래서 먼저 플래그를 사용하도록 Makefile을 업데이트했습니다. 그런 다음 C 코드를 spectral_norm8.c로 넣고 README를 업데이트했습니다. 내 컴퓨터 ( github.com/certik/spectral_norm/wiki/Timings ) 의 타이밍을 업데이트했으며 알 수 있듯이 컴파일러 플래그가 내 컴퓨터에서 C 버전을 더 빨리 만들지 못했지만 (예 : gfortran이 여전히 승리) SIMD가 벡터화되었습니다. 버전은 gfortran을 친다.
Ondřej Čertík

@ OndřejČertík : 그냥 호기심, 어떤 버전 gcc/ gfortran당신이 사용하고 있습니까? 이전 스레드에서는 버전이 다르면 결과가 크게 다릅니다.
페드로

4.6.1-9 우분투 3을 사용합니다. 인텔 컴파일러에 액세스 할 수 있습니까? gfortran에 대한 나의 경험은 때로는 최적의 코드를 생성하지 못한다는 것입니다. IFort는 보통 그렇습니다.
Ondřej Čertík

1
@ OndřejČertík : 이제 결과가 더 의미가 있습니다! matmul2Fortran 버전에서 의미 적으로 matmul3C 버전 과 동일 하다는 것을 간과했습니다 . 두 버전은 정말 지금 동일하므로 gcc/ gfortran 해야 모두에 대해 동일한 결과를, 예를 들면 아무도 프런트 엔드 / 언어는 더 다른 것보다이 경우입니다. gcc우리가 선택해야하는 벡터화 된 명령어를 이용할 수 있다는 장점이 있습니다.
페드로

1
@ cjordan1 : vector_size코드를 플랫폼 독립적으로 만들기 위해 (예 :이 구문을 gcc사용하여) IBM Power 아키텍처에서 AltiVec을 사용하는 다른 플랫폼에 대해 벡터화 된 코드를 생성 할 수 있도록 속성 을 사용하기로 선택했습니다 .
페드로

7

user389의 답변이 삭제되었지만 필자가 자신의 진영에 있다고 말하겠습니다. 다른 언어로 된 마이크로 벤치 마크를 비교하여 우리가 배우는 내용을 볼 수 없습니다. C와 Fortran이 얼마나 짧은 지에 대해이 벤치 마크에서 거의 동일한 성능을 얻는다는 것은 놀라운 일이 아닙니다. 그러나 벤치 마크는 수십 줄로 두 언어로 쉽게 작성할 수 있기 때문에 지루합니다. 소프트웨어의 관점에서 볼 때 이는 대표적인 사례가 아닙니다. 10,000 또는 100,000 줄의 코드가있는 소프트웨어와 컴파일러가 그 기능을 수행하는 방식에주의해야합니다. 물론, 그 규모에서 다른 언어를 빨리 찾을 수 있습니다. 언어 A에는 10,000 줄이 필요하지만 언어 B에는 50,000이 필요합니다. 또는 당신이하고 싶은 것에 따라 다른 방법. 그리고 갑자기

다시 말해, Fortran 77에서 개발 한 응용 프로그램이 50 % 더 빠를 수 있다는 점은 중요하지 않습니다. 대신 3 개월 걸리지 만 올바르게 실행하려면 1 개월이 걸립니다. F77에서. 여기서 질문의 문제는 실제로 내 견해와 관련이없는 측면 (개별 커널)에 초점을 맞추고 있다는 것입니다.


동의했다. 매우 사소한 편집 (-3 자, +9 자)을 제외하고는 그 가치에 대해 그의 대답에 대한 주된 감정에 동의했습니다. 내가 아는 한, C ++ / C / Fortran 컴파일러 토론은 성능 향상을 위해 가능한 모든 방법을 다 써 버린 경우에만 문제가되므로 99.9 %의 사람들에게는 이러한 비교가 중요하지 않습니다. 나는 토론이 특히 밝아지는 것을 발견하지 못했지만 성능상의 이유로 C 및 C ++ 대신 Fortran을 선택한다고 증명할 수있는 사이트에서 적어도 한 사람을 알고 있으므로 완전히 쓸모 가 없다고 말할 수 없습니다 .
Geoff Oxberry

4
나는 당신의 요점에 동의,하지만 난 아직 거기로이 논의가 유용하다고 생각 되어 여전히 어떻게 든 믿고 거기 사람들의 수가 다른 것보다 하나 개의 언어를 "빨리"만드는 마법이 동일한 컴파일러의 사용에도 불구하고, 백엔드. 나는 주로이 신화를 풀기 위해이 토론에 공헌한다. 방법론에 관해서는 "대표적 사례"가 없으며, 매트릭스 벡터 곱셈처럼 단순한 것을 취하는 것이 컴파일러에게 그들이 할 수있는 것을 보여줄 수있는 충분한 공간을 제공하기 때문에 좋은 것입니다.
페드로

@GeoffOxberry : 물론, 분명하고 합리적인 원인으로 다른 언어가 아닌 다른 언어를 사용하는 사람들을 항상 찾을 수 있습니다. 그러나 내 질문은 구조화되지 않은 적응 형 유한 요소 메쉬에 나타나는 데이터 구조를 사용하는 경우 Fortran이 얼마나 빠를 지에 대한 것입니다. Fortran에서 구현하기가 어려울 것이라는 사실 외에도 (C ++에서 이것을 구현하는 사람은 STL을 많이 사용합니다.) Fortran은 루프가 많지 않고 많은 간접 지시 사항이 있고 많은 ifs가없는 이러한 종류의 코드에서 실제로 더 빠를까요?
Wolfgang Bangerth

@WolfgangBangerth :처럼 내가 처음으로 코멘트했다, 난 그렇게 묻는 당신과 함께하고 user389 (조나단 더시)에 동의 질문은 무의미입니다. 즉, C ++ / C / Fortran 중에서 언어 선택이 응용 프로그램의 성능에 중요하다고 생각하는 사람 귀하의 질문에 대답 하도록 초대 할 것입니다 . 안타깝게도 컴파일러 버전에 대해서는 이런 종류의 토론이있을 것으로 생각됩니다.
Geoff Oxberry

@GeoffOxberry : 예, 내가 분명히 있음을 의미하지 않았다 당신이 그 질문에 대답 할 필요가 있었다.
Wolfgang Bangerth

5

시스템의 gfortran 컴파일러로 컴파일 된 Fortran 코드보다 Python 코드 (numpy를 사용하여 BLAS 작업 수행)를 더 빨리 작성할 수 있습니다.

$ 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, 매우 가볍게 수정 된 스펙트럼 _norm6.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
뺨에 혀가 있다고 생각합니까?
Robert Harvey

-1 질문에 대답하지 않았지만 이미 알고 있습니다.
Pedro

흥미롭게도, 어떤 버전의 gfortran을 사용했으며, 저장소에서 사용 가능한 C 코드를 Pedro의 플래그로 테스트 했습니까?
Aron Ahmadia

1
사실, 나는 당신이 냉소적이지 않다고 가정하면 더 분명하다고 생각합니다.
Robert Harvey

1
이 글과 다른 질문이나 글은 아론이 자신의 의견을 더 잘 일치시키는 방식으로 편집하고 있기 때문에, 모든 요점은 모든 글에 정확히 "이 결과는 의미가 없습니다" 라는 레이블이 붙어 있어야합니다. 경고, 그냥 삭제하는 중입니다.

3

이것을 Intel 컴파일러로 확인했습니다. 11.1 (빠르고 -O3을 의미)과 12.0 (-O2)을 사용하면 가장 빠른 것은 1,2,6,7, 8입니다 (즉, "가장 간단한"Fortran 및 C 코드, 손으로 벡터화 된 C) -이들은 ~ 1.5 초에서 서로 구별 할 수 없습니다. 테스트 3과 5 (배열을 함수로 사용)는 속도가 느립니다. # 4 컴파일 할 수 없었습니다.

특히, -O2가 아닌 12.0 및 -O3으로 컴파일하면 첫 번째 2 ( "가장 간단한") 포트란 코드가 ALOT (1.5-> 10.2 초)을 느리게합니다. 그러나 이것은 가장 극적인 예일 수 있습니다. 현재 릴리스에서도 여전히 문제가 발생한다면,이 간단한 경우에 최적화에 문제가있는 것이 분명하기 때문에 인텔에보고하는 것이 좋습니다.

그렇지 않으면 나는 이것이 유익한 연습이 아니라는 Jonathan에 동의합니다. :)


확인해 주셔서 감사합니다! 이것은 어떤 이유로 matmul 작업이 느리기 때문에 gfortran이 아직 완전히 성숙하지 않은 내 경험을 확인합니다. 따라서 결론은 단순히 matmul을 사용하고 Fortran 코드를 단순하게 유지하는 것입니다.
Ondřej Čertík

반면에, gfortran은 모든 matmul () 호출을 BLAS 호출로 자동 변환하는 명령 행 옵션을 가지고 있다고 생각합니다 (아마도 dot_product ()는 확실하지 않습니다). 그래도 시도하지 않았습니다.
laxxy
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.