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

perf(pycodestyle): Reduce allocations when computing logical lines #3715

Merged
merged 2 commits into from
Mar 28, 2023

Conversation

MichaReiser
Copy link
Member

@MichaReiser MichaReiser commented Mar 24, 2023

This PR refactors the logical lines computation that the pycodestyle rules use from using O(lines) allocations to O(1).

The current implementation allocates for every line:

  • a String for the line's text
  • a Vec for the logical-line text offset to Location mappings
  • a Vec for the line's token

This PR reduces the allocations by allocating a single String for the text, two Vec's for the mappings and tokens, and one Vec for storing the line-metadata. The implementation slices into the shared text, mappings, and tokens structures to get the view for a single line.

Allocating the data structures for all lines has the advantage of reserving the containers with the right capacity because we know the number of tokens. This avoids the guessing of the old implementation that used some more-or-less arbitrary numbers for the tokens-capacity.

Performance

  • no-logical: Logical lines feature disabled
  • base-logical: Logical lines feature enabled on main
  • pr3715: This branch
group                                      base-logical                           no-logical                             pr3715
-----                                      ------------                           ----------                             ------
linter/all-rules/large/dataset.py          1.15      9.8±0.02ms     4.2 MB/sec    1.00      8.5±0.01ms     4.8 MB/sec    1.10      9.4±0.15ms     4.3 MB/sec
linter/all-rules/numpy/ctypeslib.py        1.14      2.4±0.04ms     6.8 MB/sec    1.00      2.1±0.01ms     7.8 MB/sec    1.15      2.5±0.00ms     6.8 MB/sec
linter/all-rules/numpy/globals.py          1.21    301.1±1.97µs     9.8 MB/sec    1.00    247.9±3.25µs    11.9 MB/sec    1.16    286.9±3.46µs    10.3 MB/sec
linter/all-rules/pydantic/types.py         1.19      4.4±0.03ms     5.8 MB/sec    1.00      3.7±0.04ms     7.0 MB/sec    1.19      4.4±0.02ms     5.9 MB/sec
linter/default-rules/large/dataset.py      1.28      6.0±0.08ms     6.8 MB/sec    1.00      4.7±0.01ms     8.7 MB/sec    1.24      5.8±0.08ms     7.0 MB/sec
linter/default-rules/numpy/ctypeslib.py    1.32   1317.7±3.83µs    12.6 MB/sec    1.00    999.5±5.57µs    16.7 MB/sec    1.31  1304.4±16.73µs    12.8 MB/sec
linter/default-rules/numpy/globals.py      1.44    145.7±1.14µs    20.3 MB/sec    1.00    101.0±0.41µs    29.2 MB/sec    1.44    145.7±1.31µs    20.2 MB/sec
linter/default-rules/pydantic/types.py     1.32      2.8±0.04ms     9.0 MB/sec    1.00      2.1±0.01ms    11.9 MB/sec    1.26      2.7±0.03ms     9.5 MB/sec

This change improves performance overall but the pycodestyle still add a significant overhead.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 24, 2023

PR Check Results

Ecosystem

✅ ecosystem check detected no changes.

Benchmark

Linux

group                                      main                                   pr
-----                                      ----                                   --
linter/all-rules/large/dataset.py          1.05     19.9±0.89ms     2.0 MB/sec    1.00     18.9±0.51ms     2.1 MB/sec
linter/all-rules/numpy/ctypeslib.py        1.01      5.0±0.28ms     3.3 MB/sec    1.00      4.9±0.25ms     3.4 MB/sec
linter/all-rules/numpy/globals.py          1.05   661.1±28.12µs     4.5 MB/sec    1.00   631.3±34.91µs     4.7 MB/sec
linter/all-rules/pydantic/types.py         1.05      8.8±0.45ms     2.9 MB/sec    1.00      8.3±0.44ms     3.1 MB/sec
linter/default-rules/large/dataset.py      1.02     10.2±0.32ms     4.0 MB/sec    1.00      9.9±0.28ms     4.1 MB/sec
linter/default-rules/numpy/ctypeslib.py    1.03      2.2±0.08ms     7.7 MB/sec    1.00      2.1±0.12ms     7.9 MB/sec
linter/default-rules/numpy/globals.py      1.00    257.8±8.21µs    11.4 MB/sec    1.02   262.7±13.87µs    11.2 MB/sec
linter/default-rules/pydantic/types.py     1.02      4.7±0.12ms     5.4 MB/sec    1.00      4.6±0.23ms     5.6 MB/sec

Windows

group                                      main                                   pr
-----                                      ----                                   --
linter/all-rules/large/dataset.py          1.01     15.7±0.22ms     2.6 MB/sec    1.00     15.6±0.22ms     2.6 MB/sec
linter/all-rules/numpy/ctypeslib.py        1.01      4.2±0.05ms     4.0 MB/sec    1.00      4.2±0.05ms     4.0 MB/sec
linter/all-rules/numpy/globals.py          1.00    535.8±7.10µs     5.5 MB/sec    1.01    538.5±5.45µs     5.5 MB/sec
linter/all-rules/pydantic/types.py         1.01      6.9±0.19ms     3.7 MB/sec    1.00      6.8±0.13ms     3.8 MB/sec
linter/default-rules/large/dataset.py      1.00      8.3±0.09ms     4.9 MB/sec    1.00      8.3±0.10ms     4.9 MB/sec
linter/default-rules/numpy/ctypeslib.py    1.00  1842.1±29.10µs     9.0 MB/sec    1.01  1851.6±17.13µs     9.0 MB/sec
linter/default-rules/numpy/globals.py      1.00    201.5±2.79µs    14.6 MB/sec    1.00    201.3±5.35µs    14.7 MB/sec
linter/default-rules/pydantic/types.py     1.00      3.9±0.05ms     6.6 MB/sec    1.00      3.9±0.05ms     6.6 MB/sec

Base automatically changed from unicode-width to main March 24, 2023 21:17
@MichaReiser MichaReiser force-pushed the logical-lines-perf-improvements branch 2 times, most recently from f0fff40 to f86cb60 Compare March 26, 2023 00:29
@MichaReiser MichaReiser changed the title Logical lines performance improvements perf(pycodestyle): Reduce allocations when computing logical lines Mar 26, 2023
@MichaReiser MichaReiser force-pushed the logical-lines-perf-improvements branch from f86cb60 to 2c8fbb7 Compare March 26, 2023 10:29
@@ -106,61 +358,54 @@ fn build_line<'a>(
|| ((prev_text != "{" && prev_text != "[" && prev_text != "(")
&& (text != "}" && text != "]" && text != ")"))
{
logical.push(' ');
length += 1;
Copy link
Member Author

Choose a reason for hiding this comment

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

We can use text.len() to compute the offsets.

@MichaReiser MichaReiser force-pushed the logical-lines-perf-improvements branch from 2c8fbb7 to 58b2a7a Compare March 26, 2023 18:34
@MichaReiser MichaReiser marked this pull request as ready for review March 26, 2023 18:35
@MichaReiser MichaReiser force-pushed the logical-lines-perf-improvements branch from 58b2a7a to 8a08a96 Compare March 27, 2023 06:27
@konstin konstin self-requested a review March 27, 2023 12:44
Copy link
Member

@charliermarsh charliermarsh left a comment

Choose a reason for hiding this comment

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

This is stellar, I feel like I'm learning a lot even just from reading through your code here.

if is_keyword_token(tok) {
flags.insert(TokenFlags::KEYWORD);
if is_keyword_token(token) {
line.flags.insert(TokenFlags::KEYWORD);
}

// TODO(charlie): "Mute" strings.
Copy link
Member

Choose a reason for hiding this comment

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

I don't know if this TODO is still relevant, it looks like we are "muting" strings.

Copy link
Member Author

Choose a reason for hiding this comment

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

It is necessary until we remove the regex handling (I think I commented the reason why it exists in one of the upper PRs)

Copy link
Member

Choose a reason for hiding this comment

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

But, is this comment still relevant? We already do "mute" strings. So isn't the TODO resolved?

Copy link
Member Author

@MichaReiser MichaReiser Mar 27, 2023

Choose a reason for hiding this comment

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

It is still necessary (at least for this PR). I understood this is a TODO to avoid the muting :D. I'll leave it there because it will be gone soon

Tok::Rbrace | Tok::Rpar | Tok::Rsqb => {
parens -= 1;
}
Tok::Newline | Tok::NonLogicalNewline | Tok::Comment(_) if parens == 0 => {
Copy link
Member

Choose a reason for hiding this comment

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

for my own understanding, does this mean that the comment consumes the newline after it?

Copy link
Member Author

@MichaReiser MichaReiser Mar 27, 2023

Choose a reason for hiding this comment

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

I think so... I don't really know to be honest. I only moved this code around.

I remember that I tried removing the comment branch because I assumed that it isn't needed because there should be a new line token after, finishing the line. But some tests started failing. It may be worth looking into this again on the top branch.

Yep, it seems to be the case that there's no newline token after Comment token:

def f():
  """Docstring goes here."""
  # Comment goes here.
  x = 1
f()"#;

Generates the following lines without the Tok::Comment

[
	"def f():",
	""""Docstring goes here."""", 
	"x = 1", 
	"f()"
]

Where the comment is missing (because comments don't get written to the text string)

@MichaReiser MichaReiser force-pushed the logical-lines-perf-improvements branch from 8a08a96 to f62f750 Compare March 27, 2023 21:08
@MichaReiser MichaReiser force-pushed the logical-lines-perf-improvements branch from f62f750 to 7389522 Compare March 28, 2023 06:45
@MichaReiser MichaReiser merged commit 113a8b8 into main Mar 28, 2023
@MichaReiser MichaReiser deleted the logical-lines-perf-improvements branch March 28, 2023 07:09
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.

3 participants