Golang: как эффективно имитировать тип объединения

Как известно, 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

Метод способ лучше, и способ сбрасывания типа требует большего времени процессора.

Версия 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

Тип объединения полезен для многих приложений, например. сетевой протокол может содержать вариант конкретного типа. Поэтому, возможно, доступ к данным объединения может стать узким местом приложения.

Кто-нибудь может помочь? Спасибо.

Ответ 1

Вы можете использовать массивы для представления одного int32 как двух int16, а затем собрать их со сдвигами , как рекомендует Роб Пайк:

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 он работает примерно в 2 раза медленнее, чем версия C, а с gccgo (-O3) он работает примерно с C.

Имейте в виду, что этот подход предполагает использование байтов с прямым порядком байтов. Вам нужно изменить порядок сдвигов для архитектуры с прямым порядком байтов.

Также, если вам нужно декодировать структуры из байтового фрагмента, вам действительно следует использовать encoding/binary. Эта библиотека создана для перевода между последовательностями байтов и другими типами.

Ответ 2

Объединение может содержать числовые типы и строку октета, поэтому я пытаюсь использовать байтовый срез в качестве контейнера значений и использовать 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

Это лучшая версия?

Ответ 3

Бьюсь об заклад, чтобы сделать его намного ближе к варианту 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
}

//твоя скамья:

257500000000
8.111239763s

//родная скамья (8,18800064s):

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

Запустил его на 5-долларовую морскую капельку.


Реализация проклята и может быть несовместима с будущими версиями Go (текущая версия 1.13), но использование (как поведение) является C-подобным, также поддерживает любой тип (вы также можете заменить целые числа структурами)