Skip to content

optimize range over []byte(string) #2700

@FrankReh

Description

@FrankReh

The current system works but enlists an allocation and memcopy.

As it stands, there is already a mechanism enlisting unsafe and reflect used in the wild for converting a string to a byte slice without the allocation and copy. And the support by tinygo for unsafe and reflect already makes it work. See the example further down.

A function performing the range:

func rangeStringAsBytes(s string) {
	for _, b := range []byte(s) {
		save += b
	}
}

is compiled by tinygo to this wasm function (note the call to runtime_stringToBytes):

function rangeStringAsBytes() {
  var e:int;
  var a:int = stack_pointer - 16;
  stack_pointer = a;
  a[1]:long@4 = 1L;
  var b:int = 0;
  var d:int = 99548[0]:int;
  99548[0]:int = a;
  a[0]:int = d;

  (a + 8)[0]:int = (e = runtime_stringToBytes(85512, 35));

  var c:int = 101304[0]:ubyte;
  loop L_a {
    if (b == 35) {
      101304[0]:byte = c;
      99548[0]:int = d;
      stack_pointer = a + 16;
    } else {
      c = (b + e)[0]:ubyte + c;
      b = b + 1;
      continue L_a;
    }
  }
}

A lead go developer provided a solution using unsafe and reflect. This already works with tinygo.

This idea was fleshed out a bit in
https://stackoverflow.com/questions/59209493/how-to-use-unsafe-get-a-byte-slice-from-a-string-without-memory-copy .
The comments by two go developers are here:
https://groups.google.com/g/golang-nuts/c/Zsfk-VMd_fU/m/O1ru4fO-BgAJ

func unsafeGetBytes(s string) []byte {
	return (*[0x7fff0000]byte)(unsafe.Pointer(
		(*reflect.StringHeader)(unsafe.Pointer(&s)).Data),
	)[:len(s):len(s)]
}

// As in

func rangeUnsafeStringAsBytes(s string) {
	for _, b := range unsafeGetBytes(s) {
		save += b
	}
}

Some benchmarks showing bytes, strings, and manual/unsafe conversions between the two. Starred are the relevant results for this issue.

tinygo/wasm running in nodejs on older mac:
BenchmarkRangeBytes
BenchmarkRangeBytes              	83960235	        14.45 ns/op *
BenchmarkRangeBytes              	82679247	        14.36 ns/op
BenchmarkRangeBytes              	83991829	        14.38 ns/op
BenchmarkRangeStringAsBytes
BenchmarkRangeStringAsBytes      	15033096	        79.55 ns/op *
BenchmarkRangeStringAsBytes      	14958260	        79.64 ns/op
BenchmarkRangeStringAsBytes      	14833155	        79.80 ns/op
BenchmarkRangeUnsafeStringAsBytes
BenchmarkRangeUnsafeStringAsBytes	83963244	        14.51 ns/op *
BenchmarkRangeUnsafeStringAsBytes	81523156	        14.43 ns/op
BenchmarkRangeUnsafeStringAsBytes	83891115	        14.51 ns/op
BenchmarkMapStringLookup
BenchmarkMapStringLookup         	15326641	        78.12 ns/op
BenchmarkMapStringLookup         	15441502	        79.49 ns/op
BenchmarkMapStringLookup         	15446286	        77.92 ns/op
BenchmarkMapBytesLookup
BenchmarkMapBytesLookup          	 8191792	       144.5 ns/op
BenchmarkMapBytesLookup          	 8333955	       143.1 ns/op
BenchmarkMapBytesLookup          	 8344280	       142.8 ns/op
BenchmarkMapUnsafeBytesLookup
BenchmarkMapUnsafeBytesLookup    	15252482	        78.89 ns/op
BenchmarkMapUnsafeBytesLookup    	14966283	        79.39 ns/op
BenchmarkMapUnsafeBytesLookup    	15070021	        79.89 ns/op

The code for this benchmark can be found in issue #2699

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions