Last Update: Sep 04, 2024 | Published: Mar 24, 2015
As you work with PowerShell, and especially when you begin developing scripts, you’ll realize there may be several ways to achieve the same result. How do you decide which is the better option? My first criteria is that the best choice best takes advantage of the PowerShell paradigm.
For example, you might have started with code like this, which is modeled after the way we would have used VBScript.
Ostensibly it works. But this isn't really good PowerShell. This a better approach to writing our PowerShell.
The result is even formatted nicely. This is the first test. Unfortunately there really isn't a quantitative way to determine that the latter approach is better. This comes from experience. So let's assume that as you are working you have two equally valid and good PowerShell solutions to a task. Which is better? For me, the next test is legibility. That is, how easy is it to understand and follow what is happening in your code? Which makes more sense to you? Which is easier for someone else to understand? Again, this is a subjective measure. The final standard, at least for me, is performance. Normally I don't worry too much if one approach is a few milliseconds faster than another. Usually you can get a sense simply by running the different commands. But the best way is to actually measure the command expression and see how long each takes. Here are some simple examples. The task is to check the status of a list of services on a remote computer. Being new to PowerShell, you might come up with code like this:
It works just fine. Probably not the best PowerShell because Get-Service can accept an array of service names, which means this also works.
But let's find out which one truly is faster. We will do this with the Measure-Command cmdlet. Typically you use the command like this:
PowerShell runs the scriptblock,, in this case getting values from 1 to 10,000, and measures how long it takes. The result is a TimeSpan object. As you can see from the screenshot it took almost four seconds to complete. Note that you don't see any results from the command itself. So let's test our two service commands.
Here's the result: Now for the other command.
Faster, but not as much as I thought. And in this case, there may be networking or server performance issues affecting the outcome. Additionally, depending on the commands you are testing, there may be a caching effect which could skew repeated tests. So while the results are meaningful you should take them in context. That said, I might get better information if I test the expression several times and take the average.
Using a for loop, I measure how long it takes to run my scriptblock, saving the results to $Totals. To mitigate any caching or other issues, I'm sleeping for 10 seconds between each test. When finished, I can then calculate the average of the TotalMilliseconds property. Now to test the other:
So the second approach is faster and better PowerShell. I like the idea of testing over a number of runs an reporting an average so much I created a PowerShell function to make it easier. I call it Test-Expression.
Function Test-Expression { <# .SYNOPSIS Test a PowerShell expression. .DESCRIPTION This command will test a PowerShell expression or scriptblock for a specified number of times and calculate the average runtime, in milliseconds, over all the tests. .PARAMETER Expression The scriptblock you want to test. This parameter has an alias of sb. .PARAMETER ArgumentList An array of parameters to pass to the scriptblock. Arguments are positional. .PARAMETER Count The number of times to test the scriptblock. .PARAMETER Interval How much time to sleep in seconds between each test. Maximum is 60. You may want to use a sleep interval to mitigate possible caching effects. .PARAMETER AsJob Run the tests as a background job. .EXAMPLE PS C:> Test-Expression {param($cred) get-wmiobject win32_logicaldisk -computer chi-dc01 -credential $cred } -argumentList $cred Tests : 1 TestInterval : 0.5 AverageMS : 146.9054 MinimumMS : 146.9054 MaximumMS : 146.9054 Test a command once passing an argument to the scriptblock. .EXAMPLE PS C:> $sb = {1..1000 | foreach {$_*2}} PS C:> test-expression $sb -count 10 -interval 2 Tests : 10 TestInterval : 2 AverageMS : 24.78196 MinimumMS : 21.0534 MaximumMS : 36.2801 PS C:> $sb2 = { foreach ($i in (1..1000)) {$_*2}} PS C:> test-expression $sb2 -Count 10 -interval 2 Tests : 10 TestInterval : 2 AverageMS : 2.62559 MinimumMS : 0.4883 MaximumMS : 15.1753 These examples are testing two different approaches that yield the same results over a span of 10 test runs, pausing for 2 seconds between each test. The values for Average, Minimum and Maximum are in milliseconds. .NOTES NAME : Test-Expression VERSION : 1.0 LAST UPDATED: 2/13/2015 AUTHOR : Jeff Hicks Learn more about PowerShell:Essential PowerShell Learning Resources**************************************************************** * DO NOT USE IN A PRODUCTION ENVIRONMENT UNTIL YOU HAVE TESTED * * THOROUGHLY IN A LAB ENVIRONMENT. USE AT YOUR OWN RISK. IF * * YOU DO NOT UNDERSTAND WHAT THIS SCRIPT DOES OR HOW IT WORKS, * * DO NOT USE IT OUTSIDE OF A SECURE, TEST SETTING. * **************************************************************** .LINK Measure-Command Measure-Object .INPUTS None .OUTPUTS Custom measurement object #> [cmdletbinding()] Param( [Parameter(Position=0,Mandatory,HelpMessage="Enter a scriptblock to test")] [Alias("sb")] [scriptblock]$Expression, [object[]]$ArgumentList, [ValidateScript({$_ -ge 1})] [int]$Count=1, [ValidateRange(0,60)] [double]$Interval = .5, [switch]$AsJob ) Write-Verbose "Measuring expression:" Write-Verbose ($Expression | Out-String) write-Verbose "$Count time(s) with a sleep interval of $($interval*1000) milliseconds" <# define an internal scriptblock that can be used directly or used to create a background job #> $myScriptBlock = { 1..$using:count | foreach -begin { <# PowerShell doesn't seem to like passing a scriptblock as an argument when using Invoke-Command. It appears to pass it as a string so I'm recreating it as a scriptblock here. #> $script:testblock = [scriptblock]::Create($using:Expression) } -process { #invoke the scriptblock with any arguments and measure Measure-Command -Expression { Invoke-Command -ScriptBlock $script:testblock -ArgumentList @($using:ArgumentList) } #pause to mitigate any caching effects Start-Sleep -Milliseconds ($using:Interval*1000) } | Measure-Object -Property TotalMilliseconds -Average -Maximum -Minimum | Select-Object -Property @{Name="Tests";Expression={$_.Count}}, @{Name="TestInterval";Expression={$using:Interval}}, @{Name="AverageMS";Expression={$_.Average}}, @{Name="MinimumMS";Expression={$_.Minimum}}, @{Name="MaximumMS";Expression={$_.Maximum}} } #myScriptBlock #parameter hashtable to splat against Invoke-Command $paramHash = @{ ScriptBlock = $myScriptBlock ComputerName = $env:computername HideComputerName = $True } If ($AsJob) { Write-Verbose "Running as a background job" $paramHash.Add("AsJob",$True) } #exclude RunspaceID where possible Invoke-Command @paramHash | Select-Object -property * -ExcludeProperty RunspaceID } #end function
The premise is essentially what I have just demonstrated, although the implementation is a bit advanced. This should make it easier to test and compare PowerShell commands. You can specify the number of times to test and how many seconds to sleep between tests. The following tests produce the same result but one is significantly faster than the other.
Compared to this:
And finally this example using new syntax in PowerShell 4.0.
You might have other requirements that make one of these more attractive than the others but at least in terms of measurable performance you can make an informed decision.