在频谱规范枪战中(使用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,另一个使用代码实现这两个功能-速度没有差异)。我能够编写的最快的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
我现在不在编译器附近,但可以考虑将strict关键字添加到数组中。指针的别名通常是数组上的Fortran和C函数调用之间的区别。同样,Fortran以列优先顺序存储内存,以行优先顺序存储C。
moyner

1
-1这个问题的内容谈论实现,但是标题询问哪种语言更快?语言如何具有速度属性?您应该编辑问题标题,以使其反映问题的正文。
milancurcic 2012年

@ IRO-bot,我修复了它。让我知道您是否满意。
昂德里杰·塞蒂克

1
实际上,有关“ Fortran编译器到底有多好?”的结论。在那个线程中不是很正确。我曾在使用GCC,PGI,CRAY和Intel编译器的Cray上尝试过基准测试,并使用3种编译器,Fortran的速度比C快(黑白5-40%)。Cray编译器生成了最快的Fortran / C代码,但是Fortran代码的速度提高了40%。有空的时候我会发布详细的结果。顺便说一句,任何有权使用Cray机器的人都可以验证基准。这是一个很好的平台,因为可以使用4-5个编译器,并且ftn / cc包装程序会自动使用相关标志。
stali 2012年

在Opteron系统上也用pgf95 / pgcc(11.10)进行了检查:#1和#2是最快的(比ifort快20%),然后是#6,#8,#7(按此顺序)。对于所有的fortran代码,pgf95的速度都比ifort快,而所有C语言的icpc都比pgcpp快。
laxxy 2012年

Answers:


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

我还继续写了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代码作为Spectrum_norm8.c并更新了README。我更新了机器上的计时(github.com/certik/spectral_norm/wiki/Timings),如您所见,编译器标志并没有使我的机器上的C版本更快(即gfortran仍获胜),但您的SIMD向量化了版本胜过gfortran。
昂德里杰·塞蒂克

@OndřejČertík:出于好奇,您使用的是哪个gcc/ 版本gfortran?在先前的线程中,不同的版本给出了明显不同的结果。
佩德罗(Pedro)

我使用4.6.1-9ubuntu3。您可以使用英特尔编译器吗?我在gfortran上的经验是,有时(尚未)产生最佳代码。IFort通常这样做。
昂德里杰·塞蒂克

1
@OndřejČertík:现在的结果更有意义了!我忽略matmul2了Fortran版本在语义上等同matmul3于C版本。现在这两个版本实际上是相同的,因此gcc/ gfortran 应当为两者产生相同的结果,例如,在这种情况下,没有一个前端/语言比另一个更好。gcc优点是我们可以选择使用矢量化指令。
2012年

1
@ cjordan1:我选择使用该vector_size属性是为了使代码独立于平台,即使用此语法,gcc应该能够为其他平台生成矢量化代码,例如在IBM Power体系结构上使用AltiVec。
佩德罗(Pedro)

7

user389的答案已删除,但我要声明自己已经牢牢扎在他的阵营中:我无法通过比较不同语言的微基准来了解我们所学到的东西。鉴于C和Fortran的性能有多么短,它在此基准上获得的性能几乎相同就不足为奇了。但是基准测试也很无聊,因为它可以轻松地用几十种语言用两种语言编写。从软件的角度来看,这不是典型的例子:我们应该关心具有10,000或100,000行代码的软件,以及编译器如何对此进行处理。当然,以这种规模,人们会很快发现其他事情:语言A需要10,000行,而语言B需要50,000行。或反过来,这取决于您想要做什么。突然之间

换句话说,对我来说没什么关系,如果我在Fortran 77中开发它,那么我的应用程序可能会快50%,相反,如果我只需要1个月就可以正常运行,而我则需要3个月,在F77中 在此问题的问题在于,它集中于我认为与实践不相关的方面(单个内核)。


同意 除了非常小的修改(-3个字符,+ 9个字符)外,我的观点还值得,我同意他的回答的主要观点。据我所知,C ++ / C / Fortran编译器辩论只有在已经用尽其他所有可能的性能提升途径时才有意义,这就是为什么对于99.9%的人来说,这些比较并不重要。我发现讨论没有特别启发性,但是我知道该站点上至少有一个人可以出于性能原因证明选择使用C和C ++而不是C和Fortran,这就是为什么我不能说它完全没有用。
Geoff Oxberry 2012年

4
我同意你的主要观点,但我仍然认为,因为这种讨论是很有用的一些人在那里谁仍然莫名其妙地相信有某种魔力,让一个语言“快”比其他的,尽管使用相同的编译器后端。我为这些辩论做出了贡献,主要是为了消除这个神话。至于方法论,没有“代表案例”,在我看来,采用矩阵向量乘法这样简单的方法是一件好事,因为它为编译器提供了足够的空间来显示他们可以做什么或不能做什么。
2012年

@GeoffOxberry:当然,您总会发现人们使用某种语言而不是另一种语言是出于某种程度的清晰和合理的原因。但是,我的问题是,如果要使用出现在非结构化自适应有限元网格中的数据结构,Fortran将会有多快。除了在Fortran中实现这很尴尬(在C ++中实现此功能的每个人都在很大程度上使用STL)这一事实之外,对于没有紧密循环,很多间接调用,很多ifs的这种代码,Fortran真的会更快吗?
Wolfgang Bangerth,2012年

@WolfgangBangerth:就像我在第一条评论中所说,我同意您和user389(Jonathan Dursi)的观点,所以问这个问题毫无意义。这就是说,我会邀请任何人谁相信语言的这种选择(除C ++ / C / Fortran语言)可以在他们的应用程序回答你的问题性能非常重要。可悲的是,我怀疑对于编译器版本是否会发生这种争论。
Geoff Oxberry 2012年

@GeoffOxberry:是的,显然我并不是说需要回答这个问题。
Wolfgang Bangerth,2012年

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,一个经过轻微修改的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未回答问题,但我想您已经知道了。
2012年

有趣的是,您使用了哪个版本的gfortran,并且是否使用Pedro的标志测试了存储库中可用的C代码?
阿隆·艾玛迪亚

1
实际上,假设您没有讽刺的话,我认为现在更加清楚。
罗伯特·哈维

1
由于这个职位,并没有其他的问题或岗位,正在被阿隆以这样的方式编辑,以更好地匹配他的意见,即使我的整个观点是,所有的职位应被标记正是这种“这些结果是毫无意义的”注意,我只是删除它。

3

使用英特尔编译器对此进行了检查。使用11.1(快速,表示-O3),使用12.0(-O2),最快的是1、2、6、7和8(即“最简单的” Fortran和C代码以及手动矢量化的C) -它们在约1.5秒内彼此无法区分。测试3和5(将数组作为函数)较慢;#4我无法编译。

值得注意的是,如果使用12.0和-O3而不是-O2进行编译,则前2个(“最简单”)的Fortran代码会减慢很多(1.5-> 10.2秒)。这不是我第一次看到类似的东西这,但这可能是最生动的例子。如果在当前版本中仍然如此,我认为将其报告给英特尔将是一个好主意,因为在这种相当简单的情况下,其优化显然存在一些问题。

否则,我同意乔纳森(Jonathan)的观点,这不是特别有用的练习:)


感谢您检查!这证实了我的经验,gfortran尚未完全成熟,因为由于某种原因,matmul操作速度很慢。因此,对我来说,结论是简单地使用matmul并使Fortran代码保持简单。
昂德里杰·塞蒂克

另一方面,我认为gfortran具有命令行选项,可以自动将所有matmul()调用转换为BLAS调用(不确定也可以是dot_product())。从来没有尝试过。
laxxy 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.