Skip to content

Improve contains check done by FastRegexMatcher#14173

Merged
bboreham merged 3 commits intoprometheus:mainfrom
pracucci:fastregexmatcher-optimize-contains
Jun 6, 2024
Merged

Improve contains check done by FastRegexMatcher#14173
bboreham merged 3 commits intoprometheus:mainfrom
pracucci:fastregexmatcher-optimize-contains

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
@pracucci pracucci deleted the fastregexmatcher-optimize-contains branch June 6, 2024 17:01
bboreham pushed a commit that referenced this pull request Jan 8, 2026
…17707)

#14173 introduced an optimisation to better handle regex patterns like .*-.*-.*. It identifies strings the pattern cannot possibly match (because they do not contain all of the literal values) and returns false from MatchString early.

However, if the string does contain all literal values, then the Go regex engine is used to confirm that the string does match the pattern. But this is not necessary in the case where the start and end of the pattern is .* and everything in between is either a literal or .*: if the string contains all of the literals in order, then it matches the pattern, and invoking Go's regex engine to confirm this is unnecessary and quite slow.

* Add some more test cases
* Add benchmark, since existing benchmark doesn't show much impact given most of the random test strings will not match the patterns.

Signed-off-by: Charles Korn <charles.korn@grafana.com>
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