スペクトルノルムシュートアウト(gcc、intel、その他のコンパイラーを使用)でCはFortranよりも遅いですか?


13

ここでの結論:

Fortranコンパイラは本当にどれほど優れていますか?

gfortranとgccは単純なコードと同じくらい速いということです。そこで、もっと複雑なものを試してみたかったのです。スペクトル標準の銃撃戦の例を取り上げました。最初に2DマトリックスA(:, :)を事前に計算してから、ノルムを計算します。(この解決策は銃撃戦では許可されていません。)FortranとCバージョンを実装しました。コードは次のとおりです。

https://github.com/certik/spectral_norm

最速のgfortranバージョンはspectrum_norm2.f90とspectrum_norm6.f90です(一方はFortranの組み込みmatmulとdot_productを使用し、他方はコードにこれら2つの関数を実装します-速度に違いはありません)。私が記述できた最速のC / C ++コードはspectrum_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を比較するシュートアウトページとは異なります。)

更新:バージョン179dae2を使用すると、Cバージョンのmatmul3()が改善され、同じように高速になりました。

$ 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

以下のPedroのベクトル化バージョンはより高速です。

$ time ./spectral_norm8 5500
1.274224153

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

最後に、インテルコンパイラーについてlaxxyが以下に報告しているように、そこには大きな違いはないようで、最も単純なFortranコード(spectral_norm1)でさえ最速です。


5
私は今のところコンパイラの近くにはいませんが、あなたの配列にrestrictキーワードを追加することを検討してください。ポインタのエイリアスは、通常、FortranとCの関数呼び出しの違いです。また、Fortranは列優先でメモリを、行優先でCを保存します。
-moyner

1
-1この質問の本文では実装について説明していますが、タイトルではどの言語が速いかを尋ねていますか?言語はどのように速度の属性を持つことができますか?質問の本文を反映するように、質問のタイトルを編集する必要があります。
ミラノ語

@ IRO-bot、修正しました。よろしければ、教えてください。
オンデジチェルティク

1
実際、「Fortranコンパイラーはどれほど優れているのか」という結論。そのスレッドではまったく正しくありません。GCC、PGI、CRAY、およびIntelコンパイラを使用したCrayでベンチマークを試しましたが、3つのコンパイラを使用した場合、FortranはCよりも高速でした(b / w 5-40%)。Crayコンパイラーは最速のFortran / Cコードを生成しましたが、Fortranコードは40%高速でした。時間があるときに詳細な結果を投稿します。ところで、Crayマシンにアクセスできる人なら誰でもベンチマークを検証できます。4〜5個のコンパイラが使用可能で、関連するフラグがftn / ccラッパーによって自動的に使用されるため、これは優れたプラットフォームです。
12

また、Opteronシステムでpgf95 / pgcc(11.10)を使用してチェックしました。#1と#2が最速(ifortよりも20%速い)、次に#6、#8、#7(この順序で)です。pgf95はすべてのfortranコードでifortよりも高速であり、icpcはすべてのCでpgcppよりも高速でした-私の場合は、同じAMDシステムであっても、通常ifortが高速であることに注意してください。
-laxxy

回答:


12

まず第一に、この質問/チャレンジを投稿してくれてありがとう!免責事項として、私はFortranの経験があるネイティブCプログラマーであり、Cで最も家にいるように感じるため、Cバージョンの改善にのみ焦点を当てます。すべてのFortranハックにも招待してください!

初心者にこれが何であるかを思い出させるために:このスレッドの基本的な前提は、gcc / fortranとicc / ifortは、それぞれ同じバックエンドを持っているため、同じ(意味的に同一の)プログラムに対して同等のコードを生成するということでしたCまたはFortranである。結果の品質は、それぞれの実装の品質のみに依存します。

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

私も先に進み、SIMDでベクトル化された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;
}

3つのバージョンはすべて、同じフラグと同じバージョンでコンパイルされました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コードをspectrum_norm8.cとして配置し、READMEを更新しました。私のマシン(github.com/certik/spectral_norm/wiki/Timings)のタイミングを更新しました。ご覧のように、コンパイラフラグは私のマシンでCバージョンを速くしませんでした(つまり、gfortranがまだ勝ちました)バージョンはgfortranに勝っています。
オンデジチェルティク

@OndřejČertík:好奇心から、どのバージョンのgcc/ gfortranを使用していますか?以前のスレッドでは、バージョンが異なると結果が大きく異なりました。
ペドロ

4.6.1-9ubuntu3を使用します。インテル®コンパイラーにアクセスできますか?gfortranでの私の経験では、最適なコードを(まだ)生成しない場合があります。IFortは通常そうします。
オンドレジ・セティク

1
@OndřejČertík:結果はもっと意味があります!matmul2Fortranバージョンはmatmul3、私のCバージョンと意味的に同等であることを見落としていました。2つのバージョンは実際には同じであるため、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で開発した場合、アプリケーションが正しく実行されるまでに1か月しかかからず、3か月かかる場合でも、アプリケーションが50%速くなる可能性があることは重要ではありません。 F77で。ここでの質問の問題は、私の見解では実際には関係のない側面(個々のカーネル)に焦点を合わせていることです。


同意した。価値のあることについては、非常に小さな編集(-3文字、+ 9文字)は別として、私は彼の答えの主な感情に同意しました。私の知る限り、C ++ / C / Fortranコンパイラーの議論は、パフォーマンス向上のために他のあらゆる手段を使い果たした場合にのみ問題になります。そのため、99.9%の人々にとって、これらの比較は重要ではありません。議論は特に啓発なものではありませんが、パフォーマンス上の理由でCおよびC ++よりもFortranを選択することを証明できる人がサイト上に少なくとも1人いることを知っています。そのため、まったく役に立たないとは言えません。
ジェフオックスベリー

4
私はあなたの主な点に同意します、同じコンパイラを使用しているにもかかわらず、ある言語を他の言語よりも「速く」する魔法があると信じている人たくさんいるので、この議論はまだ有用だと思いますバックエンド。主にこの神話を払拭しようとするこれらの議論に貢献しています。方法論に関しては、「代表的なケース」はありません。私の意見では、行列ベクトル乗算のような単純なものをとることは良いことです。コンパイラーに何ができるかを示す十分なスペースを与えるからです。
ペドロ

@GeoffOxberry:確かに、多かれ少なかれ明確に説明された原因のために、別の言語ではなく、ある言語を使用する人を常に見つけるでしょう。しかし、私の質問は、たとえば非構造化された適応有限要素メッシュに現れるデータ構造を使用する場合、Fortranがどれほど高速になるかということです。これはFortranで実装するのが面倒だという事実(C ++でこれを実装する人はだれでもSTLを大いに使用します)は別として、Fortranはタイトなループ、多くのインダイレクション、多くのifを持たないこの種のコードでは本当に高速ですか?
ウルフガングバンガース

@WolfgangBangerth:最初のコメントで言ったように、私はあなたとuser389(Jonathan Dursi)に同意するので、質問は無意味私に尋ねます。それは私が誰招く、と述べ(C ++ / C / Fortranの間)言語の選択は、あなたの質問に答えるために自分のアプリケーションでのパフォーマンスのために重要であると考えています。悲しいことに、コンパイラーのバージョンについては、この種の議論があり得ると思います。
ジェフオックスベリー

@GeoffOxberry:はい。もちろん、あなたがその質問に答える必要があるという意味ではありませでした。
ヴォルフガングバンガース

5

私のシステムのgfortranコンパイラーでコンパイルされたFortranコードよりも高速に(numpyを使用してBLAS操作を行う)Pythonコードを作成できることがわかりました。

$ 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、非常にわずかに変更されたspectrum_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
頬の舌、私は推測する?
ロバートハーベイ

-1質問に答えなかったが、すでに知っていると思う。
ペドロ

興味深いことに、どのバージョンのgfortranを使用し、Pedroのフラグを使用してリポジトリで使用可能なCコードをテストしましたか?
アロンアフマディア

1
実際、あなたが皮肉ではなかったと仮定すれば、今はもっとはっきりしていると思います。
ロバートハーベイ

1
この記事、および他の質問や記事のどれもあるので、私の全体のポイントは、ということであるにも関わらず、彼の意見を一致させるよりよいに、このような方法で、アロンによって編集されているすべての記事がで標識する必要があります正確に「これらの結果は意味がない」など警告、私はそれを削除しています。

3

Intelコンパイラでこれを確認しました。11.1(-fast、暗黙の-O3)、および12.0(-O2)では、最速のものは1,2,6,7、および8です(つまり、「最も簡単な」FortranおよびCコード、および手でベクトル化されたC) -これらは、〜1.5秒で互いに区別できません。テスト3および5(関数として配列を使用)は遅くなります。#4コンパイルできませんでした。

かなり注目すべきは、-O2ではなく12.0と-O3でコンパイルする場合、最初の2(「最も単純な」)FortranコードはA LOTを遅くします(1.5-> 10.2秒)。これは最も劇的な例かもしれません。現在のリリースでもこれが当てはまる場合は、Intelに報告することをお勧めします。この単純なケースでは最適化に何か問題があることが明らかです。

そうでなければ、これは特に有益な演習ではないことにジョナサンに同意します:)


それをチェックしてくれてありがとう!これは、何らかの理由でmatmul操作が遅いため、gfortranがまだ完全に成熟していないという私の経験を裏付けています。したがって、私にとっての結論は、単純にmatmulを使用し、Fortranコードをシンプルに保つことです。
オンドレジ・セティク

一方、gfortranにはすべてのmatmul()呼び出しを自動的にBLAS呼び出しに変換するコマンドラインオプションがあると思います(おそらくdot_product()でもわかりません)。しかし、それを試したことはありません。
-laxxy
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.