Skip to content

cmd/compile: read-only escape analysis and avoiding string -> []byte copies #2205

Closed

Description

@bradfitz
Many functions take a []byte but only read from it.

If the escape analysis code could flag parameters of a function as read-only, then code
which passes in a []byte(string) conversion could be cheaper.

bytes.IndexByte is one example.

For example, http/sniff.go does:

// -------------------
  // Index of the first non-whitespace byte in data.                                                                                               
  firstNonWS := 0
  for ; firstNonWS < len(data) && isWS(data[firstNonWS]); firstNonWS++ {
  }

func isWS(b byte) bool {
        return bytes.IndexByte([]byte("\t\n\x0C\n "), b) != -1
}

// -------------------

But it's faster to avoid re-creating the []byte:

func BenchmarkIndexByteLiteral(b *testing.B) {
        for i := 0; i < b.N; i++ {
        IndexByte([]byte("\t\n\x0C\n "), 'x')
        }
}

func BenchmarkIndexByteVariable(b *testing.B) {
        var whitespace = []byte("\t\n\x0C\n ")
        for i := 0; i < b.N; i++ {
                IndexByte(whitespace, 'x')
        }
}

bytes_test.BenchmarkIndexByteLiteral    20000000           125 ns/op
bytes_test.BenchmarkIndexByteVariable   100000000           25.4 ns/op

Related is issue #2204.

Activity

  1. gopherbot commented on Aug 30, 2011

    @gopherbot
    Contributor

    Comment 1 by jp@webmaster.ms:

    It seem to be much easier to add the possibility of zerocopish taking string slices from
    byte slices/arrays.
    As far I understand, string slices are actually readonly and cap()'less byte slices:
    http://research.swtch.com/2009/11/go-data-structures.html
  2. bradfitz commented on Aug 30, 2011

    @bradfitz
    ContributorAuthor

    Comment 2:

    zerocopish?
  3. rsc commented on Aug 30, 2011

    @rsc
    Contributor

    Comment 3:

    Can you be more specific?
    Examples of programs that you think should be
    handled specially would be a good way to do that.
  4. rsc commented on Sep 15, 2011

    @rsc
    Contributor

    Comment 4:

    Owner changed to @rsc.

    Status changed to Accepted.

  5. lvdlvd commented on Nov 7, 2011

    @lvdlvd

    Comment 5:

    Labels changed: added compilerbug, performance.

  6. rsc commented on Dec 9, 2011

    @rsc
    Contributor

    Comment 6:

    Labels changed: added priority-later.

  7. rsc commented on Sep 12, 2012

    @rsc
    Contributor

    Comment 8:

    Labels changed: added go1.1maybe.

  8. bradfitz commented on Oct 25, 2012

    @bradfitz
    ContributorAuthor

    Comment 9:

    I run into this often, but I should start listing examples.
    In goprotobuf, text.go calls writeString with both a string and a []byte converted to a
    string:
    // writeAny writes an arbitrary field.
    func writeAny(w *textWriter, v reflect.Value, props *Properties) {
            v = reflect.Indirect(v)
            // We don't attempt to serialise every possible value type; only those
            // that can occur in protocol buffers, plus a few extra that were easy.
            switch v.Kind() {
            case reflect.Slice:
                    // Should only be a []byte; repeated fields are handled in writeStruct.
                    writeString(w, string(v.Interface().([]byte)))
            case reflect.String:
                    writeString(w, v.String())
    Note that the function writeString reallly just wants a read-only slice of bytes:
    func writeString(w *textWriter, s string) {
            w.WriteByte('"')
            // Loop over the bytes, not the runes.
            for i := 0; i < len(s); i++ {
                    // Divergence from C++: we don't escape apostrophes.
                    // There's no need to escape them, and the C++ parser
                    // copes with a naked apostrophe.
                    switch c := s[i]; c {
                    case '\n':
                            w.Write([]byte{'\\', 'n'})
                    case '\r':
                            w.Write([]byte{'\\', 'r'})
                    case '\t':
                            w.Write([]byte{'\\', 't'})
                    case '"':
                            w.Write([]byte{'\\', '"'})
                    case '\\':
                            w.Write([]byte{'\\', '\\'})
                    default:
                            if isprint(c) {
                                    w.WriteByte(c)
                            } else {
                                    fmt.Fprintf(w, "\\%03o", c)
                            }
                    }
            }
            w.WriteByte('"')
    }
    It doesn't matter that it's frozen (like a string), nor writable (like a []byte).  But
    Go lacks that type, so if instead it'd be nice to write writeAny with a []byte parameter
    and invert the switch above to be like:
            switch v.Kind() {
            case reflect.Slice:
                    // Should only be a []byte; repeated fields are handled in writeStruct.
                    writeString(w, v.Interface().([]byte))
            case reflect.String:
                    writeString(w, []byte(v.String())) // no copy!
    Where the []byte(v.String()) just makes a slice header pointing in to the string's
    memory, since the compiler can verify that writeAny never mutates its slice.
  9. bradfitz commented on Nov 17, 2012

    @bradfitz
    ContributorAuthor

    Comment 10:

    See patch and CL description at http://golang.org/cl/6850067 for the opposite
    but very related case: strconv.ParseUint, ParseBool, etc take a string but calling code
    has a []byte.
  10. robpike commented on Mar 7, 2013

    @robpike
    Contributor

    Comment 11:

    Labels changed: removed go1.1maybe.

  11. rsc commented on Mar 12, 2013

    @rsc
    Contributor

    Comment 12:

    [The time for maybe has passed.]
  12. bradfitz commented on Mar 31, 2013

    @bradfitz
    ContributorAuthor

    Comment 13:

    People who like this bug also like issue #3512 (cmd/gc: optimized map[string] lookup from
    []byte key)
  13. dvyukov commented on Mar 31, 2013

    @dvyukov
    Member

    Comment 14:

    FWIW
    var m map[string]int
    var b []byte
    _ = m[string(b)]
    case must be radically simpler to implement than general read-only analysis.
    It's peephole optimization, when the compiler sees such code it can generate hacky
    string object using the []byte pointer.
    Another example would be len(string(b)), but this seems useless.
  14. bradfitz commented on Mar 31, 2013

    @bradfitz
    ContributorAuthor

    Comment 15:

    Yes. That's why they're separate bugs.
    I want issue #3512 first, because it's easy.
  15. bradfitz commented on Apr 26, 2013

    @bradfitz
    ContributorAuthor

    Comment 16:

    Labels changed: added garbage.

  16. 50 remaining items

  17. gopherbot commented on Aug 18, 2023

    @gopherbot
    Contributor

    Change https://go.dev/cl/520600 mentions this issue: cmd/compile: enable -d=zerocopy by default

  18. jfcg commented on Aug 18, 2023

    @jfcg

    Hi,
    These changes address redundant copying for string <--> []byte conversions both ways, right?

  19. mdempsky commented on Aug 18, 2023

    @mdempsky
    Contributor

    @jfcg No, at this time only string->[]byte.

  20. cespare commented on Aug 18, 2023

    @cespare
    Contributor

    I believe that #43752 discusses at least some of the []byte->string cases.

    (@mdempsky it's exciting that this 4-digit issue is so close to being solved!)

  21. zigo101 commented on Nov 28, 2023

    @zigo101
    Contributor

    BTW, should the old optimization for for range []byte(aString) be unified into the new optimization? Maybe it has been?

  22. added 2 commits that reference this issue on Feb 7, 2024
    c8adb30
    d63c88d
  23. gopherbot commented on May 8, 2024

    @gopherbot
    Contributor

    Change https://go.dev/cl/584118 mentions this issue: encoding/hex: don't overallocate memory in DecodeString

  24. zigo101 commented on Jun 23, 2024

    @zigo101
    Contributor

    So now we can take string element addresses without unsafe.StringData:

    package main
    
    func main() {
    	s := "go"
    	
    	// All true.
    	println(StringData(s) == StringData(s))
    	println(*StringData(s) == 'g')
    	println(*StringData(s[1:]) == 'o')
    }
    
    func StringData(s string) *byte {
    	if len(s) == 0 {
    		return nil
    	}
    	return &[]byte(s)[0]
    }

    ;D

  25. locked and limited conversation to collaborators on Jun 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    ⌂ 换网址