Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PrettyPrinterPerformance Optimized the PrettyPrinter for #894 #901

Merged
merged 2 commits into from
Dec 19, 2024

Conversation

macshome
Copy link
Contributor

Worked to get the perfomance to be closer to where we were before the changes in #883. This code should be only about 1.5% slower rather than 7% slower.

Pre-changes: Instructions executed: 67167400341
After #883: Instructions executed: 71260352299
This PR: Instructions executed: 68259258073

Fixes #894.

@ahoppen
Copy link
Member

ahoppen commented Dec 15, 2024

Thank you, @macshome, that's a great improvement.

@ahoppen ahoppen enabled auto-merge December 15, 2024 21:35
@ahoppen
Copy link
Member

ahoppen commented Dec 15, 2024

Looks like this fails to build using Swift 5.10. Maybe the where label for count(where:) is required? Could you check the failure?

@allevato
Copy link
Member

Unfortunately, count(where:) didn't actually land until Swift 6.0: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0220-count-where.md

It's probably fine here to just write the loop manually instead of using that API. If you simultaneously keep track of the latest \n index every time through the loop, then you can do everything in a single forward scan and avoid a separate call to lastIndex afterwards, which should shave off a few more instructions. Every bit helps in this critical path.

@macshome
Copy link
Contributor Author

Unfortunately, count(where:) didn't actually land until Swift 6.0: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0220-count-where.md

It's probably fine here to just write the loop manually instead of using that API. If you simultaneously keep track of the latest \n index every time through the loop, then you can do everything in a single forward scan and avoid a separate call to lastIndex afterwards, which should shave off a few more instructions. Every bit helps in this critical path.

Will do. This explains why it was failing on Godbolt when including count(where:_) for me as well.

auto-merge was automatically disabled December 17, 2024 14:41

Head branch was pushed to by a user without write access

@macshome macshome force-pushed the PrettyPrinterPerformance branch from 557d19f to a0f588d Compare December 17, 2024 14:41
@macshome
Copy link
Contributor Author

macshome commented Dec 17, 2024

I added a quick fix that is Swift 5 compatible and isn't too bad a hit on the performance.

let lines = text.lazy.filter({ $0 == "\n" }).count

I thought I had found a good solution using:

text.enumerateLines {line, stop in
    column = line.count
    lineNumber += 1
}

but all the enumerate API options are escaping and this is a struct. I looked at the implementation of enumerateLines but enumerateSubstrings is escaping as well.

I can keep looking to shave off more instructions with one forward pass if you like.

@macshome macshome force-pushed the PrettyPrinterPerformance branch from a0f588d to f540f02 Compare December 17, 2024 15:24
@macshome
Copy link
Contributor Author

Shaved a few more instructions by finding the last newline in reverse.

let lastNewlineRange = text.range(of: "\n", options: .backwards)

@macshome
Copy link
Contributor Author

I find it interesting that the text.range(of: "\n", options: .backwards) option has less instructions executed, even though it actually has 3X more assembly instructions than the lastNewlineIndex = text.lastIndex(of: "\n") version.

https://godbolt.org/z/v9xjnq4qb

I guess that the benefit of reading the text corpus backwards makes up for it.

@allevato
Copy link
Member

lastIndex should also be reading the string backwards, since String conforms to BidirectionalCollection.

When you're looking at the instruction counts, are you getting those by running swift-format on a Mac or a Linux machine? Godbolt.org only has Linux machines AFAIK (at least for Swift), so if you're looking at Godbolt output there, you could be comparing something vastly different than what you would see on macOS. And even on recent versions of macOS, they've been porting Foundation from the old Obj-C implementation to the new Swift core, so the code you're executing could even vary significantly between versions there, too.

Ultimately, I think this is still doing unnecessary work (either approach); in my original comment, I suggested having a single loop that counts the number of "\n"s and simultaneously keeps track of the index of the last one that it's seen, rather than doing a separate counting operation and then a backwards scan. This should be entirely possible to do in just one sequential pass over the string, which should it get it closest to the original performance.

@allevato
Copy link
Member

Another optimization would be to iterate over the .utf8 view of the string instead of the string directly. Since all we care about here is finding the number and locations of the "\n" characters, we don't need to pay the cost of any of the grapheme clustering that comes along with Character-based APIs.

@macshome
Copy link
Contributor Author

lastIndex should also be reading the string backwards, since String conforms to BidirectionalCollection.

When you're looking at the instruction counts, are you getting those by running swift-format on a Mac or a Linux machine? Godbolt.org only has Linux machines AFAIK (at least for Swift), so if you're looking at Godbolt output there, you could be comparing something vastly different than what you would see on macOS. And even on recent versions of macOS, they've been porting Foundation from the old Obj-C implementation to the new Swift core, so the code you're executing could even vary significantly between versions there, too.

Ultimately, I think this is still doing unnecessary work (either approach); in my original comment, I suggested having a single loop that counts the number of "\n"s and simultaneously keeps track of the index of the last one that it's seen, rather than doing a separate counting operation and then a backwards scan. This should be entirely possible to do in just one sequential pass over the string, which should it get it closest to the original performance.

Good point. I'll check the assembly on my Mac as well when working on additional optimizations.

Thanks!

@macshome
Copy link
Contributor Author

macshome commented Dec 18, 2024

(Edit: I see that lineNumber is based off of 1 now, but my general question remains.)

I do have a question that isn't about the optimization work: should we be counting newlines or lines?

This string:

let text = """
This is some text
it has a lot 
of
line

breaks.
"""

has 5 "\n" but 6 lines. In general there is always one more line than there is line breaks. If I try adding an additional line to the count of "\n" a bunch of tests will fail.

Is the code handling this conversion of newlines to actual lines somewhere else so that we don't need to worry about it here?

@macshome
Copy link
Contributor Author

I've pretty much got this down to one forward pass that equals the original instruction count now. Just adding in the last tweaks.

…mance PrettyPrinterPerformance Optimized the PrettyPrinter for swiftlang#894

Worked to get the perfomance to be closer to where we were before the changes in swiftlang#883. This code should be only about 1.5% slower rather than 7% slower.

Using a lazy filter as `count(where:_)` isn't avaliable < Swift 6.0.

One forward loop and using the UTF8 view makes this faster than the original code pre-swiftlang#883.
@macshome macshome force-pushed the PrettyPrinterPerformance branch from f540f02 to 87467ee Compare December 18, 2024 22:54
@macshome
Copy link
Contributor Author

@ahoppen @allevato

Between making this one forward loop, and using the .utf8 view, the benchmark now reports that it is faster than it was pre-#883.

Instructions executed: 66824319169

Copy link
Member

@ahoppen ahoppen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing work for beating the original baseline! 🚀

Co-authored-by: Alex Hoppen <alex@alexhoppen.de>
@ahoppen ahoppen enabled auto-merge December 19, 2024 14:10
Copy link
Member

@allevato allevato left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great, thanks for fixing the regression!

@ahoppen ahoppen merged commit f334bb3 into swiftlang:main Dec 19, 2024
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

#883 regressed performance by ~7%
3 participants