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

Improve contains check done by FastRegexMatcher #14173

Merged

Conversation

pracucci
Copy link
Contributor

@pracucci pracucci commented May 31, 2024

I recently saw a production regexp pattern like (.+)-(.+)-(.+)-(.+)-(.+). Currently, this pattern is loosely optimised in the FastRegexMatcher: we only check if the input string contains 1 occurence of - (in any position) and, if yes, we run the full regexp engine, while if it doesn't we skip the regexp engine.

A simple way to further optimise it is to check for all literals in the pattern instead of just the first one, which is what I'm proposing in this PR.

Benchmark

The benchmark shows some performance change to few patterns that are not expected to change. I've re-run the benchmarks multiple times and they're consistent, which I don't fully understand. I thought it was the change to compileMatchStringFunction() so I've commented the change there (essentially skipping the "contains" check), but I still get such difference with main.

goos: darwin
goarch: arm64
pkg: github.com/prometheus/prometheus/model/labels
                                                     │  before.txt   │              after.txt              │
                                                     │    sec/op     │    sec/op     vs base               │
FastRegexMatcher/#00-11                                 40.86n ±  3%   35.52n ±  1%  -13.07% (p=0.002 n=6)
FastRegexMatcher/foo-11                                 41.51n ± 21%   36.10n ±  0%  -13.04% (p=0.050 n=6)
FastRegexMatcher/^foo-11                                33.90n ±  1%   33.62n ±  1%        ~ (p=0.065 n=6)
FastRegexMatcher/(foo|bar)-11                           47.43n ±  1%   46.74n ±  2%   -1.45% (p=0.041 n=6)
FastRegexMatcher/foo.*-11                               84.12n ±  2%   80.83n ±  4%   -3.92% (p=0.009 n=6)
FastRegexMatcher/.*foo-11                               96.67n ±  3%   96.01n ±  4%        ~ (p=0.699 n=6)
FastRegexMatcher/^.*foo$-11                             98.48n ±  2%   95.20n ±  0%   -3.34% (p=0.002 n=6)
FastRegexMatcher/^.+foo$-11                             98.22n ±  2%   97.08n ±  3%        ~ (p=0.818 n=6)
FastRegexMatcher/.?-11                                  55.68n ±  0%   55.63n ±  0%   -0.09% (p=0.002 n=6)
FastRegexMatcher/.*-11                                  66.39n ±  4%   65.73n ±  6%        ~ (p=0.485 n=6)
FastRegexMatcher/.+-11                                  72.13n ±  4%   70.46n ±  5%        ~ (p=0.394 n=6)
FastRegexMatcher/foo.+-11                               84.08n ±  1%   84.39n ±  2%        ~ (p=1.000 n=6)
FastRegexMatcher/.+foo-11                               97.27n ±  3%   97.39n ±  3%        ~ (p=0.937 n=6)
FastRegexMatcher/foo_.+-11                              70.28n ±  1%   70.11n ±  1%        ~ (p=0.485 n=6)
FastRegexMatcher/foo_.*-11                              71.00n ±  4%   69.97n ±  2%   -1.45% (p=0.026 n=6)
FastRegexMatcher/.*foo.*-11                             175.1n ±  1%   173.2n ±  5%        ~ (p=0.143 n=6)
FastRegexMatcher/.+foo.+-11                             185.0n ±  1%   184.2n ±  2%        ~ (p=0.411 n=6)
FastRegexMatcher/(?s:.*)-11                             35.58n ±  1%   34.82n ±  1%   -2.14% (p=0.002 n=6)
FastRegexMatcher/(?s:.+)-11                             34.48n ±  0%   34.78n ±  1%   +0.87% (p=0.004 n=6)
FastRegexMatcher/(?s:^.*foo$)-11                        96.44n ±  1%   95.79n ±  2%        ~ (p=1.000 n=6)
FastRegexMatcher/(?i:foo)-11                            63.73n ±  1%   63.69n ±  0%        ~ (p=0.327 n=6)
FastRegexMatcher/(?i:(foo|bar))-11                      145.4n ±  1%   145.6n ±  2%        ~ (p=0.219 n=6)
FastRegexMatcher/(?i:(foo1|foo2|bar))-11                239.5n ±  1%   240.2n ±  1%        ~ (p=0.310 n=6)
FastRegexMatcher/^(?i:foo|oo)|(bar)$-11                 642.6n ±  0%   635.7n ±  0%   -1.07% (p=0.002 n=6)
FastRegexMatcher/(?i:(foo1|foo2|aaa|bbb|ccc|ddd|e-11    1.352µ ±  1%   1.470µ ±  1%   +8.73% (p=0.002 n=6)
FastRegexMatcher/((.*)(bar|b|buzz)(.+)|foo)$-11         432.1n ±  1%   443.3n ±  1%   +2.58% (p=0.002 n=6)
FastRegexMatcher/^$-11                                  35.62n ±  0%   34.79n ±  3%   -2.32% (p=0.039 n=6)
FastRegexMatcher/(prometheus|api_prom)_api_v1_.+-11     162.5n ±  2%   162.4n ±  3%        ~ (p=0.671 n=6)
FastRegexMatcher/10\.0\.(1|2)\.+-11                     70.96n ±  0%   70.96n ±  0%        ~ (p=1.000 n=6)
FastRegexMatcher/10\.0\.(1|2).+-11                      70.97n ±  0%   71.01n ±  2%        ~ (p=0.448 n=6)
FastRegexMatcher/((fo(bar))|.+foo)-11                   181.8n ±  0%   181.7n ±  0%        ~ (p=0.210 n=6)
FastRegexMatcher/zQPbMkNO|NNSPdvMi|iWuuSoAl|qbvKM-11    186.9n ±  6%   179.3n ± 11%        ~ (p=0.240 n=6)
FastRegexMatcher/jyyfj00j0061|jyyfj00j0062|jyyfj9-11    180.4n ± 29%   189.7n ±  5%        ~ (p=0.394 n=6)
FastRegexMatcher/(?i:(zQPbMkNO|NNSPdvMi|iWuuSoAl|-11    1.371µ ±  1%   1.473µ ±  1%   +7.48% (p=0.002 n=6)
FastRegexMatcher/(?i:(zQPbMkNO.*|NNSPdvMi.*|iWuuS-11    5.176µ ±  0%   5.179µ ±  1%   +0.06% (p=0.017 n=6)
FastRegexMatcher/(?i:(.*zQPbMkNO|.*NNSPdvMi|.*iWu-11    6.163µ ±  0%   6.151µ ±  0%   -0.19% (p=0.009 n=6)
FastRegexMatcher/fo.?-11                                84.01n ±  0%   85.22n ±  0%   +1.45% (p=0.002 n=6)
FastRegexMatcher/foo.?-11                               83.43n ±  0%   85.21n ±  1%   +2.14% (p=0.002 n=6)
FastRegexMatcher/f.?o-11                                71.54n ±  0%   71.56n ±  1%        ~ (p=0.855 n=6)
FastRegexMatcher/.*foo.?-11                             183.6n ±  0%   185.9n ±  3%        ~ (p=0.067 n=6)
FastRegexMatcher/.?foo.+-11                             173.7n ±  1%   176.1n ±  5%   +1.41% (p=0.009 n=6)
FastRegexMatcher/foo.?|bar-11                           127.0n ±  0%   126.8n ±  1%        ~ (p=0.288 n=6)
FastRegexMatcher/.*-.*-.*-.*-.*-11                     4631.0n ±  2%   178.7n ±  1%  -96.14% (p=0.002 n=6)
FastRegexMatcher/(.+)-(.+)-(.+)-(.+)-(.+)-11           5704.0n ±  0%   179.8n ±  1%  -96.85% (p=0.002 n=6)
FastRegexMatcher/((.*))(?i:f)((.*))o((.*))o((.*))-11    8.558µ ±  0%   4.012µ ±  1%  -53.12% (p=0.002 n=6)
FastRegexMatcher/((.*))f((.*))(?i:o)((.*))o((.*))-11    5.493µ ±  0%   3.264µ ±  1%  -40.59% (p=0.002 n=6)
geomean                                                 183.9n         153.7n        -16.41%

(I omitted memory benchmark results because there's no diff)

@pracucci pracucci force-pushed the fastregexmatcher-optimize-contains branch from 1f8a9b0 to 8846088 Compare June 3, 2024 09:29
pracucci added 3 commits June 4, 2024 10:34
Signed-off-by: Marco Pracucci <marco@pracucci.com>
Signed-off-by: Marco Pracucci <marco@pracucci.com>
Signed-off-by: Marco Pracucci <marco@pracucci.com>
@pracucci pracucci force-pushed the fastregexmatcher-optimize-contains branch from 9981034 to d966ae6 Compare June 4, 2024 08:34
@pracucci pracucci marked this pull request as ready for review June 4, 2024 08:48
@pracucci
Copy link
Contributor Author

pracucci commented Jun 4, 2024

The benchmark shows some performance change to few patterns that are not expected to change. I've re-run the benchmarks multiple times and they're consistent, which I don't fully understand. I thought it was the change to compileMatchStringFunction() so I've commented the change there (essentially skipping the "contains" check), but I still get such difference with main.

@bboreham suggested me to try to run the benchmark with an higher number of strings, so I've done the following change:

@@ -1101,9 +1106,9 @@ func generateRandomValues() []string {
        randGenerator := rand.New(rand.NewSource(1))

        // Generate variable lengths random texts to match against.
-       texts := append([]string{}, randStrings(randGenerator, 10, 10)...)
-       texts = append(texts, randStrings(randGenerator, 5, 30)...)
-       texts = append(texts, randStrings(randGenerator, 1, 100)...)
+       texts := append([]string{}, randStrings(randGenerator, 25000, 10)...)
+       texts = append(texts, randStrings(randGenerator, 25000, 30)...)
+       texts = append(texts, randStrings(randGenerator, 25000, 100)...)
        texts = append(texts, "foo"+randString(randGenerator, 50))
        texts = append(texts, randString(randGenerator, 50)+"foo")

The resulting benchmark is:

goos: darwin
goarch: arm64
pkg: github.com/prometheus/prometheus/model/labels
                                                     │ before-with-more-strings.txt │     after-with-more-strings.txt     │
                                                     │            sec/op            │    sec/op     vs base               │
FastRegexMatcher/#00-11                                                146.1µ ±  1%   145.8µ ±  0%        ~ (p=0.485 n=6)
FastRegexMatcher/foo-11                                                150.6µ ±  0%   149.5µ ±  1%        ~ (p=0.065 n=6)
FastRegexMatcher/^foo-11                                               140.2µ ±  2%   139.2µ ±  1%        ~ (p=0.180 n=6)
FastRegexMatcher/(foo|bar)-11                                          179.4µ ±  0%   173.0µ ±  4%        ~ (p=0.180 n=6)
FastRegexMatcher/foo.*-11                                              298.9µ ±  2%   284.3µ ±  5%   -4.88% (p=0.004 n=6)
FastRegexMatcher/.*foo-11                                              354.9µ ±  2%   342.7µ ±  4%        ~ (p=0.093 n=6)
FastRegexMatcher/^.*foo$-11                                            353.2µ ±  2%   347.7µ ±  3%        ~ (p=0.394 n=6)
FastRegexMatcher/^.+foo$-11                                            354.1µ ±  1%   340.5µ ±  5%   -3.82% (p=0.026 n=6)
FastRegexMatcher/.?-11                                                 213.7µ ±  0%   213.8µ ±  0%        ~ (p=0.310 n=6)
FastRegexMatcher/.*-11                                                 298.6µ ±  2%   298.2µ ±  1%        ~ (p=0.310 n=6)
FastRegexMatcher/.+-11                                                 315.0µ ±  1%   314.3µ ±  0%        ~ (p=0.485 n=6)
FastRegexMatcher/foo.+-11                                              299.1µ ±  0%   296.6µ ±  2%        ~ (p=0.093 n=6)
FastRegexMatcher/.+foo-11                                              358.7µ ±  0%   352.1µ ±  2%        ~ (p=0.065 n=6)
FastRegexMatcher/foo_.+-11                                             278.9µ ±  0%   276.1µ ±  1%   -1.02% (p=0.002 n=6)
FastRegexMatcher/foo_.*-11                                             278.9µ ±  0%   278.6µ ±  1%        ~ (p=0.485 n=6)
FastRegexMatcher/.*foo.*-11                                            1.238m ±  2%   1.239m ±  2%        ~ (p=0.589 n=6)
FastRegexMatcher/.+foo.+-11                                            1.225m ±  2%   1.239m ±  1%        ~ (p=0.065 n=6)
FastRegexMatcher/(?s:.*)-11                                            144.0µ ±  1%   143.7µ ±  1%        ~ (p=0.589 n=6)
FastRegexMatcher/(?s:.+)-11                                            144.9µ ±  0%   145.2µ ±  1%   +0.18% (p=0.015 n=6)
FastRegexMatcher/(?s:^.*foo$)-11                                       358.6µ ±  0%   353.2µ ±  2%   -1.50% (p=0.026 n=6)
FastRegexMatcher/(?i:foo)-11                                           365.9µ ±  1%   369.5µ ±  1%   +0.98% (p=0.002 n=6)
FastRegexMatcher/(?i:(foo|bar))-11                                     711.0µ ±  0%   708.9µ ±  0%   -0.30% (p=0.002 n=6)
FastRegexMatcher/(?i:(foo1|foo2|bar))-11                               1.122m ±  0%   1.119m ±  1%        ~ (p=0.180 n=6)
FastRegexMatcher/^(?i:foo|oo)|(bar)$-11                                2.870m ±  1%   2.837m ±  1%   -1.12% (p=0.002 n=6)
FastRegexMatcher/(?i:(foo1|foo2|aaa|bbb|ccc|ddd|e-11                   17.21m ±  3%   17.70m ±  2%   +2.86% (p=0.015 n=6)
FastRegexMatcher/((.*)(bar|b|buzz)(.+)|foo)$-11                        2.553m ±  1%   2.601m ±  1%   +1.84% (p=0.002 n=6)
FastRegexMatcher/^$-11                                                 144.2µ ±  1%   144.4µ ±  0%        ~ (p=0.485 n=6)
FastRegexMatcher/(prometheus|api_prom)_api_v1_.+-11                    1.124m ±  1%   1.171m ±  4%   +4.19% (p=0.015 n=6)
FastRegexMatcher/10\.0\.(1|2)\.+-11                                    279.1µ ±  0%   279.1µ ±  0%        ~ (p=0.818 n=6)
FastRegexMatcher/10\.0\.(1|2).+-11                                     279.1µ ±  0%   279.2µ ±  0%        ~ (p=0.331 n=6)
FastRegexMatcher/((fo(bar))|.+foo)-11                                  698.5µ ±  0%   700.1µ ±  0%        ~ (p=0.065 n=6)
FastRegexMatcher/zQPbMkNO|NNSPdvMi|iWuuSoAl|qbvKM-11                   842.1µ ± 13%   876.1µ ± 16%        ~ (p=0.699 n=6)
FastRegexMatcher/jyyfj00j0061|jyyfj00j0062|jyyfj9-11                   825.3µ ±  3%   822.2µ ±  3%        ~ (p=0.699 n=6)
FastRegexMatcher/(?i:(zQPbMkNO|NNSPdvMi|iWuuSoAl|-11                   17.37m ±  1%   17.95m ±  1%   +3.35% (p=0.002 n=6)
FastRegexMatcher/(?i:(zQPbMkNO.*|NNSPdvMi.*|iWuuS-11                   22.00m ±  0%   22.02m ±  0%        ~ (p=0.132 n=6)
FastRegexMatcher/(?i:(.*zQPbMkNO|.*NNSPdvMi|.*iWu-11                   25.50m ±  0%   25.52m ±  0%   +0.12% (p=0.026 n=6)
FastRegexMatcher/fo.?-11                                               299.3µ ±  0%   299.0µ ±  0%   -0.11% (p=0.004 n=6)
FastRegexMatcher/foo.?-11                                              299.2µ ±  0%   298.9µ ±  0%   -0.10% (p=0.002 n=6)
FastRegexMatcher/f.?o-11                                               271.6µ ±  0%   271.2µ ±  0%   -0.15% (p=0.004 n=6)
FastRegexMatcher/.*foo.?-11                                            1.235m ±  2%   1.261m ±  2%        ~ (p=0.093 n=6)
FastRegexMatcher/.?foo.+-11                                            1.234m ±  1%   1.265m ± 94%   +2.51% (p=0.026 n=6)
FastRegexMatcher/foo.?|bar-11                                          481.1µ ±  0%   489.1µ ± 81%   +1.66% (p=0.002 n=6)
FastRegexMatcher/.*-.*-.*-.*-.*-11                                    50.472m ±  0%   6.762m ±  0%  -86.60% (p=0.002 n=6)
FastRegexMatcher/(.+)-(.+)-(.+)-(.+)-(.+)-11                          63.138m ±  0%   8.020m ±  0%  -87.30% (p=0.002 n=6)
FastRegexMatcher/((.*))(?i:f)((.*))o((.*))o((.*))-11                   77.30m ±  1%   40.63m ±  0%  -47.44% (p=0.002 n=6)
FastRegexMatcher/((.*))f((.*))(?i:o)((.*))o((.*))-11                   73.22m ±  1%   41.37m ±  0%  -43.50% (p=0.002 n=6)
geomean                                                                904.5µ         805.9µ        -10.90%

We still have some variance, for different cases now. The variance is smaller than previous benchmark run tho.

@bboreham
Copy link
Member

bboreham commented Jun 4, 2024

To explain my thinking: previously the test used 18 strings, total about 456 bytes. This is small enough that most branches could fit in the prediction cache of the cpu, and adding one more branch could tip that change. It's unrealistically small compared to any real-world Prometheus.

Increasing to 75,000 strings and 3.5MB makes it unlikely the CPU can predict every branch, and pushes out from L1 to L2/L3 cache.

Copy link
Member

@bboreham bboreham left a comment

Choose a reason for hiding this comment

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

Code changes seem straightforward enough. Thanks!

@bboreham bboreham merged commit 6fb738a into prometheus:main Jun 6, 2024
24 checks passed
@pracucci pracucci deleted the fastregexmatcher-optimize-contains branch June 6, 2024 17:01
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.

2 participants