Some functions of difflib.SequenceMatcher perform very poorly in real-world scenarios (for example get_opcodes() takes several minutes to return a list of tuples for two identical files that are 70kb long). This is reported in different places online, such as on StackOverflow.
It would be nice to add a note about the performance in the doc, with possible workarounds (for example there is a great performance improvement if one opens the files with readlines() and compares two lists of lines, instead of opening files with read() and compare two strings).
changed the title [-]A warning should be added to difflib about SequenceMatcher performance[/-][+]A warning should be added to difflib documentation about SequenceMatcher performance[/+]on Jul 18, 2023
Hi, could you maybe give an example (both the file and the code) for the issue? Maybe there are some low hanging fruits. I can't repro the several minutes result with 60k identical files.
Thanks for your reply. Maybe this can be me misunderstanding the use of autojunk, actually? I am writing a program that aims to generate deltas from successive versions of files, so I wanted to use SequenceMatcher.get_opcodes() to return me the sequence of instructions needed to turn one version of the file to the next one. In the documentation it is written that "junk" characters are ignored and that autojunk enables a heuristic that will automatically ignore popular characters. So to ensure the tool would return all possible changes and me being able to generate my files from the history of changes, I disabled the heuristic by setting it to false. But this comes at a huge performance cost.
I wrote a test code below that shows the difference in performance between setting autojunk to true or false.
"""SequenceMatcher test when autojunk is false or true"""importdifflib,timet1=time.time()
withopen("loremipsum.txt", "r") asf:
dat1=f.read()
# autojunk is false match=difflib.SequenceMatcher(a=dat1, b=dat1, autojunk=False)
res=match.get_opcodes()
t2=time.time()
print("It roughly took %.1f seconds to run get_opcodes with autojunk set to False."%(t2-t1))
# autojunk is true match=difflib.SequenceMatcher(a=dat1, b=dat1, autojunk=True)
res=match.get_opcodes()
t3=time.time()
print("It roughly took %.1f seconds to run get_opcodes with autojunk set to True."%(t3-t2))
It was intended to compare text files by lines, not by characters. But, as you've discovered, if you do it by characters anyway, it can be enormously expensive unless autojunk is enabled. That's why autojunk was introduced. Although:
It was more intended to avoid sync'ing on common "noise lines", like else: and # in Python source code compared by lines;, and,
It was a Bad Mistake to enable it by default. It can give surprising results.
In your example, display match.bpopular after creating the match object with autojunk enabled. You'll discover that all the characters in ",acdegilmnoprstuv" are excluded from being considered as synchronization points. As the docs say, that's because each of those characters accounts for at least 1% of the total characters in the sequence. They're "common as mud".
In fact, only 19 distinct characters aren't excluded:
Of course it runs enormously faster to look only at the rarest characters.
The opcodes produced should be "correct" either way. autojunk, and explicitly defining your own isjunk function, are ways to influence the "quality" of the diffs produced, and the time it takes to get them. not their correctness. Quality is subjective. The opcodes produced will change the first sequence to the second regardless.
At an extreme, tell the class that everything "is junk" (pass lambda ch: True as the isjunk= argument). It will run very fast then, but you may be left hoping for a "smarter" diff 😉. The utmost extreme: a single opcode that says "replace the entire first sequence with the entire second sequence." That's always correct. regardless of the inputs.
But notions of "junk" aren't just about speed. They can also be about quality. With no notion of junk, you can get diffs that sync up on what you consider to be regions of white noise; with a too-expansive notion of junk, diffs approaching the "utmost extreme" above.
The docs were updated in #118150. They now start off with text lines as the intended use case, then mention SequenceMatcher as the generalization, then a section on junk which includes "this heuristic can speed the algorithm up".
I think that's enough for this issue, so I'll close. If anyone disagrees, shout :)
Activity
gaogaotiantian commented on Jul 18, 2023
Hi, could you maybe give an example (both the file and the code) for the issue? Maybe there are some low hanging fruits. I can't repro the several minutes result with 60k identical files.
maathieu commented on Jul 20, 2023
Hi,
Thanks for your reply. Maybe this can be me misunderstanding the use of autojunk, actually? I am writing a program that aims to generate deltas from successive versions of files, so I wanted to use SequenceMatcher.get_opcodes() to return me the sequence of instructions needed to turn one version of the file to the next one. In the documentation it is written that "junk" characters are ignored and that autojunk enables a heuristic that will automatically ignore popular characters. So to ensure the tool would return all possible changes and me being able to generate my files from the history of changes, I disabled the heuristic by setting it to false. But this comes at a huge performance cost.
I wrote a test code below that shows the difference in performance between setting autojunk to true or false.
loremipsum.txt
tim-one commented on Jul 20, 2023
It was intended to compare text files by lines, not by characters. But, as you've discovered, if you do it by characters anyway, it can be enormously expensive unless autojunk is enabled. That's why autojunk was introduced. Although:
In your example, display match.bpopular after creating the match object with autojunk enabled. You'll discover that all the characters in ",acdegilmnoprstuv" are excluded from being considered as synchronization points. As the docs say, that's because each of those characters accounts for at least 1% of the total characters in the sequence. They're "common as mud".
In fact, only 19 distinct characters aren't excluded:
Of course it runs enormously faster to look only at the rarest characters.
The opcodes produced should be "correct" either way. autojunk, and explicitly defining your own isjunk function, are ways to influence the "quality" of the diffs produced, and the time it takes to get them. not their correctness. Quality is subjective. The opcodes produced will change the first sequence to the second regardless.
At an extreme, tell the class that everything "is junk" (pass lambda ch: True as the isjunk= argument). It will run very fast then, but you may be left hoping for a "smarter" diff 😉. The utmost extreme: a single opcode that says "replace the entire first sequence with the entire second sequence." That's always correct. regardless of the inputs.
But notions of "junk" aren't just about speed. They can also be about quality. With no notion of junk, you can get diffs that sync up on what you consider to be regions of white noise; with a too-expansive notion of junk, diffs approaching the "utmost extreme" above.
maathieu commented on Jul 20, 2023
Thanks, this is much more clear explained this way. Is it possible to consider including a similar explanation in the documentation?
(more specifically that autojunk does not prevent get_opcodes() from returning a functional list of differences)
encukou commented on Sep 7, 2026
The docs were updated in #118150. They now start off with text lines as the intended use case, then mention SequenceMatcher as the generalization, then a section on junk which includes "this heuristic can speed the algorithm up".
I think that's enough for this issue, so I'll close. If anyone disagrees, shout :)