Skip to content

[unicode-PERF]: use optmized BM algorithm to replace the brute-force finder - #8626

Merged
sklam merged 2 commits into
numba:mainfrom
dlee992:da/fast_search
Dec 16, 2022
Merged

sklam merged 2 commits into
numba:mainfrom
dlee992:da/fast_search

Conversation

@dlee992

@dlee992 dlee992 commented Nov 30, 2022

Copy link
Copy Markdown
Contributor

Hi, after some digging, I found cpython has at least 3 ways to tackle string-matching operations, based on some metrics on needle and haystack lengths:

  1. find_char: use memchr as possible as we can, which using hand-tuned assembly code with vectorized instructions;
  2. default_find: use Boyer-Moore algorithm with Horspool (O(1) space) and Sunday (bloom filter) optimizations (merged in 2006);
  3. _two_way_find: a more advanced but longer preprocessing algorithm (merged in 2021).

Indeed, I haven't known how to implement a vectorized find for char search (any idea? I would like to contribute, but don't know exactly the details), so right now I just implemented the second algorithm for numba default string find underlying impl.

I haven't perf the speedup ratio, but choose to trust cpython's decision. If needed, I could make up some perf tests to show how much reduced comparsions w/ this PR.

PS, during the development, I found numba doesn't support python reversed keyword, is this owing to some numba limitations? Or we just didn't have time to implement it?

@dlee992

dlee992 commented Dec 7, 2022

Copy link
Copy Markdown
Contributor Author

FYI,

  1. this is the python blog in 2006, describing how they implemented the exact BM algorithm in that period of Python: https://web.archive.org/web/20201107074620/http://effbot.org/zone/stringlib.htm
  2. this is about all kinds of string fast matching algorithms: http://www-igm.univ-mlv.fr/~lecroq/string/index.html

@guilhermeleobas guilhermeleobas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hi @dlee992, thanks for working on this. I've done an initial review, but the code looks pretty similar to the one implemented in CPython.

Question: Have you measure the time using the new algorithm vs the older implementation?

Comment thread numba/cpython/unicode.py Outdated
Comment thread numba/cpython/unicode.py Outdated
Comment thread numba/cpython/unicode.py
Comment thread numba/cpython/unicode.py Outdated
@dlee992

dlee992 commented Dec 9, 2022 •

Copy link
Copy Markdown
Contributor Author

but the code looks pretty similar to the one implemented in CPython.

Yeah. I did copy that piece of CPython code and modify based on it, and I add the code link in the comments. I think CPython impl should be a best practice, and Numba UnicodeType is also imitating the CPython UnicodeObject, so I think I don't need to re-design this algorithm if we don't have specific Numba-related issues to tackle.

Question: Have you measure the time using the new algorithm vs the older implementation?

No, not yet. I can work on that, make up some test cases to see the time difference. Will update it here when the perf test is ready.

@guilhermeleobas guilhermeleobas added 4 - Waiting on author Waiting for author to respond to review and removed 3 - Ready for Review labels Dec 9, 2022
@dlee992

dlee992 commented Dec 12, 2022 •

Copy link
Copy Markdown
Contributor Author

Here's a made-up test case, copied from the method test_find in test_unicode.py, and add a very long test case in it. U can put this as a method in the class UnicodeTest in test_unicode.py and rerun this.

Details
    def test_find_perf(self):
        pyfunc = find_usecase
        cfunc = njit(pyfunc)

        # pre-run cfunc
        cfunc("abc", "a")

        default_subs = [
            (s, ['', 'xx', s[:-2], s[3:], s]) for s in UNICODE_EXAMPLES
        ]

        cpython_subs = [
            ('a' * 100 + '\u0102', ['\u0102', '\u0201', '\u0120', '\u0220']),
            ('a' * 100 + '\U00100304', ['\U00100304', '\U00100204',
                                        '\U00102004']),
            ('\u0102' * 100 + 'a', ['a']),
            ('\U00100304' * 100 + 'a', ['a']),
            ('\U00100304' * 100 + '\u0102', ['\u0102']),
            ('a' * 100, ['\u0102', '\U00100304', 'a\u0102', 'a\U00100304']),
            ('\u0102' * 100, ['\U00100304', '\u0102\U00100304']),
            ('\u0102' * 100 + 'a_', ['a_']),
            ('\U00100304' * 100 + 'a_', ['a_']),
            ('\U00100304' * 100 + '\u0102_', ['\u0102_']),
        ]

        start_time = time.time()
        for _ in range(1000):
            for s, subs in default_subs + cpython_subs:
                for sub_str in subs:
                    cfunc(s, sub_str)
        print(f"consumed time in short test cases: {time.time() - start_time}")

        # made-up cases for fast search
        for leng in [10, 100, 1000, 10000]:
            s = "abcdefg" * leng + "hijk"
            sub_str = "hijk"
            start_time = time.time()
            for _ in range(1000):
                cfunc(s, sub_str)
            print(f"consumed time with len={len(s)}: {time.time() - start_time}")

Consumed time in Numba 0.56.3:

consumed time in short test cases: 0.19890618324279785
consumed time with len=74: 0.003802061080932617
consumed time with len=704: 0.006798982620239258
consumed time with len=7004: 0.03879284858703613
consumed time with len=70004: 0.44359922409057617

Consumed time in Numba main with this PR:

total time consumed: 0.18944501876831055
consumed time with len=74: 0.004054069519042969
consumed time with len=704: 0.003863811492919922
consumed time with len=7004: 0.007459163665771484
consumed time with len=70004: 0.04593682289123535

In short test cases, consumed time are similar (perhaps a minor degradation with this PR, but marginal, I think).
When putting the sub_str in the end of a very long s, the acceleration ratio is 1.76, 5.2, and 9.66, which is nice.

cc @guilhermeleobas

@dlee992
dlee992 requested review from guilhermeleobas and removed request for sklam and stuartarchibald December 12, 2022 08:29
@guilhermeleobas

Copy link
Copy Markdown
Contributor

When putting the sub_str in the end of a very long s, the acceleration ratio is 1.76, 5.2, and 9.66, which is nice.

Can you share the source code for these?

@dlee992

dlee992 commented Dec 13, 2022

Copy link
Copy Markdown
Contributor Author

Can you share the source code for these?

I have already shared the code, u can unfold the Details button in #8626 (comment), then u will see the source code!

@guilhermeleobas

@guilhermeleobas

Copy link
Copy Markdown
Contributor

Oh, sorry. I'll check it tomorrow morning.

@guilhermeleobas guilhermeleobas added 4 - Waiting on reviewer Waiting for reviewer to respond to author and removed 4 - Waiting on author Waiting for author to respond to review labels Dec 13, 2022
@guilhermeleobas

Copy link
Copy Markdown
Contributor

Thanks @dlee992. I ran the code and your approach is way faster than the old one:

# your branch
consumed time in short test cases: 0.1025538444519043
consumed time with len=74: 0.0019850730895996094
consumed time with len=704: 0.002277851104736328
consumed time with len=7004: 0.006390094757080078
consumed time with len=70004: 0.04852104187011719
consumed time with len=700004: 0.45890283584594727
consumed time with len=7000004: 4.588933944702148

# Numba main
consumed time in short test cases: 0.10606002807617188
consumed time with len=74: 0.002021312713623047
consumed time with len=704: 0.003590106964111328
consumed time with len=7004: 0.01938009262084961
consumed time with len=70004: 0.17744994163513184
consumed time with len=700004: 1.744581937789917
consumed time with len=7000004: 17.453103065490723

@guilhermeleobas guilhermeleobas added 5 - Ready to merge Review and testing done, is ready to merge 4 - Waiting on second reviewer Patch needs a second reviewer. and removed 4 - Waiting on reviewer Waiting for reviewer to respond to author 5 - Ready to merge Review and testing done, is ready to merge 4 - Waiting on second reviewer Patch needs a second reviewer. labels Dec 13, 2022
@sklam
sklam merged commit 8fc20e2 into numba:main Dec 16, 2022
@dlee992
dlee992 deleted the da/fast_search branch December 17, 2022 00:55
@dlee992

dlee992 commented Jan 28, 2023

Copy link
Copy Markdown
Contributor Author

Hi, @guilhermeleobas and @sklam

Since no obvious side effects with this PR, I feel this one can also be put into Numba 0.57 RC if possible.

@guilhermeleobas guilhermeleobas added this to the Numba 0.57 RC milestone Jan 31, 2023
@guilhermeleobas

Copy link
Copy Markdown
Contributor

Yes! I just tagged to 0.57

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

5 - Ready to merge Review and testing done, is ready to merge Effort - medium Medium size effort needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants