2015-07-22 49 views
4

这是一个简单的程序。 测试环境:Debian的8,去1.4.2golang:数组索引效率

union.go:

package main 

import "fmt" 

type A struct { 
    t int32 
    u int64 
} 

func test() (total int64) { 
    a := [...]A{{1, 100}, {2, 3}} 

    for i := 0; i < 5000000000; i++ { 
     p := &a[i%2] 
     total += p.u 
    } 
    return 
} 
func main() { 
    total := test() 
    fmt.Println(total) 
} 

union.c:

#include <stdio.h> 

struct A { 
    int t; 
    long u; 
}; 

long test() 
{ 
    struct A a[2]; 
    a[0].t = 1; 
    a[0].u = 100; 
    a[1].t = 2; 
    a[1].u = 3; 

    long total = 0; 
    long i; 
    for (i = 0; i < 5000000000; i++) { 
     struct A* p = &a[i % 2]; 
     total += p->u; 
    } 
    return total; 
} 
int main() 
{ 
    long total = test(); 
    printf("%ld\n", total); 
} 

结果比较:

走:

257500000000 

real 0m9.167s 
user 0m9.196s 
sys 0m0.012s 

C:

257500000000 

real 0m3.585s 
user 0m3.560s 
sys 0m0.008s 

看来,去编译很多奇怪的汇编代码(你可以使用objdump -D来检查它)。

例如,为什么movabs $0x12a05f200,%rbp出现两次?

400c60:  31 c0     xor %eax,%eax 
    400c62:  48 bd 00 f2 05 2a 01 movabs $0x12a05f200,%rbp 
    400c69:  00 00 00 
    400c6c:  48 39 e8    cmp %rbp,%rax 
    400c6f:  7d 46     jge 400cb7 <main.test+0xb7> 
    400c71:  48 89 c1    mov %rax,%rcx 
    400c74:  48 c1 f9 3f    sar $0x3f,%rcx 
    400c78:  48 89 c3    mov %rax,%rbx 
    400c7b:  48 29 cb    sub %rcx,%rbx 
    400c7e:  48 83 e3 01    and $0x1,%rbx 
    400c82:  48 01 cb    add %rcx,%rbx 
    400c85:  48 8d 2c 24    lea (%rsp),%rbp 
    400c89:  48 83 fb 02    cmp $0x2,%rbx 
    400c8d:  73 2d     jae 400cbc <main.test+0xbc> 
    400c8f:  48 6b db 10    imul $0x10,%rbx,%rbx 
    400c93:  48 01 dd    add %rbx,%rbp 
    400c96:  48 8b 5d 08    mov 0x8(%rbp),%rbx 
    400c9a:  48 01 f3    add %rsi,%rbx 
    400c9d:  48 89 de    mov %rbx,%rsi 
    400ca0:  48 89 5c 24 28   mov %rbx,0x28(%rsp) 
    400ca5:  48 ff c0    inc %rax 
    400ca8:  48 bd 00 f2 05 2a 01 movabs $0x12a05f200,%rbp 
    400caf:  00 00 00 
    400cb2:  48 39 e8    cmp %rbp,%rax 
    400cb5:  7c ba     jl  400c71 <main.test+0x71> 
    400cb7:  48 83 c4 20    add $0x20,%rsp 
    400cbb:  c3      retq 
    400cbc:  e8 6f e0 00 00   callq 40ed30 <runtime.panicindex> 
    400cc1:  0f 0b     ud2  
     ... 

而C装配更干净:

0000000000400570 <test>: 
    400570:  48 c7 44 24 e0 64 00 movq $0x64,-0x20(%rsp) 
    400577:  00 00 
    400579:  48 c7 44 24 f0 03 00 movq $0x3,-0x10(%rsp) 
    400580:  00 00 
    400582:  b9 64 00 00 00   mov $0x64,%ecx 
    400587:  31 d2     xor %edx,%edx 
    400589:  31 c0     xor %eax,%eax 
    40058b:  48 be 00 f2 05 2a 01 movabs $0x12a05f200,%rsi 
    400592:  00 00 00 
    400595:  eb 18     jmp 4005af <test+0x3f> 
    400597:  66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1) 
    40059e:  00 00 
    4005a0:  48 89 d1    mov %rdx,%rcx 
    4005a3:  83 e1 01    and $0x1,%ecx 
    4005a6:  48 c1 e1 04    shl $0x4,%rcx 
    4005aa:  48 8b 4c 0c e0   mov -0x20(%rsp,%rcx,1),%rcx 
    4005af:  48 83 c2 01    add $0x1,%rdx 
    4005b3:  48 01 c8    add %rcx,%rax 
    4005b6:  48 39 f2    cmp %rsi,%rdx 
    4005b9:  75 e5     jne 4005a0 <test+0x30> 
    4005bb:  f3 c3     repz retq 
    4005bd:  0f 1f 00    nopl (%rax) 

有人能解释一下吗?谢谢!

+0

您可以用'-gcflag -B'选项禁用数组边界检查来构建go应用程序并重新运行您的基准测试? – kostya

+0

@kostya,我尝试了国旗,但它没有帮助。 – kingluo

+0

@kingluo使用'gccgo'和-O3运行你的代码产生与C类似的结果:'time go run -compiler gccgo -gccgoflags -O3 foo.go'输出:'real \t 0m3.667s用户\t 0m3.644s'。所以这可能是gcc正在做的一些优化。 –

回答

4

主要区别在于数组边界检查。在为围棋程序反汇编转储,有:

400c89:  48 83 fb 02    cmp $0x2,%rbx 
400c8d:  73 2d     jae 400cbc <main.test+0xbc> 
... 
400cbc:  e8 6f e0 00 00   callq 40ed30 <runtime.panicindex> 
400cc1:  0f 0b     ud2  

所以,如果%rbx大于或等于2,然后跳下来向runtime.panicindex通话。鉴于你正在处理大小为2的数组,这显然是边界检查。你可以提出这样一个观点,即编译器应该足够聪明,可以在这种特殊情况下跳过边界检查,其中索引的范围可以静态确定,但似乎还不够聪明。

虽然您看到这个微基准测试显着的性能差异,但可能值得考虑这是否代表您的实际代码。如果你在循环中做其他事情,其差异可能不那么明显。

虽然边界检查确实有成本,但在许多情况下,它比继续存在未定义行为的程序的替代方案要好。

+0

正如@kostya建议的那样,我尝试使用-gcflag -B,但它比原来的只有1秒更快,而且距离C还很远。那么还有其他原因吗? – kingluo

+0

@kingluo您创建一个本地堆栈变量到指向该数组的指针是多余的。通过使整个数组成为指针数组,您将获得几秒的加速。 –

+0

@Not_a_Golfer:你确定吗?它看起来是在边界检查(在'400c96')之后唯一的内存加载是加载'p.u'的值。如果你有一个指向结构体的指针数组,它需要做两个加载。 –