Skip to content

bytes, strings: optimize Trim for ASCII cutsets #46446

Closed

Description

@dsnet

I analyzed usages of strings.Trim (and related functions in both strings and bytes) in all source code known by the module proxy:

  • 76.6% have a cutset of len=1, and
  • 13.4% have a cutset of len=2.

Given that a vast majority of usages only have a cutset of len=1, I argue we should more heavily optimize for that situation. Currently, there is some optimization for cutsets of len=1, but it’s within the internal makeCutsetFunc function. This is sub-optimal as it incurs an allocation in the internal makeCutsetFunc for the closure over that single byte. I believe we should place special handling of cutsets with len=1 directly in Trim, TrimRight, and TrimLeft.

Example benchmark for strings.TrimRight("hello==", "="):

BenchmarkStd   	18299240	        69.16 ns/op	      24 B/op	       1 allocs/op
BenchmarkOpt   	226575349	         5.330 ns/op	       0 B/op	       0 allocs/op

The suggested optimization results in a >10x speedup for this common case.

P.S. There is currently an optimization for cutsets that are all ASCII. This optimization is still justified to keep as 99.3% of all cutsets are pure ASCII.

Activity

  1. added
    NeedsDecisionFeedback is required from experts, contributors, and/or the community before a change can be made.
    on May 29, 2021
  2. ianlancetaylor commented on May 29, 2021

    @ianlancetaylor
    Contributor

    Out of curiosity, is there any particular byte that predominantly appears in the set of single-byte cutsets?

  3. dsnet commented on May 30, 2021

    @dsnet
    MemberAuthor

    Out of curiosity, is there any particular byte that predominantly appears in the set of single-byte cutsets?

    The top-ten single-byte cutsets are:

    • '/' at 30.2%
    • '\n' at 15.7%
    • ' ' at 10.1%
    • '.' at 7.0%
    • '\x00' at 5.7%
    • '0' at 5.0%
    • ',' at 3.5%
    • '=' at 3.4%
    • '-' at 2.6%
    • '|' at 2.5%
    • everything else at 14.3%
  4. dsnet commented on May 30, 2021

    @dsnet
    MemberAuthor

    https://go-review.googlesource.com/c/go/+/323315 is an example of speedup in application code if bytes.Trim were to be optimized.

  5. gopherbot commented on May 30, 2021

    @gopherbot
    Contributor

    Change https://golang.org/cl/323318 mentions this issue: bytes, strings: optimize Trim for single byte cutsets

  6. nightlyone commented on May 30, 2021

    @nightlyone
    Contributor

    Maybe is is useful to do the same optimization as LastIndexAny and IndexAny where the cutover to use a cutset is 8 bytes.

  7. dsnet commented on May 30, 2021

    @dsnet
    MemberAuthor

    @nightlyone, can you elaborate more what you have in mind?

    Currently, the logic for LastIndexAny is already fairly complex. Any optimizations of LastIndexAny should be driven by how Go code at scale uses it, which is an analysis I haven't performed yet.

    I should note that the we could conceivably delete the optimization in LastIndexAny for a cutset with len=1, which was added in golang.org/cl/156998. I don't find that specific optimization particularly compelling since:

    1. LastIndexByte already exists, and
    2. a previous commit by mine indicates that only 6% of usages have 1 character cutsets.
  8. josharian commented on Jun 1, 2021

    @josharian
    Contributor

    I recently cobbled together https://go-review.googlesource.com/c/go/+/271669 to work around that allocation. As I noted there, "another alternative is to duplicate a bunch of the trimming code to work directly on asciiSets", which might be a viable option to consider.

    @bradfitz also had some thoughts about this. I've forgotten what they were, but perhaps he remembers. :)

  9. dsnet commented on Jun 1, 2021

    @dsnet
    MemberAuthor

    Interesting idea caching the cutset functions. I think inlining the single-byte handling is still preferred since a quick check against a single byte is dramatically faster than always calling a func(rune) bool.

    That said, if it provides any insights, the top most common cutsets with len>1 are:

    • "\r\n" at 4.6%
    • " \t" at 2.3%
    • "\n\r" at 0.8%
    • "//" at 0.7%
    • ", " at 0.5%
    • " \t\r\n" at 0.5%
    • "\t\n\f\r " at 0.4%
    • " \t\n\f\r" at 0.4%
    • "./" at 0.3%
    • "01234567" at 0.3%

    Observations of the above:

    • Most of them deal with ASCII whitespace characters.
    • Trimming ASCII digits seems common.
    • One of them is useless (e.g., "//" is an identical cutset to "/").
  10. gopherbot commented on Jun 5, 2021

    @gopherbot
    Contributor

    Change https://golang.org/cl/325251 mentions this issue: strings,bytes: optimize the Trim/TrimLeft/TrimRight fast paths

  11. added 2 commits that reference this issue on Jul 4, 2021
    9e4418b
    6882a11
  12. gopherbot commented on Jul 4, 2021

    @gopherbot
    Contributor

    Change https://golang.org/cl/332771 mentions this issue: strings,bytes: optimize the Trim/TrimLeft/TrimRight fast paths

  13. CAFxX commented on Jul 5, 2021

    @CAFxX
    Contributor

    https://golang.org/cl/332771 removes the allocation in all cases, and in general speeds up Trim/TrimLeft/TrimRight 2x~10x depending on the cutset, with the biggest speedups for the case of a cutset composed of a single ASCII character.

  14. 2 remaining items

  15. changed the title [-]bytes, strings: optimize Trim for 1-byte cutsets[/-] [+]bytes, strings: optimize Trim for multi-byte cutsets[/+] on Aug 25, 2021
  16. reopened this on Aug 25, 2021
  17. dsnet commented on Aug 25, 2021

    @dsnet
    MemberAuthor

    Re-opening since there's some interest in optimizing for multi-byte ASCII cutsets. The evidence for supporting single-byte cutsets is very strong given that they are >75% of all usages. For the remaining 25% where the cutset is not a single byte, 97% of them are all ASCII.

  18. cespare commented on Aug 25, 2021

    @cespare
    Contributor

    @dsnet what do you think about opening a new issues and reverting the title of this one? I think it's confusing that this was originally about 1-byte cutsets, and then that was implemented, and now the issue is reopened and is about something new.

    At minimum, it should be retitled to be about both optimizations.

  19. changed the title [-]bytes, strings: optimize Trim for multi-byte cutsets[/-] [+]bytes, strings: optimize Trim for ASCII cutsets[/+] on Aug 25, 2021
  20. dsnet commented on Aug 25, 2021

    @dsnet
    MemberAuthor

    I renamed the issue to be about ASCII cutsets, which I think accurately captured both optimizations being discussed.

  21. CAFxX commented on Sep 4, 2021

    @CAFxX
    Contributor

    I just rebased and re-ran the benchmarks in https://golang.org/cl/332771. Would be ideal if someone could take a look at it. It completely eliminates all memory allocations and significantly speeds up both the ASCII fast path and the slow path.

  22. added
    NeedsFixThe path to resolution is known, but the work has not been done.
    and removed
    NeedsDecisionFeedback is required from experts, contributors, and/or the community before a change can be made.
    on Nov 19, 2021
  23. added this to the Go1.18 milestone on Nov 19, 2021
  24. locked and limited conversation to collaborators on Nov 19, 2022
  25. added a commit that references this issue on Apr 29, 2024
    5baf60d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions