6

I'm experimenting with the Go language and am quite new to it. I have successfully gone through the tutorials and am now writing a little program to evaluate its performance for the type of operations that I typically do. I have a lengthy slice of float32 type and need to convert it to a slice of type float64 as efficiently as possible. Other than iterating through the elements of the slice and explicitly converting types of individual elements via output[i] = float64(data[i]), is there method that I can use to convert the entire slice without need for iteration? I've tried searching for a solution but have not found anything directly related.

user1503949
  • 357
  • 5
  • 10
  • 3
    No... even if this were a builtin, it could hardly do anything other than iterate over the elements in the same way that your 4 lines of code would do. – Paul Hankin Nov 16 '14 at 14:18

2 Answers2

5

Go is quite low-level, this means that iterating through the slice is the most efficient method. Other languages may have built-in functions for such things, but all they do is iterate through the slice, there is no way to do it without the iteration. But there are some tricks, specifically use range and avoid indexing the slice as there is overhead in the out of bounds check. This would be the most efficient:

func convertTo64(ar []float32) []float64 {
   newar := make([]float64, len(ar))
   var v float32
   var i int
   for i, v = range ar {
      newar[i] = float64(v)
   }
   return newar
}

slice32 := make([]float32, 1000)
slice64 := convertTo64(slice32)

Note that the use of := in the range loop would be inefficient because in the current version of Go the variable is thrown away and recreated each time instead of being reused. Using range instead of for i=0; i<n; i++ is more efficient because it saves bounds checks on ar.

Alasdair
  • 13,348
  • 18
  • 82
  • 138
2

It's always wise to be skeptical of performance claims. For example, "the use of := in the range loop would be inefficient because ... the variable is thrown away and recreated each time instead of being reused."

Let's look at benchmark results from three successive runs.

floats_test.go:

package main

import "testing"

var slice64 []float64

func FuncVar(f32 []float32) []float64 {
    f64 := make([]float64, len(f32))
    var f float32
    var i int
    for i, f = range f32 {
        f64[i] = float64(f)
    }
    return f64
}

func BenchmarkFuncVar(b *testing.B) {
    f32 := make([]float32, 1024)
    b.ReportAllocs()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        slice64 = FuncVar(f32)
    }
}

func RangeVar(f32 []float32) []float64 {
    f64 := make([]float64, len(f32))
    for i, f := range f32 {
        f64[i] = float64(f)
    }
    return f64
}

func BenchmarkRangeVar(b *testing.B) {
    f32 := make([]float32, 1024)
    b.ReportAllocs()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        slice64 = RangeVar(f32)
    }
}

func main() {}

Output:

$ go test -v -run=! -bench=.
testing: warning: no tests to run
PASS
BenchmarkFuncVar      100000         12260 ns/op        8192 B/op          1 allocs/op
BenchmarkRangeVar     100000         12125 ns/op        8192 B/op          1 allocs/op
ok      so/test 2.703s
$ go test -v -run=! -bench=.
testing: warning: no tests to run
PASS
BenchmarkFuncVar      100000         12620 ns/op        8192 B/op          1 allocs/op
BenchmarkRangeVar     100000         12623 ns/op        8192 B/op          1 allocs/op
ok      so/test 2.782s
$ go test -v -run=! -bench=.
testing: warning: no tests to run
PASS
BenchmarkFuncVar      100000         12730 ns/op        8192 B/op          1 allocs/op
BenchmarkRangeVar     100000         12971 ns/op        8192 B/op          1 allocs/op
ok      so/test 2.852s
$ 

As requested by a comment that has been deleted. Here's a benchmark using just the system time.

package main

import (
    "fmt"
    "time"
)

const N = 1e6

var f32 = make([]float32, 1024)
var slice64 []float64

func FuncVar(f32 []float32) []float64 {
    f64 := make([]float64, len(f32))
    var f float32
    var i int
    for i, f = range f32 {
        f64[i] = float64(f)
    }
    return f64
}

func BenchmarkFuncVar() {
    t1 := time.Now()
    for i := 0; i < N; i++ {
        slice64 = FuncVar(f32)
    }
    t2 := time.Now()
    fmt.Println("FuncVar", t2.Sub(t1))
}

func RangeVar(f32 []float32) []float64 {
    f64 := make([]float64, len(f32))
    for i, f := range f32 {
        f64[i] = float64(f)
    }
    return f64
}

func BenchmarkRangeVar() {
    t1 := time.Now()
    for i := 0; i < N; i++ {
        slice64 = RangeVar(f32)
    }
    t2 := time.Now()
    fmt.Println("RangeVar", t2.Sub(t1))
}

func main() {
    BenchmarkFuncVar()
    BenchmarkRangeVar()
}

Output:

$ go build floata.go && ./floata
FuncVar 10.479653966s
RangeVar 10.208178244s
$ go build floata.go && ./floata
FuncVar 10.123357283s
RangeVar 10.173007394s
$ go build floata.go && ./floata
FuncVar 9.935580721s
RangeVar 10.109644784s
$ go build floata.go && ./floata
FuncVar 10.070552761s
RangeVar 10.317730473s
$ go build floata.go && ./floata
FuncVar 10.075578601s
RangeVar 10.012273678s
$
peterSO
  • 158,998
  • 31
  • 281
  • 276
  • 1
    Hi peterSo, I had discovered the := range issue in a benchmark I did a few weeks ago and reported it on Golang-Devs. Redoing that benchmark now I can see that it gives no difference between the two. Most likely by earlier benchmark was flawed. Thanks for taking the take to double check. – Alasdair Nov 17 '14 at 05:03