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

kin*_*luo 14 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)
}
Run Code Online (Sandbox Code Playgroud)

结果:

257500000000
1m23.508223094s
257500000000
34.95081661s
Run Code Online (Sandbox Code Playgroud)

方法方式更好,类型转换方式花费更多的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);
}
Run Code Online (Sandbox Code Playgroud)

结果:

257500000000

real    0m5.620s
user    0m5.620s
sys 0m0.000s
Run Code Online (Sandbox Code Playgroud)

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

有人可以帮忙吗?谢谢.

Lae*_*ter 8

我打赌自己会使其更接近 C 变体,这就是我得到的:

(full code)

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

事情是,我们遍历所有结构体的字段并将它们重定向到缓冲区的存储(为了内存拯救和通用性,它具有从模板结构体引用的编译时 len)

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
}
Run Code Online (Sandbox Code Playgroud)

// 你的板凳:

257500000000
8.111239763s
Run Code Online (Sandbox Code Playgroud)

// 原生工作台 (8,18800064s):

BenchmarkUnion         1        8188000640 ns/op              80 B/op          1 allocs/op
Run Code Online (Sandbox Code Playgroud)

在 5 美元的 digitalocean Droplet 上运行它。


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


Ain*_*r-G 7

您可以使用数组将单个表示int32为两个int16,然后按照Rob Pike建议的顺序组合它们:

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
}
Run Code Online (Sandbox Code Playgroud)

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

但请注意,这种方法假设为little-endian整数.您需要切换big-endian架构的班次顺序.

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

  • 谢谢.我尝试二进制函数,它们似乎非常昂贵.由于联合不打算是有线格式,因此数字类型不需要二进制表示.在我看来,我认为不安全更有意义. (3认同)
  • 是的,它可以提高性能,但是有时变量类型可以包括其他类型,例如字符串或字节数组。 (2认同)

Zac*_*ach 5

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

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

package main

type Int struct {
    i int32
    v int16
}
Run Code Online (Sandbox Code Playgroud)

现在用于unionize生成实际的联合代码,该代码将进入a_union.go

$ unionize -output=a_union.go Int union.go
Run Code Online (Sandbox Code Playgroud)

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)
}
Run Code Online (Sandbox Code Playgroud)

当我计时时,我得到:

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

real    0m6.202s
user    0m6.197s
sys 0m0.012s
Run Code Online (Sandbox Code Playgroud)

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