Regex performance

Viewed 549

I am benchmarking different approaches to RegEx and seeing something I really don't understand. I am specifically comparing using the -match operator vs using the [regex]::Matches() accelerator. I started with

(Measure-Command {
    foreach ($i in 1..10000) {
        $path -match $testPattern
    }
}).TotalSeconds
(Measure-Command {
    foreach ($i in 1..10000) {
        [regex]::Matches($path, $testPattern)
    }
}).TotalSeconds

and -match is always very slightly faster. But it's also not apples to apples because I need to assign the [Regex] results to a variable to use it. So I added that

(Measure-Command {
    foreach ($i in 1..10000) {
        $path -match $testPattern
    }
}).TotalSeconds
(Measure-Command {
    foreach ($i in 1..10000) {
        $test = [regex]::Matches($path, $testPattern)
    }
}).TotalSeconds

And now [Regex] is consistently slightly faster, which makes no sense because I added to the workload with the variable assignment. The performance difference is ignorable, 1/100th of a second when doing 10,000 matches, but I wonder what is going on under the hood to make [Regex] faster when there is a variable assignment involved?

For what it's worth, without the variable assignment -match is faster, .05 seconds vs .03 seconds. With variable assignment [Regex] is faster by .03 seconds vs .02 seconds. So while it IS all negligible, adding the variable cuts [Regex] processing time more than in half, which is a (relatively) huge delta.

2 Answers

The outputs of both tests are different. The accelerator output a lot more text.

Even though they are not displayed when wrapped in the Measure-Command cmdlet, they are part of the calculation.

Output of $path -match $testPattern

$true

Output of [regex]::Matches($path,$testPattern


Groups   : {0}
Success  : True
Name     : 0
Captures : {0}
Index    : 0
Length   : 0
Value    :

Writing stuff is slow. In your second example, you take care of the accelerator output by assigning it to a variable. That's why it is significantly faster.

You can see the difference without assignment by voiding the outputs If you do that, you'll see the accelerator is consistently slightly faster.


(Measure-Command {
        foreach ($i in 1..10000) {
            [void]($path -match $testPattern)
        }
    }).TotalSeconds
(Measure-Command {
        foreach ($i in 1..10000) {
            [void]([regex]::Matches($path, $testPattern))
        }
    }).TotalSeconds

Additional note

void is always more efficient than Command | Out-null. Pipeline is slower but memory efficient.

This isn't an answer to the direct question asked, but it's an expansion on the performance of pre-compiled regexes that I mentioned in comments...

First, here's my local performance benchmark for the original code in the question for comparison (with some borrowed text and patterns):

$text    = "foo" * 1e6;
$pattern = "f?(o)";
$count   = 1000000;

# example 1
(Measure-Command {
    foreach ($i in 1..$count) {
        $text -match $pattern
    }
}).TotalSeconds
# 8.010825

# example 2
(Measure-Command {
    foreach ($i in 1..$count) {
        $result = [regex]::Matches($text, $pattern)
    }
}).TotalSeconds
# 6.8186813

And then using a pre-compiled regex, which according to Compilation and Reuse in Regular Expressions emits a native assembly to process the regex rather than the default "sequence of internal instructions" - whatever that actually means :-).

$text    = "foo" * 1e6;
$pattern = "f?(o)";
$count   = 1000000;

# example 3
$regex = [regex]::new($pattern, "Compiled");
(Measure-Command {
    foreach ($i in 1..$count) {
       $result = $regex.Matches($text)
    }
}).TotalSeconds
# 5.8794981

# example 4
(Measure-Command {
    $regex = [regex]::new($pattern, "Compiled");
    foreach ($i in 1..$count) {
       $result = $regex.Matches($text)
    }
}).TotalSeconds
# 3.6616832

# example 5
# see https://github.com/PowerShell/PowerShell/issues/8976
(Measure-Command {
    & {
        $regex = [regex]::new($pattern, "Compiled");
        foreach ($i in 1..$count) {
            $result = $regex.Matches($text);
        }
    }
}).TotalSeconds
# 1.5474028

Note that Example 3 has a performance overhead of finding / resolving the $regex variable from inside each iteration because it's defined outside the Measure-Command's -Expresssion scriptblock - see https://github.com/PowerShell/PowerShell/issues/8976 for details.

Example 5 defines the variable inside a nested scriptblock and so is a lot faster. I'm not sure why Example 4 sits in between the two in performance, but it's useful to note there's a definite difference :-)

Also, as an aside, in my comments above, my original version of Example 5 didn't have the &, which meant I was timing the effort required to define the scriptblock, not execute it, so my numbers were way off. In practice, the performance increase is a lot less than my comment suggested, but it's still a decent improvement if you're executing millions of matches in a tight loop...

Related