Which is the Go way, PutUint32 or use >> operator directly?

1

This is two ways to get the same 4 bytes:

package main

import (
  "encoding/binary"
  "fmt"
)

func main() {
  i := binary.LittleEndian.Uint32([]byte{1, 2, 3, 0})
  bs := make([]byte, 4)
  binary.LittleEndian.PutUint32(bs, uint32(i))
  fmt.Println(bs[0] == 1 && bs[1] == 2 && bs[2] == 3 && bs[3] == 0)

  bs = []byte{byte(i & 0x000000ff),
    byte(i >> 8 & 0x000000ff),
    byte(i >> 16 & 0x000000ff),
    byte(i >> 24)}
  fmt.Println(bs[0] == 1 && bs[1] == 2 && bs[2] == 3 && bs[3] == 0)
}

They both work. But which is the way the Go community would say is best?

go
binary
asked on Stack Overflow Nov 9, 2019 by Andrew Arrow • edited Nov 9, 2019 by wasmup

1 Answer

3

Let's look inside the Go standard library:

func (littleEndian) PutUint32(b []byte, v uint32) {
    _ = b[3] // early bounds check to guarantee safety of writes below
    b[0] = byte(v)
    b[1] = byte(v >> 8)
    b[2] = byte(v >> 16)
    b[3] = byte(v >> 24)
}

And see the Package binarydocumentation:

This package favors simplicity over efficiency. Clients that require high-performance serialization, especially for large data structures, should look at more advanced solutions such as the encoding/gob package or protocol buffers.

Which way we should go, depends on a problem we are trying to solve:
When the efficiency is not the main concern (of the problem we are trying to solve with programming), The simplicity is the way to go (because less bug is the main goal here):

  1. You may use the Go standard library function binary.LittleEndian.PutUint32 with an array (note you may use an array [4]byte{} here, for efficiency and simplicity over make([]byte, 4)):
        var v uint32 = 0x4030201
        ary := [4]byte{}
        binary.LittleEndian.PutUint32(ary[:], v)
        fmt.Println(ary) // [1 2 3 4]
  1. You may use the Go standard library function binary.LittleEndian.PutUint32 with a slice:
        var v uint32 = 0x4030201
        b := make([]byte, 4)
        binary.LittleEndian.PutUint32(b, v)
        fmt.Println(b) // [1 2 3 4]
  1. You may write it directly (efficiency, since no library function call):
        var v uint32 = 0x4030201
        a := [4]byte{
            byte(v),
            byte(v >> 8),
            byte(v >> 16),
            byte(v >> 24),
        }
        fmt.Println(a) // [1 2 3 4]
  1. You may use unsafe.Pointer (Just for efficiency or compatiblity or ...):
        var v uint32 = 1
        a := (*[4]byte)(unsafe.Pointer(&v))
        a[0] = 100        // same memory (Like `union` in the C language)
        fmt.Println(a, v) // &[100 0 0 0] 100
  1. You may use unsafe.Pointer and make a copy in one-line (so not a union):
        var v uint32 = 0x4030201
        a := *(*[4]byte)(unsafe.Pointer(&v))
        fmt.Println(a) // [1 2 3 4]
  1. You may use unsafe.Pointer with the copy to a slice:
    b := make([]byte, 4)
    copy(b, (*[4]byte)(unsafe.Pointer(&v))[:])

Try it all here


Benchmark:

BenchmarkFn1-8          580469606                1.94 ns/op
BenchmarkFn2-8          568699358                2.06 ns/op
BenchmarkFn3-8          604883466                1.86 ns/op
BenchmarkFn4-8          824232160                1.33 ns/op
BenchmarkFn5-8          626357875                1.82 ns/op
BenchmarkFn6-8          622969119                1.82 ns/op
BenchmarkFn7-8          469203398                2.35 ns/op
BenchmarkFn8-8          637403140                1.80 ns/op
BenchmarkFn9-8          647179550                1.80 ns/op

main.go file:

package main

import (
    "encoding/binary"
    "unsafe"
)

// 1.94 ns/op
func fn1(v uint32) [4]byte {
    ary := [4]byte{}
    binary.LittleEndian.PutUint32(ary[:], v)
    return ary
}

// 2.06 ns/op
func fn2(v uint32) []byte {
    b := make([]byte, 4)
    binary.LittleEndian.PutUint32(b, v)
    return b
}

// 1.86 ns/op
func fn3(v uint32) [4]byte {
    a := [4]byte{
        byte(v),
        byte(v >> 8),
        byte(v >> 16),
        byte(v >> 24),
    }
    return a
}

// 1.33 ns/op
func fn4(v uint32) *[4]byte {
    a := (*[4]byte)(unsafe.Pointer(&v))
    return a
}

// 1.82 ns/op
func fn5(v uint32) [4]byte {
    a := *(*[4]byte)(unsafe.Pointer(&v))
    return a
}

// 1.82 ns/op
func fn6(v uint32) []byte {
    b := make([]byte, 4)
    copy(b, (*[4]byte)(unsafe.Pointer(&v))[:])
    return b
}

// 2.35 ns/op
func fn7(v uint32) [4]byte {
    b := [4]byte{}
    copy(b[:], (*[4]byte)(unsafe.Pointer(&v))[:])
    return b
}

// 1.80 ns/op
func fn8(v uint32) *[4]byte {
    b := [4]byte{}
    copy(b[:], (*[4]byte)(unsafe.Pointer(&v))[:])
    return &b
}

//1.80 ns/op
func fn9(v uint32) []byte {
    b := [4]byte{}
    copy(b[:], (*[4]byte)(unsafe.Pointer(&v))[:])
    return b[:]
}

func main() {}

main_test.go file:

package main

import "testing"

var result int

func BenchmarkFn1(b *testing.B) {
    sum := 0
    for i := 0; i < b.N; i++ {
        r := fn1(uint32(i))
        sum += int(r[0]) + int(r[1]) + int(r[2]) + int(r[3])
    }
    result = sum
}

func BenchmarkFn2(b *testing.B) {
    sum := 0
    for i := 0; i < b.N; i++ {
        r := fn2(uint32(i))
        sum += int(r[0]) + int(r[1]) + int(r[2]) + int(r[3])
    }
    result = sum
}

func BenchmarkFn3(b *testing.B) {
    sum := 0
    for i := 0; i < b.N; i++ {
        r := fn3(uint32(i))
        sum += int(r[0]) + int(r[1]) + int(r[2]) + int(r[3])
    }
    result = sum
}

func BenchmarkFn4(b *testing.B) {
    sum := 0
    for i := 0; i < b.N; i++ {
        r := fn4(uint32(i))
        sum += int(r[0]) + int(r[1]) + int(r[2]) + int(r[3])
    }
    result = sum
}

func BenchmarkFn5(b *testing.B) {
    sum := 0
    for i := 0; i < b.N; i++ {
        r := fn5(uint32(i))
        sum += int(r[0]) + int(r[1]) + int(r[2]) + int(r[3])
    }
    result = sum
}

func BenchmarkFn6(b *testing.B) {
    sum := 0
    for i := 0; i < b.N; i++ {
        r := fn6(uint32(i))
        sum += int(r[0]) + int(r[1]) + int(r[2]) + int(r[3])
    }
    result = sum
}

func BenchmarkFn7(b *testing.B) {
    sum := 0
    for i := 0; i < b.N; i++ {
        r := fn7(uint32(i))
        sum += int(r[0]) + int(r[1]) + int(r[2]) + int(r[3])
    }
    result = sum
}

func BenchmarkFn8(b *testing.B) {
    sum := 0
    for i := 0; i < b.N; i++ {
        r := fn8(uint32(i))
        sum += int(r[0]) + int(r[1]) + int(r[2]) + int(r[3])
    }
    result = sum
}

func BenchmarkFn9(b *testing.B) {
    sum := 0
    for i := 0; i < b.N; i++ {
        r := fn9(uint32(i))
        sum += int(r[0]) + int(r[1]) + int(r[2]) + int(r[3])
    }
    result = sum
}


Conclusion

Ultra fast and unsafe (shared memory e.g. C union and it may differ from little-endian to big-endian system):

a := (*[4]byte)(unsafe.Pointer(&v))

Fast and safe (copy of v as an array):

a := [4]byte{byte(v), byte(v >> 8), byte(v >> 16), byte(v >> 24)}

Simple and fast (copy of v as an array using the standard library):

    ary := [4]byte{}
    binary.LittleEndian.PutUint32(ary[:], v)

Beautiful (copy of v as a slice using the standard library):

    b := make([]byte, 4)
    binary.LittleEndian.PutUint32(b, v)
answered on Stack Overflow Nov 9, 2019 by wasmup • edited Nov 9, 2019 by wasmup

User contributions licensed under CC BY-SA 3.0