x86 pslldq to Loongson psllq

x86 pslldq 指令逻辑左移字节为单位的数据,而转换成龙芯的MMI只能使用 dsll 和 dsrl 指令模拟实现,需要特别注意的是 dsll 和 dsrl 指令移动的数据是以位为单位的。

/* SSE: pslldq (bytes) */
#define _mm_psllq(_D, _d, _s, _s64, _tf)                    \
        "subu %["#_tf"], %["#_s64"], %["#_s"] \n\t"         \
        "dsrl %["#_tf"], %["#_d"l], %["#_tf"] \n\t"         \
        "dsll %["#_D"h], %["#_d"h], %["#_s"] \n\t"          \
        "dsll %["#_D"l], %["#_d"l], %["#_s"] \n\t"          \
        "or %["#_D"h], %["#_D"h], %["#_tf"] \n\t"
pslldq $4, %xmm0 => mm_psllq(d, d, s32, s64, t)

Over!

看龙芯3A的 dmtc1 指令有多慢!

龙芯2F和3A处理器都实现了与 x86 MMX 基本兼容的 SIMD,即 MMI,该 ASE 是在浮点部件中的实现的,并且复用了 64-bit 的浮点寄存器(FPR)。在使用 MMI 时不可避免的会使用到通用寄存器向浮点器移动数据的情况,那么 dmtc1 的效率如何呢?

GPR 向 FPR 移动数据的指令共有3种:
mtc1 : 从 GPR 向 FPR 移动 32-bit 的数据,64-bit 平台上目标 FPR 的高 32-bit 清 0。
mthc1 : 从 GPR (低 32-bit)向 FPR 的高 32-bit 移动 32-bit 的数据,目标 FPR 的低 32-bit 数据保留。
dmtc1 : 从 GPR 向 FPR 移动 64-bit 数据。

从上面的说明可以看出, dmtc1 的功能是可以使用 mtc1 与 mthc1 模拟实现的,那么我们就设计个实验程序来验证一下这两条方式的时间开销分别如何吧。
程序的逻辑大致如下:

for (i=0; i<100000000; i++) {
#if 0
    move $2, $3
    mtc1 $3, $f31
    dsra $3, 32
    mthc1 $3, $f31
    move $3, $2
    ....
    ....
#else
    dmtc1 $3, $f31
    dmtc1 $3, $f31
     ....
     ....
#endif
}

结果:
在 MIPS64 系统上,每个循环中做8次GPR2FPR的数据移动,其 dmtc1 实现时间大概为 0m4.463s,而 mtc1 与 mthc1 组合实现为 0m3.857s,后者如不做寄存器的保存恢复,开销仅为 0m1.791s。

Over!

Firefox 30.0a1 for 龙芯3初步评测

经过一段时间的 OdinMonkey for MIPS N32 的移植,现在 Firefox 30.0a1 终于可以在龙芯3A笔记本上跑起来了,OdinMonkey for MIPS N32 基于 OdinMonkey for MIPS O32,这过程中感谢 Branislav Rankov 的热心帮助。初步跑了一下 Octane 2.0 性能评测,得分为 1010,比 Firefox 17.0.1 增加了 405 分。下一步的工作是针对龙芯平台的特定优化。

617834d12f2eb9385d05ca44d7628535e4dd6fbf

虽然 JavaScript 的性能有所提升了,但部分页面滚动的响应仍然不是太好,经简单的分析发现瓶颈有可能在 Xorg 上。

Over!

GDB 使用自定义命令实现一次执行多个命令

在调试 JIT 的过程中,经常要输出一些信息再跳过断点,使用 GDB 的自定义命令可以使工作变得简单。

(gdb) define mynext
Type commands for definition of "mynext".
End with a line saying just "end".
>i r
>x/8x $sp
>set $pc = $pc + 4
>c
>end
(gdb) mynext

Over!

MIPS o32 ‘__sync_add_and_fetch_8’ for Loongson

编译 WebKitGtk 过程中,链接测试程序阶段报出 undefined symbol ‘__sync_add_and_fetch_8’,这是由于 ‘__sync_add_and_fetch_8’ 在 MIPS O32 平台上没有实现,但龙芯实际上是 64-bit 的 CPU,所以可以使用下面的实现:

#include <sys/regdef.h>
#include <sys/asm.h>
 
LEAF(__sync_add_and_fetch_8)
    .set push
    .set mips64r2
    .set noreorder
    dins t0, a2, 0, 32
    dins t0, a3, 32, 32
    sync
_retry:
    lld t1, 0(a0)
    daddu t1, t1, t0
    scd t1, 0(a0)
    beqz t1, _retry
    daddu t1, t1, t0
    sync
    dext v0, t1, 0, 32
    jr ra
    dext v1, t1, 32, 32
    .set pop
END(__sync_add_and_fetch_8)

Over!

MIPS mtc1 & mthc1 组合使用问题

在 Port SpiderMonkey MIPS N32 时,有个测试始终跑不过,调试定位到一个 double 类型的立即数装载到 FPR 时用了大概这样的一个指令序列:

li t0, 0x41dfffff
mthc1 t0, $f21
li t0, 0xf2400000
mtc1 t0, $f21

结果 $f21 就杯具的等于了 0x00000000f2400000

查找手册后发现 mtc1 指令将 GPR 的低32位移动到 FPR 的低32位后,FPR 的高32位的值是未定义的,龙芯3上实测值是 0,解决方法是先做 mtc1,再做 mthc1。

Over!

龙芯3的 128-bit 访存指令

龙芯3实现了两组 128-bit 的访存指令 gslq, gssq, gslqc1, gssqc1,分别用于加载、存储 128-bit 数据至通用寄存器和浮点寄存器。这两组指令都要求地址对齐到 16 字节,另外由于指令占用 lwc2, swc2 编码域,所以如果要使用需要启用 CP2。

gslq/gssq

gslq gpr0, gpr1, off(gpr2) // match: 0xc8000020, mask: 0xfc008020
gssq gpr0, gpr1, off(gpr2) // match: 0xe8000020, mask: 0xfc008020
 
gpr0 : 编码域 bit0-bit4,取值 0-32,高 64-bit
gpr1 : 编码域 bit16-bit20,取值 0-32,低 64-bit
off  : 编码域 bit6-bit14,取值 -256-255,实际偏移值需要左移 4 位,即 -4096-4080
gpr2 : 编码域 bit21-bit25,取值 0-32

gslqc1/gssqc1

gslqc1 fpr0, fpr1, off(gpr0) // match: 0xc8008020, mask: 0xfc008020
gssqc1 fpr0, fpr1, off(gpr0) // match: 0xe8008020, mask: 0xfc008020
 
fpr0 : 编码域 bit0-bit4,取值 0-32,高 64-bit
fpr1 : 编码域 bit16-bit20,取值 0-32,低 64-bit
off  : 编码域 bit6-bit14,取值 -256-255,实际偏移值需要左移 4 位,即 -4096-4080
gpr0 : 编码域 bit21-bit25,取值 0-32

Over!

MIPS N32/N64 ABI 调用约定

MIPS N32 ABI 是 MIPS N64 ABI 的一个变种,在保留 MIPS N64 ABI 的几乎所有特性的情况下,使用了 32-bit 的指针。理由是在一个 32-bit 地址空间足够使用的应用程序上使用 64-bit 的指针不会带来任何好处。虽然它也是 32-bit 指针的,但和 MIPS O32 ABI 的差异还是非常之大的,这也是为什么应该将它归类到 MIPS64 家族中的原因了。

下面我们具体分析一下 MIPS N32 ABI 与 MIPS N64 ABI 的调用约定,会将其中的差异标记出来。

基本数据类型

C类型 汇编名称 MIPS N32 数据宽度(字节) MIPS N64 数据宽度(字节)
char byte 1 1
short half 2 2
int word 4 4
long word 4 8
long long dword 8 8
float word 4 4
double dword 8 8

对齐要求
上述的数据类型,只有当自然对齐的情况下,才可以使用标准的 MIPS 访存指令直接处理。MIPS N32 与 N64 都要求栈对齐到 16 字节。

函数调用约定
常规参数传递
MIPS N32/N64 ABI 约定了多达 8 个通用寄存器(a0-a7)和 8 个双精度浮点寄存器($f12-$f19)用于传递前 8 个参数,其后的所有参数在栈中传递,并且在栈中没有空间保留给前 8 个参数(这一点不同与 MIPS O32 ABI)。所有的常规参数(寄存器、栈中)的都是占用 8 字节空间,寄存器中的参数会符号扩展,就像是加载到寄存器中。通常情况下,前 8 个参数具体是在通用寄存器还是浮点寄存器是由函数原型中的类型决定的,如下例:

void
func (int a, float b, double c, void *d)
{
    // a : GPR a0
    // b : FPR $f13
    // c : FPR $f14
    // d : GPR a3
}
 
func (0, 1.0, 2.1, NULL);

结构体参数传递
C语言允许使用结构体类型作为参数,为了和 MIPS 规则保持一致,被传递的结构体就成为了参数结构中的一部分,其内部结构布局和其通常的存储器映像是完全相同的。在C结构体内,字节或半字紧缩到单个字的存储器单元中,所以当我们通过寄存器来传递概念上属于驻留内存的结构时,我们不得不用数据装满寄存器以模仿存储器的数据排布。如下例:

// Little-Endian
 
struct Arg
{
    char a;
    short b;
    int c;
    double d;
    int e;
};
 
void
test (struct Arg a)
{
    // a, b, c : a0 (value: 0x0000006400018863)
    // d : $f13 (value: 0x4008cccccccccccd)
    // e : a2 (value: 0x000000000000ff00)
}
 
struct Arg a = { 'c', 1, 100, 3.1, 0xff00 };
test (a);

上面的例子中,a0 寄存器的值有些复杂,其中低 8-bit 对应于 a = 0x63,bit8 – bit15 用于对齐,其值无效,bit16 – bit31 对应于 b = 0x0001, bit32 – bit63 对应于 c = 0x00000064。

联合体参数传递
联合体中即有定点又有浮点数据类型时,以此联合体类型作为参数时,通过通用寄存器传递参数。如下例:

union T
{
    uint64_t u;
    double d;
};
 
void
func (int a, union T b)
{
    // a : GPR a0
    // b : GPR a1
}
 
union T ud;
ud.d = 3.1;
func (0xff00, ud);

可变参数传递
目标函数原型是可变参数的情况下,前 8 个参数中确定类型的部分(第一个参数必然是确定的)根据类型传递,其余的参数统一在通用寄存器中传递。如下例:

void
func0 (int a, ...)
{
    // a : GPR a0
    // b : GPR a1
    // c : GPR a2
    // d : GPR a3
}
 
func0 (0, 1, 1.0, NULL);
 
void
func1 (float a, ...)
{
    // a : FPR $f12
    // b : GPR a1
    // c : GPR a2
    // d : GPR a3
}
 
func1 (1.0, 0, 1, 1.0);

返回值传递
在返回基本数据类型的情况下,与常规参数传递一样,有专用的寄存器约定为传递返回值,整型在通用寄存器 v0 中返回,浮点在浮点寄存器 $f0 中返回。比较复杂的情况是返回结构体等导出数据类型,根据返回数据的长度可分为两种情况:
1. 返回数据长度小于等于 16 字节,又按结构体成员的数据类型分为三种情况:
a. 所有成员都是整数型(包含指针)或整数与浮点型混合,返回数据在 v0(低64-bit)和 v1(高64-bit)寄存器传递,寄存器中数据布局与传递结构体参数相同。
b. 所有成员都是浮点型且数量小于等于2,其实就4种组合:float/float, float/double, double/double, double/float,这种情况下使用 f0(第一个成员)和 f2(第二个成员) 寄存器返回。需要格外注意的是 float 类型成员独立占用一个浮点寄存器。
c. 所有成员都是浮点型且数量大于2,返回数据在 v0(低64-bit)和 v1(高64-bit)寄存器传递,寄存器中数据布局与传递结构体参数相同。(其实与情况a相同,独立出来为了引起注意。)
2. 返回数据长度大于 16 字节,则调用者使用 a0 传递一个返回结构体的指针(也就是说调用者分配空间),其原有参数依次后移传递,返回时通过 v0 寄存器返回传入的结构体指针。

// 1.a
struct RetVal
{
    char a;
    int b;
    float c;
};
 
struct RetVal
func (int a)
{
    // a : GPR a0
 
    // return:
    // a : GPR v0 (bit0 - bit7)
    // b : GPR v0 (bit32 - bit63)
    // c : GPR v1 (bit0 - bit31)
}
 
func (0xff);
 
// 1.b
struct RetVal
{
    float a;
    float b;
};
 
struct RetVal
func (int a)
{
    // a : GPR a0
 
    // return:
    // a : FPR f0
    // b : FPR f2
}
 
func (0xff);
 
// 1.c
struct RetVal
{
    float a;
    float b;
    float c;
    float d;
};
 
struct RetVal
func (int a)
{
    // a : GPR a0
 
    // return:
    // a : GPR v0 (bit0 - bit31)
    // b : GPR v0 (bit32 - bit63)
    // c : GPR v1 (bit0 - bit31)
    // d : GPR v1 (bit32 - bit63)
}
 
func (0xff);
 
// 2
struct RetVal
{
    char a;
    int b;
    float c;
    double d;
};
 
struct RetVal
func (int a)
{
    // AddressOf (RetVal) : GPR a0
    // a : GPR a1
 
    // return AddressOf (RetVal) : GPR v0
}
 
func (0xff);

Link: MIPS 寄存器使用约定

Over!

MIPS 使用 synci 指令刷新 Cache

MIPS 是一个缓存不透明的架构设计,通常设计存在两个 Cache,即 指令 Cache 与 数据 Cache。在装载完代码执行前(一般仅应用程序动态生成的代码需要,如 JIT)一定要刷新指令 Cache,确保 Cache 中指令与写入的一致。这个过程分两个步骤:1. 是将数据 Cache 中的数据写回 RAM。2. 将 RAM 中的数据装载到指令 Cache。

较早的实现依赖于 cache 指令,但 cache 指令是个特权指令,事实上并不是需要做这件事的程序都是特权程序,所以普通应用程序在 Linux 平台上可以调用系统调用 cacheflush,进入特权模式完整刷新操作。

上述方法虽然可行,但如果频繁的执行系统调用效率较低,所以在 MIPS r2 的 ISA 中增加了一条常规特权下的 synci 指令,专门用于做这件事。它的用法是:

synci offset(base)

要求对每个 Cache Line 执行一次 synci 操作。

例程

void
cache_flush (void *code, size_t size)
{
       long start, end, line_size, mask;
 
       __asm__ volatile (
               ".set  push                             \t\n"
               ".set  noreorder                        \t\n"
               "rdhwr %[line_size], $1                 \t\n"
               "addu  %[end], %[code], %[size]         \t\n"
               "subu  %[mask], $0, %[line_size]        \t\n"
               "and   %[end], %[end], %[mask]          \t\n"
               "and   %[start], %[code], %[mask]       \t\n"
               "addu  %[end], %[end], %[line_size]     \t\n"
               "1:                                     \t\n"
               "subu  %[end], %[end], %[line_size]     \t\n"
               "bne   %[start], %[end], 1b             \t\n"
               "synci 0(%[end])                        \t\n"
               ".set  pop                              \t\n"
               :[start]"=&r"(start), [end]"=&r"(end),
                [line_size]"=&r"(line_size), [mask]"=&r"(mask)
               :[code]"r"(code), [size]"r"(size)
       );
}

Over!