PowerShell Performance Part 1, Introduction:

Comparatively speaking, PowerShell’s concise and flexible syntax translates to faster development, but usually not faster programs.  From its earliest days, one of PowerShell’s most annoying drawbacks is its comparatively poor performance.

I typically start script projects as a purist, leveraging native functionality, cmdlets, pipes, where clauses etc.  All the stuff that makes PowerShell code easy to write. Frequently the elation of quickly building a working prototype is overshadowed by unsatisfactory performance, and what follows is a time-consuming effort to refactor the code.  There may be better workflows to implement, but the issues usually boil down to PowerShell itself.  In turn, this forces a regressive break from the purist approach. Mining for performance results in a proliferation of increasingly exotic techniques within the code.

Refactoring is rather unfortunate because it erodes some of PowerShell’s eloquence.

Hazards of Refactoring for Performance (An incomplete list for sure):

  1. The resulting time deficit is the biggest problem, encompassing the additional effort to improve performance and generally more difficult maintenance.
  2. Code can become less readable.  Resulting code can end up so far from PoSh norms that it’s difficult even for the author to revisit.  Comments can help, but it’s difficult to relay the reasoning behind hard fought code.
  3. It’s hard to foresee at the outset, but you may only yield so much improvement.  Hence you can get lured into a time consuming yet relatively fruitless effort

I’ve spent a lot of time on these types of problems; in fact, I’m a little obsessed with maximizing performance.  This is the first in a series of posts about various tips & tricks I use to make things a bit faster.

It goes without saying that to analyze performance we need a method of measuring it.  PowerShell provides the cmdlet Measure-Command for this.  Measure-Command makes it easy to gauge the performance of almost any block of code.  Here I’m going to focus on simple commands, but keep in mind you can do this on much bigger script blocks.  So if you’re testing a different code pattern as opposed to a simple command, Measure-Command is still quite useful.

Measure-Command { Get-Process Explorer }
Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 0
Milliseconds      : 8
Ticks             : 81151
TotalDays         : 9.39247685185185E-08
TotalHours        : 2.25419444444444E-06
TotalMinutes      : 0.000135251666666667
TotalSeconds      : 0.0081151
TotalMilliseconds : 8.1151

Performance usually becomes a factor when processing large collections of data, where a relatively small performance difference can accumulate into a sub-optimal run-duration.  As such we’ll often be comparing granular pieces of code but this isn’t as straight forward as Measure-Command would have us believe.  One thing to consider is the reliability of the measurements.  After exalting the awesomeness of Measure-Command, this seems like a stupid question, but there are circumstances where inconsistent results can be presented.  This should be easy to address; repeat the tests and compare averages.  In other words, use a larger sample size.  Nevertheless, there are some anomalies I’ve never been comfortable with.

To get a larger sample size you can wrap a particular test in a loop, something like:

For($i = 1; $i -le 20; ++$i)
{ ( Measure-Command { Get-Service AtherosSvc } ).TotalMilliseconds }

I ran this test 3 times, but the results were questionable:

Note: Just to make sure, I repeated this with several different common cmdlets & sessions:

  1. Get-Service
  2. Get-Process
  3. Get-ChildItem
  4. Write-Host

Obviously something isn’t right here.  The first run is way slower than the others.  A few days ago at MS Ignite 2019, I spoke with PowerShell team members Jason Helmick & Tyler Leonhardt.  They confirmed my long-held suspicion that this is due to a caching mechanism.  That’s probably a good feature, but something to be aware of when you’re measuring performance.

This doesn’t really invalidate the test either.  If you are comparing 2 different commands generally the first run of each will be predictive enough.  However, I always track the results over some number of executions.  Furthermore, this appears to be specific to the cmdlet (or more likely the underlying .Net classes) not the command. Hence if you run Get-Service Service_1, then Get-Service Service_2, the second command should show the faster result.

As of yet, I’m not sure how long the cmdlet stays cached but considering this is a performance scenario centered on repetition it’s not a big concern.  Granted this lends itself to some ambiguity, but I’ll, try to resolve that in a later post.

Additional Guidelines I use when Evaluating Performance:

  1. Don’t look at performance in isolation.  Whether we’re talking about PowerShell or anything else. Performance is based on a variety of environmental factors including CPU disk, and memory, which are important to consider.
  2.  Always consider expected runtime conditions.  Say you choose a faster but more memory intensive piece of code.  If you then run it on a memory constrained system you may get unexpected results.
  3. Stay current!  The PowerShell team is aware of performance issues and they’ve been steadily improving the native cmdlets.  This is especially true since PowerShell went open source. So, if you’ve been using a faster technique on an older version, you may want to recheck it.  It’s possible whatever issue has been resolved, and your code can remain a little more pure.
  4. Think before you refactor.  Just because something is faster doesn’t mean you should use it.  You have to decide the specific tradeoffs.
  5.  Always re-evaluate performance after implementation.  Sometimes despite our best efforts we still miss the mark.  Of course that could mean anything, but don’t draw conclusions based purely on pre-testing.  Besides, it’s always a bit more satisfying to demonstrate improvements in the final product.

This first post on driving PowerShell performance should establish a reasonable, albeit informal framework for testing and validating such improvements.

As always, I’d love to get some feedback.  Comment, click follow or grab the RSS feed to get notifications of future posts.