龙芯开源社区

 找回密码
 注册新用户(newuser)
查看: 11415|回复: 36

龙芯的性能调优(一)

[复制链接]
发表于 2008-10-19 17:39:51 | 显示全部楼层 |阅读模式
龙芯的性能调优

本文来自中科院计算所的工作。
经过一段时间的调优,总结龙芯的一些特征如下:
性能n32>n64>o32,前两者需mips64el,遗憾的是世伟兄和张乐兄都只build了mipsel的系统。我们人手有限,非常希望能有人帮我们维护mips64el的工具链的最新版(gcc,g++,glibc,gfortran,g77)。此外比如atlas库等都对很多程序的性能有重大影响。
原因如下:o32受abi限制,指令集版本低,只使用64位寄存器的32位。
n64(或称64)使用了64位指针,因为一般32位指针就已够用,所以很多时候占用了宝贵的片外带宽(2e/2f的内存都在片外,所以使用memory与cache间带宽),因此性能反而差于n32。在对SPEC CPU2000的调优中发现,n32总体上性能最好。

指令选择中,有两点需考虑,一是latency(延迟),即指令发射(issue)后多久能拿到结果;二是throughput(吞吐率),即每几个cycle能发射该条指令。latency长和throughput低都造成瓶颈。各指令中,访存指令(特别是load)在cache miss(缓存失效)时因为需到主存取数,所以常有几百个周期的latency,严重降低性能。throughput低的指令如非流水设计的sqrt。

1. 龙芯2e/2f中,访存指令(包括相关的cache(缓存)命中率)对性能影响极大。如对art指令作结构体分裂而改善空间局部性(Spatial locality,见http://en.wikipedia.org/wiki/Locality_of_reference )后,能在2e上提升性能80%,2f上20%。
2. 龙芯特有乘法(mult.g)替换mips原有乘法的效果远低于预期。虽然就单条指令来看,可能改善throughput,但是用到SPEC中
只有约1%的提升。这是因为乘法指令的latency与throughput与访存指令相比都要好的多,不构成瓶颈。
 楼主| 发表于 2008-10-19 18:50:09 | 显示全部楼层
性能调优很关键的是测试集的选择。版上使用的一些测试集常是自已针对一些指令编写。这样有一些问题,一个是这样的代码是否会在实际使用中出现,二是对于简单的代码,编译器往往能进行非常复杂的变换,从而使结果受优化严重影响。三是性能与全系统有关,有时涉及到系统调用的效率问题
比如说可能利用这样的代码来测试向量点积的速度:
#include<stdio.h>
#define N 50000000
double a[N];
double b[N];
main()
{
    int i;
    double s;
    s=0;
    for(i=0;i<N;i++){
        a=i;
        b=i;
    }
    for(i=0;i<N;i++)
        s+=a*b;
    printf("%f\n",s);
    return 0;
}
但是这里初始化a和b数组的时间实际上不可忽略。第二个循环虽然有乘法,看起来所花时间应较长。但实际上乘法被流水后也是很快的。所以这样的程序用time ./a.out就不能准确测量点积的时间。
如果不初始化,由于copy-on-write机制,会得到完全错误的信息!

[ 本帖最后由 matman 于 2010-1-20 13:13 编辑 ]
 楼主| 发表于 2008-10-19 18:50:39 | 显示全部楼层
又比如库函数里的memset是如下代码:
10008330 <memset>:
10008330:       28c90010        slti    a5,a2,16
10008334:       1520001d        bnez    a5,100083ac <memset+0x7c>
10008338:       0080102d        move    v0,a0
1000833c:       10a00007        beqz    a1,1000835c <memset+0x2c>
10008340:       30a500ff        andi    a1,a1,0xff
10008344:       00054238        dsll    a4,a1,0x8
10008348:       00a82825        or      a1,a1,a4
1000834c:       00054438        dsll    a4,a1,0x10
10008350:       00a82825        or      a1,a1,a4
10008354:       0005403c        dsll32  a4,a1,0x0
10008358:       00a82825        or      a1,a1,a4
1000835c:       00044022        neg     a4,a0
10008360:       31080007        andi    a4,a4,0x7
10008364:       11000003        beqz    a4,10008374 <memset+0x44>
10008368:       00c83022        sub     a2,a2,a4
1000836c:       b4850000        sdr     a1,0(a0)
10008370:       00882020        add     a0,a0,a4
10008374:       30c8000f        andi    a4,a2,0xf
10008378:       11060007        beq     a4,a2,10008398 <memset+0x68>
1000837c:       00c83822        sub     a3,a2,a4
10008380:       00e43820        add     a3,a3,a0
10008384:       0100302d        move    a2,a4
10008388:       20840010        addi    a0,a0,16
1000838c:       fc85fff0        sd      a1,-16(a0)
10008390:       1487fffd        bne     a0,a3,10008388 <memset+0x58>
10008394:       fc85fff8        sd      a1,-8(a0)
10008398:       30c80008        andi    a4,a2,0x8
1000839c:       11000003        beqz    a4,100083ac <memset+0x7c>
100083a0:       00c83022        sub     a2,a2,a4
100083a4:       fc850000        sd      a1,0(a0)
100083a8:       20840008        addi    a0,a0,8
100083ac:       18c00004        blez    a2,100083c0 <memset+0x90>
100083b0:       00c43820        add     a3,a2,a0
100083b4:       20840001        addi    a0,a0,1
100083b8:       1487fffe        bne     a0,a3,100083b4 <memset+0x84>
100083bc:       a085ffff        sb      a1,-1(a0)
100083c0:       03e00008        jr      ra

可以发现热循环是
10008388:       20840010        addi    a0,a0,16
1000838c:       fc85fff0        sd      a1,-16(a0)
10008390:       1487fffd        bne     a0,a3,10008388 <memset+0x58>
10008394:       fc85fff8        sd      a1,-8(a0)
这里每循环一次只执行2条有用的指令,一次只存16个字节,对于龙芯这样的超标量cpu来说太少了
可以循环展开为一次存64个指令,性能将有较大提高。
 楼主| 发表于 2008-10-19 18:53:15 | 显示全部楼层
一个性能较好的针对64位龙芯的memset源代码如下
#include <linux/types.h>

#define op_t unsigned long long
#define OPSIZ (sizeof(op_t))

typedef unsigned char byte;

void *memset(void *dstpp, char c, size_t len)
{
    long int dstp = (long int) dstpp;

    if (len >= 8) {
            size_t xlen;
            op_t cccc;

            cccc = (unsigned char) c;
            cccc |= cccc << 8;
            cccc |= cccc << 16;
            cccc |= cccc << 32;

            /* There are at least some bytes to set.
               No need to test for LEN == 0 in this alignment loop.  */
            while (dstp % OPSIZ != 0) {
                ((byte *) dstp)[0] = c;
                dstp += 1;
                len -= 1;
            }

            /* Write 8 `op_t' per iteration until less
             * than 8 `op_t' remain.
             */
            xlen = len / (OPSIZ * 8);
            while (xlen > 0) {
                ((op_t *) dstp)[0] = cccc;
                ((op_t *) dstp)[1] = cccc;
                ((op_t *) dstp)[2] = cccc;
                ((op_t *) dstp)[3] = cccc;
                ((op_t *) dstp)[4] = cccc;
                ((op_t *) dstp)[5] = cccc;
                ((op_t *) dstp)[6] = cccc;
                ((op_t *) dstp)[7] = cccc;
                dstp += 8 * OPSIZ;
                xlen -= 1;
            }
            len %= OPSIZ * 8;

            /* Write 1 `op_t' per iteration until less than
             * OPSIZ bytes remain.
             */
            xlen = len / OPSIZ;
            while (xlen > 0) {
                ((op_t *) dstp)[0] = cccc;
                dstp += OPSIZ;
                xlen -= 1;
            }
            len %= OPSIZ;
    }

    /* Write the last few bytes.  */
    while (len > 0) {
        ((byte *) dstp)[0] = c;
        dstp += 1;
        len -= 1;
    }

    return dstpp;
}
发表于 2008-10-19 19:26:56 | 显示全部楼层
拜读一下,希望性能会优化的越来越好  
发表于 2008-10-19 19:34:42 | 显示全部楼层
原帖由 matman 于 2008-10-19 18:50 发表
又比如库函数里的memset是如下代码:
10008330 :
10008330:       28c90010        slti    a5,a2,16
10008334:       1520001d        bnez    a5,100083ac
10008338:       0080102d        move    v0,a0
...
这里每循环一次只执行2条有用的指令,一次只存16个字节,对于龙芯这样的超标量cpu来说太少了
可以循环展开为一次存64个指令,性能将有较大提高。


呵呵  看老兄辛苦  俺也说一下自己的看法  原函数有个很重要的特性  就是函数不会破坏 t0~t9以及s0~s7  如果循环展开的话  破坏将不可避免  而破坏寄存器内容通常就隐含着要增加 sd/ld指令对

俺使用的就是这种编程风格(与库函数的设计者倒是不谋而合啊)  叶子函数尽量只使用a0~aX寄存器  这对主调用函数会有很大的帮助
 楼主| 发表于 2008-10-19 19:36:08 | 显示全部楼层
兄台的频率调节模块非常牛啊,我们还不懂这个事儿。
现在2F的一个问题是由于使用了DDR2内存,带宽有改善,但是clock latency增加了。不知道兄弟有没有研究过换用更高频率,如DDR2 1066MHz是否会有帮助。
 楼主| 发表于 2008-10-19 19:41:10 | 显示全部楼层

回复 6# 的帖子

与memcpy不同,memset 中unroll不占用更多寄存器,这是它的特殊性。
对于memcpy,可以使用浮点寄存器。一大堆caller-save的可以用。
浮点部件有一个访存端口,可以和整点部件并行执行。
ldc1 $f0,0($a0)
ldc1 $f1,8($a0)
....
sdc1 $f0,0($a0)
sdc1 $f1,8($a0)
....
不过因为memcpy原来就展开了不少了,再多展开没测到什么效果。

[ 本帖最后由 matman 于 2008-10-20 08:25 编辑 ]
发表于 2008-10-19 19:50:22 | 显示全部楼层
原帖由 matman 于 2008-10-19 19:41 发表
与memcpy不同,memset 中unroll不占用更多寄存器,这是它的特殊性。
对于memcpy,可以使用浮点寄存器。一大堆callee-save的可以用。
浮点部件有一个访存端口,可以和整点部件并行执行。
ldc1 $f0,0($a0)
ldc1 $f ...


呵呵  这倒也是  特例总是存在

另 memcpy如果用浮点寄存器的话  是不是要额外花费格式转换与数据传送的时间啊 dmfc1貌似延迟比较大

[ 本帖最后由 water 于 2008-10-19 19:52 编辑 ]
 楼主| 发表于 2008-10-19 19:57:49 | 显示全部楼层
浮点寄存器在这里只是放一下值,不用作任何定点浮点转换。ldc1直接就把数取到了寄存器里,用不着dmfc1
换句话说
ld $t4,0($a0)
sd $t4,0($a1)

ldc1 $f0,0($a0)
sdc1 $f0,0($a1)
完成的功能是完全一样的,都是把一个double work从a0地址写到a1地址处。

本版积分规则

小黑屋|手机版|Archiver|Lemote Inc.  

GMT+8, 2019-9-21 18:50 , Processed in 0.194967 second(s), 18 queries .

快速回复 返回顶部 返回列表