GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — gh-130167: Improve speed of `difflib.IS_LINE_JUNK` by replacing `re` by donbarbos · Pull Request #130170 · python/cpython

Conversation

@donbarbos

@donbarbos donbarbos commented Feb 16, 2025 •
edited by bedevere-app Bot
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

Now we don't import of re globally and there is no longer an extra (undocumented) function parameter (see documentation)

How did i benchmark a new version?

I wrote next python script difflib_bench.py:

import re
import timeit

def is_line_junk_regex(line, pat=re.compile(r"\s*(?:#\s*)?$").match):
    return pat(line) is not None

def is_line_junk_no_regex(line):
    return line.strip() == "" or line.lstrip().rstrip() == "#"

test_cases = [
    "    ", "    #", "   # comment", "code line", " 123 ", " 123 #",
    " 123 # hi", " 123 #comment", "\n", "  #   \n", "hello\n", "", " ", "\t",
    "#", " #", "# ", "   #   ", "text", "text # comment", "#text", "##", "#\t",
    "   ", " #\t ",
]

def benchmark(func, cases, n=100000):
    return timeit.timeit(lambda: [func(line) for line in cases], number=n)

regex_time = benchmark(is_line_junk_regex, test_cases)
no_regex_time = benchmark(is_line_junk_no_regex, test_cases)

print(f"Regex time: {regex_time:.6f} sec")
print(f"No regex time: {no_regex_time:.6f} sec")

timeit result: 1.33s -> 0.64s = x2.08 as fast

$ ./python -B difflib_bench.py
Regex time: 1.330593 sec
No regex time: 0.638559 sec
  • Issue: Improve speed of stdlib functions by replacing re uses #130167

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@donbarbos
Improve speed of difflib.IS_LINE_JUNK by replacing re
8f2670d
@bedevere-app bedevere-app Bot added the awaiting review label Feb 16, 2025
@bedevere-app bedevere-app Bot mentioned this pull request Feb 16, 2025
Improve speed of stdlib functions by replacing re uses #130167
Open
AA-Turner
AA-Turner reviewed Feb 16, 2025
View reviewed changes
Comment thread Lib/difflib.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Lib/difflib.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

donbarbos and others added 2 commits February 16, 2025 07:20
@donbarbos @AA-Turner
Update difflib.py
0fae9c5
Co-authored-by: Adam Turner <[email protected]>
@donbarbos @AA-Turner
Update difflib.py
352b4bc
Co-authored-by: Adam Turner <[email protected]>
AA-Turner
AA-Turner approved these changes Feb 16, 2025
View reviewed changes
@bedevere-app bedevere-app Bot added awaiting merge and removed awaiting review labels Feb 16, 2025
terryjreedy
terryjreedy requested changes Feb 16, 2025
View reviewed changes

@terryjreedy terryjreedy left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

One oddity of difflib is that re is imported at module level for IS_LINE_JUNK and again at class level in class _mdiff line 1378. It is used in line 1381 to define class name change_re = re.compile(r'(\++|\-+|\^+)'). To avoid importing re when importing difflib, the second import would have to be moved within a function. This is possible since change_re is only used within the _make_line method the immediately follows (1386).

I have not looked at how commonly this method is used.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread Lib/difflib.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

@bedevere-app bedevere-app Bot added awaiting changes and removed awaiting merge labels Feb 16, 2025
@bedevere-app

bedevere-app Bot commented Feb 16, 2025

Copy link
Copy Markdown

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@terryjreedy

terryjreedy commented Feb 16, 2025

Copy link
Copy Markdown
Member

I checked correctness of the return expression replacement, using test_cases above, with

for s in test_cases:
    (pat(s) is not None) is (s.strip() in ('', '#'))

and all are True.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@donbarbos
Add backward compatibility
0ab8da8
@donbarbos

donbarbos commented Feb 16, 2025

Copy link
Copy Markdown
Contributor Author

I checked correctness of the return expression replacement, using test_cases above, with

Ok, thank you, but this test_cases is too small a sample and therefore I have supplemented my script with the following checks:

import string
import random
from itertools import product

# First Checks:
for case in test_cases:
    assert is_line_junk_regex(case) == is_line_junk_no_regex(case), f"Inconsistent behavior for {repr(case)}"

def random_string(length=10):
    chars = string.printable + " " * 5 + "#" * 5
    return "".join(random.choice(chars) for _ in range(length))

# Second Checks:
for _ in range(10000):
    case = random_string(random.randint(0, 20))
    assert is_line_junk_regex(case) == is_line_junk_no_regex(case), f"Inconsistent behavior for {repr(case)}"

# Third Checks:
chars = " #\tabc"
for length in range(6):
    for case in map("".join, product(chars, repeat=length)):
        assert is_line_junk_regex(case) == is_line_junk_no_regex(case), f"Inconsistent behavior for {repr(case)}"

Then I checked the tests in Lib/test/test_difflib.py and everything finished well 👍

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@donbarbos

donbarbos commented Feb 16, 2025

Copy link
Copy Markdown
Contributor Author

I have made the requested changes; please review again

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@bedevere-app bedevere-app Bot added awaiting change review and removed awaiting changes labels Feb 16, 2025
@bedevere-app

bedevere-app Bot commented Feb 16, 2025

Copy link
Copy Markdown

Thanks for making the requested changes!

@terryjreedy, @AA-Turner: please review the changes made to this pull request.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@bedevere-app
bedevere-app Bot requested review from AA-Turner and terryjreedy February 16, 2025 11:52
sobolevn
sobolevn reviewed Feb 16, 2025
View reviewed changes
Comment thread Lib/difflib.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Lib/difflib.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

picnixz
picnixz reviewed Feb 16, 2025
View reviewed changes
Comment thread Lib/difflib.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

@donbarbos
Update difflib.py
64dff4a
@donbarbos
donbarbos requested a review from picnixz February 16, 2025 14:48
picnixz
picnixz reviewed Feb 16, 2025
View reviewed changes

@picnixz picnixz left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Since we're improving performance, we don't want to recompute line.strip()

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread Lib/difflib.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

@donbarbos @picnixz
Update Lib/difflib.py
32337c3
Co-authored-by: Bénédikt Tran <[email protected]>
@donbarbos

donbarbos commented Feb 16, 2025

Copy link
Copy Markdown
Contributor Author

Since we're improving performance, we don't want to recompute line.strip()

Sorry, I suspected this but hoped that there was a cache mechanism here :)

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

serhiy-storchaka
serhiy-storchaka reviewed Feb 16, 2025
View reviewed changes

@serhiy-storchaka serhiy-storchaka left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

I am not sure that the performance of this function is worth changing the code. In any case, the relative performance depends on the input -- for a long string with a single space character at one end (e.g. 'x'*10000+' ') the non-regex version is more than 10 times slower than the regex version. For length 100000 the difference is more than 100, and for length 1000000 -- more than 1000.

Hmm, it seems, that that implementation has more than quadratic complexity. Of course, most data for which this function is used is short strings, but such behavior can be considered a security threat.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread Lib/difflib.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

@terryjreedy

terryjreedy commented Feb 16, 2025 •
edited by hugovk
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

I checked correctness of the return expression replacement, using test_cases above, with

for s in test_cases:
    (pat(s) is not None) is (s.strip() in ('', '#'))

and all are True.

I am going to be neutral an whether the basic change should be merged. If it is, I personally think the 'pat' parameter can be considered to be private and eliminated. But I would defer the back-compatibility to others, include 'encukou'. The only documented use of the function, marked as a constant, is to be passed as an argument. The argument is always passed on to SequenceMatcher, which calls it once with 1 argument, which would be a line.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@donbarbos

donbarbos commented Feb 16, 2025 •
edited by hugovk
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor Author

@serhiy-storchaka

In any case, the relative performance depends on the input -- for a long string with a single space character at one end (e.g. 'x'*10000+' ') the non-regex version is more than 10 times slower than the regex version. For length 100000 the difference is more than 100, and for length 1000000 -- more than 1000.

I tested your cases but didn't get the result you expected (maybe I did something wrong?)
I ran next script (accidentally mixed up the dividend and divisor, but it doesn't matter):

import re
import timeit

def is_line_junk_regex(line, pat=re.compile(r"\s*(?:#\s*)?$").match):
    return pat(line) is not None

def is_line_junk_no_regex(line):
    return line.strip() == "" or line.lstrip().rstrip() == "#"

def benchmark(func, cases, n=1_000):
    return timeit.timeit(lambda: [func(line) for line in cases], number=n)

lengths = [10_000, 100_000, 1_000_000]
for length in lengths:
    test_string = "x" * length + " "
    regex_time = benchmark(is_line_junk_regex, test_string)
    no_regex_time = benchmark(is_line_junk_no_regex, test_string)

    print(f"String length: {length}")
    print(f"Regex version: {regex_time:.6f} seconds")
    print(f"Non-regex version: {no_regex_time:.6f} seconds")
    print(f"Speedup: {no_regex_time / regex_time:.2f}x\n")

and got next results:

$ ./python -B bench.py
String length: 10000
Regex version: 4.220341 seconds
Non-regex version: 1.983180 seconds
Speedup: 0.47x

String length: 100000
Regex version: 42.698992 seconds
Non-regex version: 20.897327 seconds
Speedup: 0.49x

String length: 1000000
Regex version: 516.844790 seconds
Non-regex version: 262.632697 seconds
Speedup: 0.51x
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@picnixz

picnixz commented Feb 16, 2025 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

Aren't you iterating over a single character instead of a full line:

[func(line) for line in cases]

You should pass [test_string] instead of just test_string (sorry if I'm wrong, I'm tired)

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@AA-Turner

AA-Turner commented Feb 16, 2025

Copy link
Copy Markdown
Member

I was the one who originally reqested restoring pat, but I do find Terry's argument compelling, and I wouldn't oppose removing pat.

I think we should restore this PR to the simpler previous version (the differences micro-benchmarks between string comparisons are so small they will be swallowed by noise). The class-level re import should be resolved before merge, though.

A

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@donbarbos @hugovk
Update difflib.py
390b6e7
Co-authored-by: Hugo van Kemenade <[email protected]>
serhiy-storchaka
serhiy-storchaka reviewed Feb 17, 2025
View reviewed changes

@serhiy-storchaka serhiy-storchaka left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

I was wrong, the complexity is not quadratic, but linear.

Anyway, the relative performance depends on input. For some input the regexp version is faster, for other it is slower. Claiming that this improves speed for any input is bold.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread Lib/difflib.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Misc/NEWS.d/next/Library/2025-02-16-06-25-01.gh-issue-130167.kUg7Rc.rst Resolved

Uh oh!

There was an error while loading. Please reload this page.

@donbarbos
Back iff word
30d4535
@rhettinger
rhettinger requested a review from tim-one February 18, 2025 18:16
@rhettinger

rhettinger commented Feb 18, 2025

Copy link
Copy Markdown
Contributor

@tim-one Do you have a reason to want to keep the regex?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@tim-one

tim-one commented Feb 18, 2025

Copy link
Copy Markdown
Member

Can't say I much care, but I don't get it. What's the point? It's dubious on the face of it to build an entirely new string object just to do simple lexical analysis. If this is some "micro-optimizztion" thing, instead of

        stripped = line.strip()
        return stripped == '' or stripped == '#'

try

        return line.strip() in '#' # empty or single '#'

?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@donbarbos

donbarbos commented Feb 18, 2025 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor Author

Can't say I much care, but I don't get it. What's the point? It's dubious on the face of it to build an entirely new string object just to do simple lexical analysis. If this is some "micro-optimizztion" thing, instead of

        stripped = line.strip()

        return stripped == '' or stripped == '#'

try

        return line.strip() in '#' # empty or single '#'

?

that's how it was originally :)

I'll probably return it so as not to overcomplicate things

tim-one reacted with laugh emoji
All reactions
  • 😄 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@donbarbos
Update difflib
0e7293d
tim-one
tim-one reviewed Feb 18, 2025
View reviewed changes
Comment thread Lib/difflib.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

@donbarbos @tim-one
Update difflib.py
bcf45a3
Co-authored-by: Tim Peters <[email protected]>
@donbarbos

donbarbos commented Mar 8, 2025

Copy link
Copy Markdown
Contributor Author

@rhettinger

@tim-one Do you have a reason to want to keep the regex?

Tim doesn't seem to mind

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@donbarbos
Fix News message
46a4126
@donbarbos

donbarbos commented Apr 27, 2025

Copy link
Copy Markdown
Contributor Author

I don't know who I can ping but it seems like there are more suggestions

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

hugovk
hugovk approved these changes Apr 28, 2025
View reviewed changes
@bedevere-app bedevere-app Bot added awaiting merge and removed awaiting change review labels Apr 28, 2025
AA-Turner
AA-Turner reviewed May 1, 2025
View reviewed changes
Comment thread Lib/difflib.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

@AA-Turner
Improve clarity of expression
60951fb
AA-Turner
AA-Turner reviewed May 1, 2025
View reviewed changes
Comment thread Lib/difflib.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

@AA-Turner
Revert, but add explanatory comments
9d987b4
AA-Turner
AA-Turner approved these changes May 1, 2025
View reviewed changes
@AA-Turner
AA-Turner enabled auto-merge (squash) May 1, 2025 03:48
@AA-Turner
AA-Turner merged commit bce45bc into python:main May 1, 2025
@bedevere-app bedevere-app Bot removed the awaiting merge label May 1, 2025
@donbarbos donbarbos mentioned this pull request Jul 26, 2025
[difflib] Update pat param for IS_LINE_JUNK python/typeshed#14449
Merged
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@eendebakpt eendebakpt eendebakpt left review comments
@serhiy-storchaka serhiy-storchaka serhiy-storchaka left review comments
@sobolevn sobolevn sobolevn left review comments
@AA-Turner AA-Turner AA-Turner approved these changes
@picnixz picnixz picnixz left review comments
@hugovk hugovk hugovk approved these changes
@terryjreedy terryjreedy Awaiting requested review from terryjreedy
+1 more reviewer
@tim-one tim-one tim-one left review comments
Reviewers whose approvals may not affect merge requirements

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

10 participants

@donbarbos @terryjreedy @picnixz @AA-Turner @rhettinger @tim-one @eendebakpt @hugovk @serhiy-storchaka @sobolevn
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.