LeMoussel
LeMoussel

Reputation: 5767

Efficient way to convert an int to Hex string and then pad it with 0's in Golang?

I'm trying to convert 10 millions of int to Hex, then, padding it with 0's in order to get a 4 characters string which represents the Hex number.

So far, I tried the following:

var hexNumber string
for idx := O; idx < 10000000; idx++ {
    hexNumber = fmt.Sprintf("%04x", idx)

    // Do some stuff ....
}

But fmt.Sprintf is not very efficient. How can I achieve this in a efficient way?

Solution: It turns out that @peterSO strconv.AppendInt solution is way faster.

package bench

import (
    "fmt"
    "strconv"
    "strings"
    "testing"
)

var stringHex [16]string
var runesHex [16]rune

func init() {
    stringHex = [16]string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"}
    runesHex = [16]rune{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}
}

func intToHex1(intNumber int) string {
    hexNumber := []rune("0000")
    for i, j := int(0), uint(12); i < 4; i, j = i+1, j-4 {
        hexNumber[i] = runesHex[(intNumber>>j)&0x0f]
    }
    return string(hexNumber)
}

func intToHex2(intNumber int) string {
    hexNumber := "0000"
    for i, j := int(0), uint(12); i < 4; i, j = i+1, j-4 {
        hexNumber = hexNumber[:i] + stringHex[(intNumber>>j)&0x0f] + hexNumber[i+1:]
    }
    return hexNumber
}

func BenchmarkFmtSprintf(b *testing.B) {
    b.ReportAllocs()
    for n := 0; n < b.N; n++ {
        hexNumber := fmt.Sprintf("%04x", n)
        _ = hexNumber
    }
}

func BenchmarkStrconvFormatInt(b *testing.B) {
    b.ReportAllocs()
    for n := 0; n < b.N; n++ {
        retStr := strings.Repeat("0", 4) + strconv.FormatInt(int64(n), 16)
        hexNumber := retStr[(len(retStr) - 4):]
        _ = hexNumber
    }
}

func BenchmarkAppend(b *testing.B) {
    b.ReportAllocs()
    buf := []byte{'0', '0', '0', '0', 4 + 16: 0}
    for n := 0; n < b.N; n++ {
        buf = strconv.AppendInt(buf[:4], int64(n), 16)
        hexNumber := string(buf[len(buf)-4:])
        _ = hexNumber
    }
}

func BenchmarkIntToHex1(b *testing.B) {
    b.ReportAllocs()
    for n := 0; n < b.N; n++ {
        hexNumber := intToHex1(n)
        _ = hexNumber
    }
}

func BenchmarkIntToHex2(b *testing.B) {
    b.ReportAllocs()
    for n := 0; n < b.N; n++ {
        hexNumber := intToHex2(n)
        _ = hexNumber
    }
}

So the benchmark:

BenchmarkFmtSprintf-2            3000000               364 ns/op              16 B/op          2 allocs/op
BenchmarkStrconvFormatInt-2      5000000               354 ns/op              15 B/op          3 allocs/op
BenchmarkAppend-2               20000000                75.6 ns/op             0 B/op          0 allocs/op
BenchmarkIntToHex1-2            10000000               162 ns/op               8 B/op          1 allocs/op
BenchmarkIntToHex2-2             3000000               536 ns/op              16 B/op          4 allocs/op

Upvotes: 3

Views: 4476

Answers (3)

Jeffrey Goldberg
Jeffrey Goldberg

Reputation: 420

I am not sure that this is the best way to do it, but I have a gist and playground illustrating the problem you are trying to solve and the approach I eventually took.

// hex formater with %x can lose a leading 0
// This can produce strings with an odd number of hex digits

package main

import (
    "encoding/binary"
    "encoding/hex"
    "fmt"
)

func main() {
    var foo uint16 = 0x0ABC // decimal 2748

    with_pct := fmt.Sprintf("%x", foo)
    fmt.Printf("With string formatter: %q\n", with_pct)
    // Outputs: With string formatter: "abc"

    // I hope there is a cleaner way to get an integer into a []byte
    foo_bytes := make([]byte, 2)
    binary.BigEndian.PutUint16(foo_bytes, foo)

    with_bytes := hex.EncodeToString(foo_bytes)
    fmt.Printf("With hex encoding: %q\n", with_bytes)
    // Outputs: With hex encoding: "0abc"
}

I am not particularly happy with this approach. I have to manually set the length of the byte array based on the size of the int, as Go's len() function isn't like C's size_of operator.

This does work more nicely when trying to get a hex representation of an non-negative big.Int, as .Bytes() gives us the big-endian byte array.

Upvotes: 0

peterSO
peterSO

Reputation: 166596

strconv.AppendUint appears to be faster than fmt.Sprintf. For example,

hex_test.go:

package main

import (
    "fmt"
    "strconv"
    "testing"
)

func BenchmarkFmtSprintf(b *testing.B) {
    b.ReportAllocs()
    for n := 0; n < b.N; n++ {
        hexNumber := fmt.Sprintf("%04x", n&0xFFFF)
        _ = hexNumber
    }
}

func BenchmarkAppend(b *testing.B) {
    b.ReportAllocs()
    for n := 0; n < b.N; n++ {
        buf := []byte{'0', '0', '0', 3 + 4: 0}
        buf = strconv.AppendUint(buf[:3], uint64(n)&0xFFFF, 16)
        hexNumber := string(buf[len(buf)-4:])
        _ = hexNumber // Do some stuff ....
    }
}

Output:

$ go test -bench=. hex_test.go
BenchmarkSprintf-4      10000000       116 ns/op      16 B/op     1 allocs/op
BenchmarkAppend-4       100000000       19.2 ns/op     0 B/op     0 allocs/op

Upvotes: 3

Kenny Grant
Kenny Grant

Reputation: 9623

You should be able to use strconv, then just pad manually. This probably does less work.

strconv.FormatInt(idx,16)

Upvotes: -1

Related Questions