golang:如何有效地模拟联合类型

时间:2015-07-22 08:15:23

标签: go

众所周知,go没有联合类型,只能通过接口模拟。

我尝试了两种方法来模拟联合,但结果远不如C。

package main

import (
    "fmt"
    "time"
)

type U interface {
    i32() int32
    i16() int16
}

type i32 int32

func (u i32) i32() int32 {
    return int32(u)
}

func (u i32) i16() int16 {
    return int16(u)
}

type i16 int16

func (u i16) i32() int32 {
    return int32(u)
}

func (u i16) i16() int16 {
    return int16(u)
}

func test() (total int64) {
    type A struct {
        t int32
        u interface{}
    }
    a := [...]A{{1, int32(100)}, {2, int16(3)}}

    for i := 0; i < 5000000000; i++ {
        p := &a[i%2]
        switch p.t {
        case 1:
            total += int64(p.u.(int32))
        case 2:
            total += int64(p.u.(int16))
        }
    }
    return
}

func test2() (total int64) {
    type A struct {
        t int32
        u U
    }
    a := [...]A{{1, i32(100)}, {2, i16(3)}}

    for i := 0; i < 5000000000; i++ {
        p := &a[i%2]
        switch p.t {
        case 1:
            total += int64(p.u.i32())
        case 2:
            total += int64(p.u.i16())
        }
    }
    return
}

type testfn func() int64

func run(f testfn) {
    ts := time.Now()
    total := f()
    te := time.Now()
    fmt.Println(total)
    fmt.Println(te.Sub(ts))
}

func main() {
    run(test)
    run(test2)
}

结果:

257500000000
1m23.508223094s
257500000000
34.95081661s

方法方式更好,而且类型转换方式会花费更多的CPU时间。

C版:

#include <stdio.h>

struct A {
    int t;
    union {
        int i;
        short v;
    } u;
};

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

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

结果:

257500000000

real    0m5.620s
user    0m5.620s
sys 0m0.000s

联合类型对许多应用程序很有用,例如:网络协议可能包含变体具体类型。 因此,联合数据的访问可能会成为应用程序的瓶颈。

有人可以帮忙吗?感谢。

4 个答案:

答案 0 :(得分:8)

您可以使用数组将单个int32表示为两个int16,然后使用班次as Rob Pike recommends汇总它们:

func test3() (total int64) {
    type A struct {
        t int32
        u [2]int16
    }
    a := [...]A{
        {1, [2]int16{100, 0}},
        {2, [2]int16{3, 0}},
    }

    for i := 0; i < N; i++ {
        p := &a[i%2]
        switch p.t {
        case 1:
            total += int64(p.u[0]<<0 | p.u[1]<<8)
        case 2:
            total += int64(p.u[0])
        }
    }
    return
}

使用原始Go编译器,它运行速度比C版慢2倍,而使用gccgo(-O3)运行速度与C版一样快。

请注意,这种方法假定为小端整数。您需要切换big-endian架构的班次顺序。

此外,如果您需要从字节切片解码结构,那么您应该使用encoding/binary。创建此库是为了在字节序列和其他类型之间进行转换。

答案 1 :(得分:2)

union可能包含数字类型和八位字节字符串,因此我尝试使用字节切片作为值容器,并根据具体类型使用unsafe.Pointer来访问它。

func test3() (total int64) {
    type A struct {
        t int32
        u []byte
    }   

    a := [...]A{{1, make([]byte, 8)}, {2, make([]byte, 8)}}
    *(*int32)(unsafe.Pointer(&a[0].u)) = 100 
    *(*int16)(unsafe.Pointer(&a[1].u)) = 3 

    for i := 0; i < 5000000000; i++ {
        p := &a[i%2]
        switch p.t {
        case 1:
            total += int64(*(*int32)(unsafe.Pointer(&p.u)))
        case 2:
            total += int64(*(*int16)(unsafe.Pointer(&p.u)))
        }   
    }   
    return
}

结果:

$ go run union.go
257500000000
12.844752701s

$ go run -compiler gccgo -gccgoflags -O3 union.go
257500000000
6.640667s

它是最好的版本吗?

答案 2 :(得分:2)

我敢打赌,要使其更接近C变体,这就是我得到的:

(full code)

https://play.golang.org/p/3FJTI6xSsd8

事情是,我们遍历所有struct的字段并将它们重定向到缓冲区的存储(出于内存节省和通用性的考虑,它在编译时从模板struct引用)

result:

func test() (total int64) {

    type A struct {
        t int32
        u struct {
            // embedded buffer of union
            FooSize

            // mark all types inside as pointer types
            i *int32 // long
            v *int16 //short
        }
    }
    var a [2]A

    // initialize them
    Union(&a[0].u)
    Union(&a[1].u)

    a[0].t = 1
    *a[0].u.i = 100
    a[1].t = 2
    *a[1].u.v = 3

    for c := 0; c < 5000000000; c++ {
        p := &a[c%2]
        switch p.t {
        case 1:
            total += int64(*p.u.i)
        case 2:
            total += int64(*p.u.v)
        }
    }

    return
}

//您的板凳:

257500000000
8.111239763s

//本机工作台(8,18800064s):

BenchmarkUnion         1        8188000640 ns/op              80 B/op          1 allocs/op

将其投放到5美元的Digitalocean小滴上。


实施被诅咒,可能与Go的未来版本(当前为1.13)不兼容,但是用法(由于行为)类似于C,还支持任何类型(您也可以用结构替换整数)

答案 3 :(得分:0)

我编写了一个小工具来生成名为 unionize 的 C 风格联合,您可以在 https://github.com/zyedidia/unionize 找到它。你给它一个模板,然后它会生成像联合一样的 Go 代码,并且具有与 C 相当的性能(警告:它使用 unsafe 包,请参阅 github 存储库了解其工作原理以及对替代方案的详细讨论)。

我使用 unionize 将您的 C 基准测试复制到 Go 中。首先为联合创建一个模板,例如在 union.go:

package main

type Int struct {
    i int32
    v int16
}

现在使用 unionize 生成将进入 a_union.go 的实际联合代码:

$ unionize -output=a_union.go Int union.go

这会从 IntUnion 模板创建一个新类型 Int,该模板公开了操作联合成员的函数。现在我们可以使用该类型编写基准测试:

package main

import "fmt"

type A struct {
    t int
    u IntUnion
}

func main() {
    var a [2]A
    a[0].t = 1
    a[0].u.iPut(100)
    a[1].t = 2
    a[1].u.vPut(3)

    var total int
    for i := 0; i < 5000000000; i++ {
        p := &a[i%2]
        switch p.t {
        case 1:
            total += int(p.u.i())
        case 2:
            total += int(p.u.v())
        }
    }

    fmt.Println(total)
}

当我计时的时候:

$ go build main.go a_union.go
$ time ./main
257500000000

real    0m6.202s
user    0m6.197s
sys 0m0.012s

还不错! (在我的机器上,C 基准测试运行大约需要 3 秒)。该工具相当小,如果您需要更多功能,或者您发现任何错误,请告诉我。