-1
\$\begingroup\$

Recently I have answered another question about performance. A solution found using PowerShell-ISE successfully, and good speed testified using PowerShell Core.

However, running the same script(s) in PowerShell 5.1 displays utterly different results, absolutely worse in the performance improvement. Moreover, the original script runs more than 4× slower than in ISE.

So my question is: where lays a reason for such an enormous variance, and what should I avoid in PowerShell scripting (or, conversely, maybe explicitly load a .NET library or something alike)?

PowerShell-ISE: performance improvement cca 10:1 (good):

.\cr\122635wrapper.ps1 -maxLoop 8;$Host.Name;$PSVersionTable
VERBOSE: maxRange=7, lowCountThreshold=1, operators="+-*/"
permutations=4096 formatString="1{0}2{1}3{2}4{3}5{4}6{5}7"
orig.     7       269       756        19,574412
VERBOSE: maxRange=7, lowCountThreshold=1, operators="+-*/"
permutations=4096 formatString="1{5}2{4}3{3}4{2}5{1}6{0}7"
answer    7       284       839         1,911312

Windows PowerShell ISE Host

Name                      Value                  
----                      -----                  
PSVersion                 5.1.17763.503          
PSEdition                 Desktop                
PSCompatibleVersions      {1.0, 2.0, 3.0, 4.0...}
BuildVersion              10.0.17763.503         
CLRVersion                4.0.30319.42000        
WSManStackVersion         3.0                    
PSRemotingProtocolVersion 2.3                    
SerializationVersion      1.1.0.1

PowerShell Core: performance improvement cca 28:1 (excellent):

pwsh -noprofile -command .\cr\122635wrapper.ps1 -maxLoop 8;$Host.Name;$PSVersionTable
VERBOSE: maxRange=7, lowCountThreshold=1, operators="+-*/"
permutations=4096 formatString="1{0}2{1}3{2}4{3}5{4}6{5}7"
orig.     7       269       756       22,0310531
VERBOSE: maxRange=7, lowCountThreshold=1, operators="+-*/"
permutations=4096 formatString="1{5}2{4}3{3}4{2}5{1}6{0}7"
answer    7       284       839        0,7800336

ConsoleHost

Name                           Value
----                           -----
PSVersion                      6.2.0
PSEdition                      Core
GitCommitId                    6.2.0
OS                             Microsoft Windows 10.0.17763
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

PowerShell 5.1: performance improvement cca 4:3 (very poor):

powershell -noprofile -command .\cr\122635wrapper.ps1 -maxLoop 8;$Host.Name;$PSVersionTable
VERBOSE: maxRange=7, lowCountThreshold=1, operators="+-*/"
permutations=4096 formatString="1{0}2{1}3{2}4{3}5{4}6{5}7"
orig.     7       269       756       87,1714765
VERBOSE: maxRange=7, lowCountThreshold=1, operators="+-*/"
permutations=4096 formatString="1{5}2{4}3{3}4{2}5{1}6{0}7"
answer    7       284       839        64,888286

ConsoleHost

Name                           Value
----                           -----
PSVersion                      5.1.17763.503
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.17763.503
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1

Edit

Included code snippets from the link to another question about performance:

122635answer.ps1 script:

# [CmdletBinding(PositionalBinding=$false)] # slows script execution cca 50%
param (     # Variables
    [parameter()]                  # [ValidateRange(3,20)] # ???
    [int]$maxRange = 9,
    [parameter()]
    [int]$lowCountThreshold = 5,
    [parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$opString = '+-*/'     # Mathematical Operators as string
)
Begin {
    Set-StrictMode -Version Latest
    # cast $operators variable as an array of characters
    $operators = [char[]]$opString
    $opsCount  = $operators.Count
    # Define the number range for calculations. 13 would make for the largest values 13!. Cap the script as 13
    $maxRangeMinus1 = $maxRange - 1
    # Build an array for extending 
    $maxOpsArray = 1..$maxRange
    for ( $i=0; $i -lt $maxRange; $i++ ) {
        $maxOpsArray[$maxRangeMinus1 -$i] = ,$operators[0] * $i
    }
    # Build the format string that will be used for invoking.
    # Will look like 1{2}2{1}3{0}4. Acting as place holders for mathematic operators
    [string]$formatString = -join (1..($maxRangeMinus1) | 
        ForEach-Object{"$_{$([int]$maxRangeMinus1 - $_)}"}) + $maxRange # reverse order
      # ForEach-Object{"$_{$([int]$_ - 1)}"}) + $maxRange  # $range[-1] # ascending order
      # ascending order would require `[array]::Reverse($newOperatorArr)` below in the process loop
    if ( $maxRange -gt 11 ) {
        # force decimal computing in following `$DataTable.Compute( $mathString, '')`
        $formatString = $formatString.Replace('{','.0{') + '.0'
    }
    # Determine the number of possible permutations of those operators inbetween the number set. 
    [int64]$permutations = [System.Math]::Pow($opsCount, $maxRangeMinus1)
    # Narrow down $alphanumerics array size to necessary count
    $alphanumerics = $([char[]]'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
                      )[0..($opsCount -1)]
    Write-Verbose -Verbose -Message `
      ("maxRange=$maxRange, lowCountThreshold=$lowCountThreshold, operators=""$(-join $operators)""" `
     + "`r`npermutations=$permutations formatString=""$formatString""")
    $DataTable=[System.Data.DataTable]::new()
    $Error.Clear()                             # for debugging purposes
}
Process {
# Cycle each permutation. Use `for` loop instead of `0..($permutations - 1) | ForEach-Object`
$( for ( $i=0; $i -lt $permutations; $i++ ) {
    # Build an array of operators:
    #        ( based on the number converted to base `$opsCount` )
        $Number = $i
        $newOperatorArr = @( $( do {
            $Remainder = $Number % $opsCount
            # Get the associated character
            $operators[$Remainder]
            $Number = ($Number - $Remainder) / $opsCount
        } while ($Number -gt 0) ))
        # Extend array of operators to appropriate length if necessary
        if ( $newOperatorArr.Count -lt $maxRangeMinus1 ) {
            $newOperatorArr += $maxOpsArray[$newOperatorArr.Count]
        }
    ### [array]::Reverse($newOperatorArr) # only if `$formatString` is in ascending order
    $mathString = $formatString -f @( $newOperatorArr )
    # evaluate math expression using the Compute method of the DataTable class
    #                          rather than time consuming Invoke-Expression
    $value122635 = $DataTable.Compute( $mathString, '')
    # Effectively reduce the output size in advance: refuse "non-integers"
    if ( $value122635 -eq [System.Math]::Floor($value122635) ) {
        # Build an object that contains the result and the mathematical expression 
        [pscustomobject]@{
            Expression = $mathString
            Value      = [System.Math]::Floor($value122635) # [int64]$value122635
        }
    }
    # Since this take a while try and give the user some semblance of progress.
    Write-Progress -Activity "Performing mathematical calculations" `
        -Status "Please wait." -PercentComplete (100 * $i / $permutations) `
        -CurrentOperation "$([math]::Round(100 * $i / $permutations))% Completed."
    # Only give group results
} ) | Group-Object Value |
      Where-Object{$_.Count -ge $lowCountThreshold} |
       Sort-Object -property Count <# -Descending <##>, @{Expression = {[int]$_.Name} }

122635wrapper.ps1 script:

param (
    [parameter()]                  
    [ValidateRange(8,13)]
    [int]$maxLoop = 12
)

$y = (Measure-Command {$x = D:\PShell\CR\122635.ps1}).TotalSeconds
$z = ($x | Measure-Object -Property  Count -Sum).Sum
'orig.  {0,4} {1,9} {2,9} {3,16}' -f 7, $x.count, $z, $y

for ( $icnt=7; $icnt -lt $maxLoop; $icnt++ ) { 
    $y = (Measure-Command {
        $x = D:\PShell\CR\122635answer.ps1 -maxRange $icnt -lowCountThreshold 1
                          }).TotalSeconds
    $z = ($x | Measure-Object -Property  Count -Sum).Sum
    'answer {0,4} {1,9} {2,9} {3,16}' -f $icnt, $x.count, $z, $y
    if ($icnt -eq 7) {''}
}

The original 122635.ps1 script:

function ConvertTo-Base
{
    [CmdletBinding()]
    param (
        [parameter(ValueFromPipeline=$true,Mandatory=$True, HelpMessage="Base10 Integer number to convert to another base")]
        [int]$Number=1000,
        [parameter(Mandatory=$True)]
        [ValidateRange(2,20)]
        [int]$Base
    )
    [char[]]$alphanumerics = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"

    do
    {
        # Determine the remainder
        $Remainder = ($Number % $Base)
        # Get the associated character and add it to the beginning of the string.
        $newBaseValue = "$($alphanumerics[$Remainder])$newBaseValue"
        # Adjust the number to remove the calculated portion
        $Number = ($Number - $Remainder) / $Base
        # Loop until we have processed the whole number
    } while ($Number -gt 0)

    return $newBaseValue
}

# Variables
$maxRange = 3                  #13 would make for the largest values 13!. Cap the script as 13
$lowCountThreshold = 1         # Only show group results where the match exists more than five times. 

# Mathematical Operators 
[char[]]$operators = "+-*/"

# Define the number range for calculations. 13 would make for the largest values 13!. Cap the script as 13
$range = 1..$maxRange

# Build the format string that will be used for invoking. Will look like 1{0}2{1}3. Acting as place holders for mathematic operators
$formatString = -join (1..($range.count - 1) | ForEach-Object{"$_{$([int]$_ - 1)}"}) + $range[-1]

# Determine the number of possible permutations of those operators inbetween the number set. 
$permutations = [System.Math]::Pow($operators.Count,$range.count - 1)

# Cycle each permutation
0..($permutations - 1) | ForEach-Object{
    # Convert the number to a base equal to the element count in operators. Use those values to represent the index of the operators array.
    $mathString = $formatString  -f @([string[]][char[]]((ConvertTo-Base -Number $_ -Base $operators.Count).PadLeft($range.count - 1,"0")) | ForEach-Object{$operators[[int]$_]})
    # Build an object that contains the result and the mathematical expression 
    [pscustomobject]@{
        Expression = $mathString
        Value = Invoke-Expression $mathString      
    }
    # Since this take a while try and give the user some semblance of progress. 
    Write-Progress -Activity "Performing mathematical calculations" -Status "Please wait." -PercentComplete ($_ / $permutations * 100) -CurrentOperation "$([math]::Round($_ / $permutations * 100))% Completed."

    # Filter for whole number and only give group results
} | Where-Object{$_.Value -is [int32]} | Group-Object Value | Where-Object{$_.Count -ge $lowCountThreshold} | Sort-Object Count -Descending
\$\endgroup\$
3
  • \$\begingroup\$ We need the source code... \$\endgroup\$ – t3chb0t May 27 '19 at 6:31
  • \$\begingroup\$ I think putting your source code at the top would be good, but that's a suggestion. \$\endgroup\$ – IEatBagels May 28 '19 at 15:56
  • \$\begingroup\$ The current question title, which states your concerns about the code, is too general to be useful here. Please edit to the site standard, which is for the title to simply state the task accomplished by the code. Please see How to get the best value out of Code Review: Asking Questions for guidance on writing good question titles. \$\endgroup\$ – Toby Speight May 29 '19 at 14:36
2
\$\begingroup\$

Normally to find out what part of your code is slow, you would run it in a profiler. PowerShell however, unfortunately, doesn't have a profiler, so we have to use other means. There are various ways of finding the slow part.

You could put timing code in your code to measure how long the various parts take. The .NET Stopwatch class might be helpful here.

Another easy way is to simply just chop out bits of your code until it speeds up. That's what I tried with your code.

The first thing I tried was to comment out the Write-Progress. That seemed like a likely candidate for being different in the ISE and in the console. When I did that, the code sped up instantly. So that seems to be the culprit.

All you have to do is call it less frequently. You could do something like this:

# Show progress bar. We don't call Write-Progress each time through the loop 
# because that is slow.
if ($i % 1000 -eq 0)
{
    Write-Progress -Activity "Performing mathematical calculations" `
        -Status "Please wait." -PercentComplete (100 * $i / $permutations) `
        -CurrentOperation "$([math]::Round(100 * $i / $permutations))% Completed."
}
\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.