Skip to content

Instantly share code, notes, and snippets.

@pelletier
Last active August 5, 2021 21:15
Show Gist options
  • Save pelletier/f6de3afb24d07a0638165f3d7c6dfd79 to your computer and use it in GitHub Desktop.
Save pelletier/f6de3afb24d07a0638165f3d7c6dfd79 to your computer and use it in GitHub Desktop.
Don't do this at work!

Pattern:

func process(inputs []string) {
	// (1)
	c := make(chan string, len(inputs))
	for _, x := range inputs {
		c<-x
	}
	close(c)

	// (2) spin up go routine, do some work.

	// (3) wait for results, error. 
}

Let's see how we can make (1) more efficient.

  • ReadOnlyChanFromSlice: safe, this code above.
  • ReadOnlyChanFromSliceUnsafe: unsafe, copy the input directly to the chan's buffer.
  • BenchmarkUnsafer: unsafer, makes the chan point to the input directly.

(go 1.16.6)

Please don't do this.


goos: linux
goarch: amd64
pkg: github.com/pelletier/foo
cpu: AMD Ryzen 9 5950X 16-Core Processor            
BenchmarkSafe/strings_1-32         	13653536	        87.90 ns/op	     112 B/op	       2 allocs/op
BenchmarkSafe/strings_3-32         	 8785500	       181.0 ns/op	     144 B/op	       2 allocs/op
BenchmarkSafe/strings_32-32        	 1892090	       635.6 ns/op	     608 B/op	       2 allocs/op
BenchmarkSafe/strings_100-32       	  667189	      1805 ns/op	    1888 B/op	       2 allocs/op
BenchmarkSafe/strings_1000-32      	   71019	     16853 ns/op	   16480 B/op	       2 allocs/op
BenchmarkSafe/strings_10000-32     	    6999	    169562 ns/op	  163936 B/op	       2 allocs/op
BenchmarkUnsafe/strings_1-32       	14865825	        80.16 ns/op	     112 B/op	       2 allocs/op
BenchmarkUnsafe/strings_3-32       	12097567	        98.35 ns/op	     144 B/op	       2 allocs/op
BenchmarkUnsafe/strings_32-32      	 5981284	       200.5 ns/op	     608 B/op	       2 allocs/op
BenchmarkUnsafe/strings_100-32     	 2817764	       429.3 ns/op	    1888 B/op	       2 allocs/op
BenchmarkUnsafe/strings_1000-32    	  409248	      2975 ns/op	   16480 B/op	       2 allocs/op
BenchmarkUnsafe/strings_10000-32   	   27013	     46426 ns/op	  163936 B/op	       2 allocs/op
BenchmarkUnsafer/strings_1-32      	30140151	        40.36 ns/op	      96 B/op	       1 allocs/op
BenchmarkUnsafer/strings_3-32      	31440512	        40.51 ns/op	      96 B/op	       1 allocs/op
BenchmarkUnsafer/strings_32-32     	30768156	        39.77 ns/op	      96 B/op	       1 allocs/op
BenchmarkUnsafer/strings_100-32    	27903132	        40.11 ns/op	      96 B/op	       1 allocs/op
BenchmarkUnsafer/strings_1000-32   	29842083	        39.70 ns/op	      96 B/op	       1 allocs/op
BenchmarkUnsafer/strings_10000-32  	28547709	        40.79 ns/op	      96 B/op	       1 allocs/op
PASS
package main
import (
"reflect"
"unsafe"
)
// Safe version to do this.
func ReadOnlyChanFromSlice(s []string) <-chan string {
c := make(chan string, len(s))
for _, x := range s {
c <- x
}
close(c)
return c
}
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
// a lot more
}
// Unsafe version that does a copy of the input slice directly into the
// channel's buffer.
func ReadOnlyChanFromSliceUnsafe(s []string) <-chan string {
c := make(chan string, len(s))
cp := *(**hchan)(unsafe.Pointer(&c))
cs := reflect.SliceHeader{
Data: uintptr(cp.buf),
Len: len(s),
Cap: len(s),
}
copy(*(*[]string)(unsafe.Pointer(&cs)), s)
cp.qcount = uint(len(s))
close(c)
return c
}
// Unsafer version that makes the channel use the backing array of the slice
// directly.
// Mutates the backing array!
// I think it also potentially makes the GC forget about that array; you need to
// keep a reference to it on your own.
func ReadOnlyChanFromSliceUnsafer(s []string) <-chan string {
c := make(chan string)
cp := *(**hchan)(unsafe.Pointer(&c))
cp.buf = unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&s)).Data)
cp.qcount = uint(len(s))
cp.dataqsiz = cp.qcount * uint(cp.elemsize)
close(c)
return c
}
func main() {
}
package main
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
type fnType = func(s []string) <-chan string
func tests(fn fnType, t *testing.T) {
counts := []int{1, 3, 32, 100, 1000, 10_000}
for _, count := range counts {
t.Run(fmt.Sprintf("simple %d", count), func(t *testing.T) {
s := make([]string, count)
for i := 0; i < count; i++ {
s[i] = fmt.Sprintf("%d", i)
}
expected := make([]string, len(s))
copy(expected, s)
c := fn(s)
r := []string{}
for x := range c {
r = append(r, x)
}
require.Equal(t, expected, r)
})
}
}
func TestReadOnlyChanFromSlice(t *testing.T) {
tests(ReadOnlyChanFromSlice, t)
}
func TestReadOnlyChanFromSliceUnsafe(t *testing.T) {
tests(ReadOnlyChanFromSliceUnsafe, t)
}
func TestReadOnlyChanFromSliceUnsafer(t *testing.T) {
tests(ReadOnlyChanFromSliceUnsafer, t)
}
func bench(fn fnType, b *testing.B) {
counts := []int{1, 3, 32, 100, 1000, 10_000}
for _, count := range counts {
b.Run(fmt.Sprintf("strings %d", count), func(b *testing.B) {
s := make([]string, count)
for i := 0; i < count; i++ {
s[i] = fmt.Sprintf("%d", i)
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
fn(s)
}
})
}
}
func BenchmarkSafe(b *testing.B) {
bench(ReadOnlyChanFromSlice, b)
}
func BenchmarkUnsafe(b *testing.B) {
bench(ReadOnlyChanFromSliceUnsafe, b)
}
func BenchmarkUnsafer(b *testing.B) {
bench(ReadOnlyChanFromSliceUnsafer, b)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment