Search code examples
powershellperformanceinvoke

What is the best way to reuse static code


As defined in the PowerShell scripting performance considerations document, repeatedly calling a function can be an expensive operation. Yet the concerned function (or just code) might be (re)used at several locations in a script, which leaves a dilemma:

  • Should I create a quiet expensive function for a easier manageable DRY code, or
  • Should I go for performance and copy the concerned piece of code block at several locations?

Especially if the concerned code block is inexpensive but very verbose.

Use case example

Sticking with the performance goal, it quiet known that using a hashtable as a lookup table could make quiet a difference. For this you will usually need to the define each key at the point you create the lookup table and where you would (try) retrieve the value hold by the hashtable. That key might as literal as it is provided. In my particular case, I want it more corresponding to the -eq comparison operator than usual. This means for my use case:

  • ValueTypes should be converted to strings to be type casting alike (e.g.: 1 -eq '1' and '1' -eq 1)
  • $Null should be accepted but not match anything (as e.g. $Null -ne '') except for $Null itself ( $Null -eq $Null )
  • Objects should compare by value at least one level.
    Knowing that normally object keys as e.g. an array like @{ @(1,2,3) = 'Test' }[@(1,2,3)] don't return anything.

Note that this use case doesn't stand on it self, there are a lot of other situations were you might reuse a function that is also used in a large iteration. (note that the self answer is also a use case where would like to reuse the concerned codeblock without extra costs.)

Choice

In other words, should I go for the DRY code:

Function Lookup {
    param (
        [Parameter(ValueFromPipeLine = $True)]$Item,
        [Int]$Size = 100000
    )
    begin {
        function Value2Key ($Value) { # Indexing
            if ( $Null -eq $Value ) { "`u{1B}`$Null" }
            elseif ($Value -is [ValueType]) { "$Value" }
            elseif ($Value -is [System.MarshalByRefObject]) { "`u{1B}$($Value |Select-Object *)" }
            elseif ($Value -is [System.Collections.IDictionary]) { "`u{1B}$($Value.GetEnumerator())" }
            else { "`u{1B}$Value" }
        }

        $Hashtable = @{}
        for ($Value = 0; $Value -lt $Size; $Value++) {
            $Key = Value2Key $Value
            $Hashtable[$Key] = "Some value: $Value"
        }
    }
    process {
        $Key = Value2Key $_
        $Hashtable[$Key]
    }
}

'DRY code = {0}ms' -f (Measure-Command -Expression { 3 |Lookup |Write-Host }).TotalMilliseconds

Some value: 3
DRY code = 5025.3474ms

Or should I go for the fast code (which is more than 10 times faster for 100.000 items):

Function Lookup {
    param (
        [Parameter(ValueFromPipeLine = $True)]$Item,
        [Int]$Size = 100000
    )
    begin {
        $Hashtable = @{}
        for ($Value = 0; $Value -lt $Size; $Value++) { # Indexing
            $Key =
                if ( $Null -eq $Value ) { "`u{1B}`$Null" }
                elseif ($Value -is [ValueType]) { "$Value" }
                elseif ($Value -is [System.MarshalByRefObject]) { "`u{1B}$($Value |Select-Object *)" }
                elseif ($Value -is [System.Collections.IDictionary]) { "`u{1B}$($Value.GetEnumerator())" }
                else { "`u{1B}$Value" }
            $Hashtable[$Key] = "Some value: $Value"
        }
    }
    process {
        $Value = $_
        $Key =
            if ( $Null -eq $Value ) { "`u{1B}`$Null" }
            elseif ($Value -is [ValueType]) { "$Value" }
            elseif ($Value -is [System.MarshalByRefObject]) { "`u{1B}$($Value |Select-Object *)" }
            elseif ($Value -is [System.Collections.IDictionary]) { "`u{1B}$($Value.GetEnumerator())" }
            else { "`u{1B}$Value" }
        $Hashtable[$Key]
    }
}

'Fast code = {0}ms' -f (Measure-Command -Expression { 3 |Lookup |Write-Host }).TotalMilliseconds

Some value: 3
Fast code = 293.3154ms

Question

As the used case implies, I don't care about which scope (current or child) the code is invoked.
Are there any better or faster ways to reuse static code blocks in a script?


Solution

  • In the use case example, the concerned code block is:

    if ( $Null -eq $Value ) { "`u{1B}`$Null" }
    elseif ($Value -is [ValueType]) { "$Value" }
    elseif ($Value -is [System.MarshalByRefObject]) { "`u{1B}$($Value |Select-Object *)" }
    elseif ($Value -is [System.Collections.IDictionary]) { "`u{1B}$($Value.GetEnumerator())" }
    else { "`u{1B}$Value" }
    

    Below, I have tested the performance of several ways to invoke the specific code:

    $Repeat = 10000
    
    [PSCustomObject]@{ Method = 'Invoke-Command with parameter'; 'Time (ms)' = (Measure-Command -Expression {
        $Value2Key = {
            param($Value)
            if ( $Null -eq $Value ) { "`u{1B}`$Null" }
            elseif ($Value -is [ValueType]) { "$Value" }
            elseif ($Value -is [System.MarshalByRefObject]) { "`u{1B}$($Value |Select-Object *)" }
            elseif ($Value -is [System.Collections.IDictionary]) { "`u{1B}$($Value.GetEnumerator())" }
            else { "`u{1B}$Value" }
        }
        for ($Value = 0; $Value -lt $Repeat; $Value++) {
            $Key = Invoke-Command $Value2Key -ArgumentList $Value
        }
    }).TotalMilliseconds }
    $Key |Should -be "$($Repeat - 1)"
    
    [PSCustomObject]@{ Method = 'Invoke-Command without parameter'; 'Time (ms)' = (Measure-Command -Expression {
        $Value2Key = {
            if ( $Null -eq $Value ) { "`u{1B}`$Null" }
            elseif ($Value -is [ValueType]) { "$Value" }
            elseif ($Value -is [System.MarshalByRefObject]) { "`u{1B}$($Value |Select-Object *)" }
            elseif ($Value -is [System.Collections.IDictionary]) { "`u{1B}$($Value.GetEnumerator())" }
            else { "`u{1B}$Value" }
        }
        for ($Value = 0; $Value -lt $Repeat; $Value++) {
            $Key = Invoke-Command $Value2Key
        }
    }).TotalMilliseconds }
    $Key |Should -be "$($Repeat - 1)"
    
    [PSCustomObject]@{ Method = 'Advanced function'; 'Time (ms)' = (Measure-Command -Expression {
        function Value2Key {
            Param($Value)
            if ( $Null -eq $Value ) { "`u{1B}`$Null" }
            elseif ($Value -is [ValueType]) { "$Value" }
            elseif ($Value -is [System.MarshalByRefObject]) { "`u{1B}$($Value |Select-Object *)" }
            elseif ($Value -is [System.Collections.IDictionary]) { "`u{1B}$($Value.GetEnumerator())" }
            else { "`u{1B}$Value" }
        }
        for ($Value = 0; $Value -lt $Repeat; $Value++) {
            $Key = Value2Key $Value
        }
    }).TotalMilliseconds }
    $Key |Should -be "$($Repeat - 1)"
    
    [PSCustomObject]@{ Method = 'Simple function with parameter'; 'Time (ms)' = (Measure-Command -Expression {
        function Value2Key ($Value) {
            if ( $Null -eq $Value ) { "`u{1B}`$Null" }
            elseif ($Value -is [ValueType]) { "$Value" }
            elseif ($Value -is [System.MarshalByRefObject]) { "`u{1B}$($Value |Select-Object *)" }
            elseif ($Value -is [System.Collections.IDictionary]) { "`u{1B}$($Value.GetEnumerator())" }
            else { "`u{1B}$Value" }
        }
        for ($Value = 0; $Value -lt $Repeat; $Value++) {
            $Key = Value2Key $Value
        }
    }).TotalMilliseconds }
    $Key |Should -be "$($Repeat - 1)"
    
    [PSCustomObject]@{ Method = 'Simple function without parameter'; 'Time (ms)' = (Measure-Command -Expression {
        function Value2Key {
            if ( $Null -eq $Value ) { "`u{1B}`$Null" }
            elseif ($Value -is [ValueType]) { "$Value" }
            elseif ($Value -is [System.MarshalByRefObject]) { "`u{1B}$($Value |Select-Object *)" }
            elseif ($Value -is [System.Collections.IDictionary]) { "`u{1B}$($Value.GetEnumerator())" }
            else { "`u{1B}$Value" }
        }
        for ($Value = 0; $Value -lt $Repeat; $Value++) {
            $Key = Value2Key
        }
    }).TotalMilliseconds }
    $Key |Should -be "$($Repeat - 1)"
    
    [PSCustomObject]@{ Method = 'Call codeblock with parameter'; 'Time (ms)' = (Measure-Command -Expression {
        $Value2Key = {
            param($Value)
            if ( $Null -eq $Value ) { "`u{1B}`$Null" }
            elseif ($Value -is [ValueType]) { "$Value" }
            elseif ($Value -is [System.MarshalByRefObject]) { "`u{1B}$($Value |Select-Object *)" }
            elseif ($Value -is [System.Collections.IDictionary]) { "`u{1B}$($Value.GetEnumerator())" }
            else { "`u{1B}$Value" }
        }
        for ($Value = 0; $Value -lt $Repeat; $Value++) {
            $Key = & $Value2Key $Value
        }
    }).TotalMilliseconds }
    $Key |Should -be "$($Repeat - 1)"
    
    [PSCustomObject]@{ Method = 'Call codeblock without parameter'; 'Time (ms)' = (Measure-Command -Expression {
        $Value2Key = {
            if ( $Null -eq $Value ) { "`u{1B}`$Null" }
            elseif ($Value -is [ValueType]) { "$Value" }
            elseif ($Value -is [System.MarshalByRefObject]) { "`u{1B}$($Value |Select-Object *)" }
            elseif ($Value -is [System.Collections.IDictionary]) { "`u{1B}$($Value.GetEnumerator())" }
            else { "`u{1B}$Value" }
        }
        for ($Value = 0; $Value -lt $Repeat; $Value++) {
            $Key = & $Value2Key
        }
    }).TotalMilliseconds }
    $Key |Should -be "$($Repeat - 1)"
    
    [PSCustomObject]@{ Method = 'Dot source codeblock with parameter'; 'Time (ms)' = (Measure-Command -Expression {
        $Value2Key = {
            param($Value)
            if ( $Null -eq $Value ) { "`u{1B}`$Null" }
            elseif ($Value -is [ValueType]) { "$Value" }
            elseif ($Value -is [System.MarshalByRefObject]) { "`u{1B}$($Value |Select-Object *)" }
            elseif ($Value -is [System.Collections.IDictionary]) { "`u{1B}$($Value.GetEnumerator())" }
            else { "`u{1B}$Value" }
        }
        for ($Value = 0; $Value -lt $Repeat; $Value++) {
            $Key = . $Value2Key $Value
        }
    }).TotalMilliseconds }
    $Key |Should -be "$($Repeat - 1)"
    
    [PSCustomObject]@{ Method = 'Dot source codeblock without parameter'; 'Time (ms)' = (Measure-Command -Expression {
        $Value2Key = {
            if ( $Null -eq $Value ) { "`u{1B}`$Null" }
            elseif ($Value -is [ValueType]) { "$Value" }
            elseif ($Value -is [System.MarshalByRefObject]) { "`u{1B}$($Value |Select-Object *)" }
            elseif ($Value -is [System.Collections.IDictionary]) { "`u{1B}$($Value.GetEnumerator())" }
            else { "`u{1B}$Value" }
        }
        for ($Value = 0; $Value -lt $Repeat; $Value++) {
            $Key = . $Value2Key
        }
    }).TotalMilliseconds }
    $Key |Should -be "$($Repeat - 1)"
    
    [PSCustomObject]@{ Method = 'Class'; 'Time (ms)' = (Measure-Command -Expression {
        class Value2Key {
            [String]$Key
            Value2Key($Value) {
                $This.Key = if ( $Null -eq $Value ) { "`u{1B}`$Null" }
                    elseif ($Value -is [ValueType]) { "$Value" }
                    elseif ($Value -is [System.MarshalByRefObject]) { "`u{1B}$($Value |Select-Object *)" }
                    elseif ($Value -is [System.Collections.IDictionary]) { "`u{1B}$($Value.GetEnumerator())" }
                    else { "`u{1B}$Value" }
            }
        }
        for ($Value = 0; $Value -lt $Repeat; $Value++) {
            $Key = ([Value2Key]$Value).Key
        }
    }).TotalMilliseconds }
    $Key |Should -be "$($Repeat - 1)"
    
    [PSCustomObject]@{ Method = 'Steppable Pipeline'; 'Time (ms)' = (Measure-Command -Expression {
        function Value2Key {
            param (
                [Parameter(ValueFromPipeLine = $True)]$Value
            )
            process {
                if ( $Null -eq $Value ) { "`u{1B}`$Null" }
                elseif ($Value -is [ValueType]) { "$Value" }
                elseif ($Value -is [System.MarshalByRefObject]) { "`u{1B}$($Value |Select-Object *)" }
                elseif ($Value -is [System.Collections.IDictionary]) { "`u{1B}$($Value.GetEnumerator())" }
                else { "`u{1B}$Value" }
            }
        }
        $Pipeline = { Value2Key }.GetSteppablePipeline()
        $Pipeline.Begin($True)
        for ($Value = 0; $Value -lt $Repeat; $Value++) {
            $Key = $Pipeline.Process($Value)
        }
        $Pipeline.End()
    }).TotalMilliseconds }
    $Key |Should -be "$($Repeat - 1)"
    
    [PSCustomObject]@{ Method = 'Hardcoded'; 'Time (ms)' = (Measure-Command -Expression {
        for ($Value = 0; $Value -lt $Repeat; $Value++) {
            $Key = 
                if ( $Null -eq $Value ) { "`u{1B}`$Null" }
                elseif ($Value -is [ValueType]) { "$Value" }
                elseif ($Value -is [System.MarshalByRefObject]) { "`u{1B}$($Value |Select-Object *)" }
                elseif ($Value -is [System.Collections.IDictionary]) { "`u{1B}$($Value.GetEnumerator())" }
                else { "`u{1B}$Value" }
        }
    }).TotalMilliseconds }
    $Key |Should -be "$($Repeat - 1)"
    

    This is the result:

    Method                                 Time (ms)
    ------                                 ---------
    Invoke-Command with parameter            1581.06
    Invoke-Command without parameter          992.65
    Advanced function                         377.98
    Simple function with parameter            326.36
    Simple function without parameter         304.04
    Call codeblock with parameter             273.72
    Call codeblock without parameter          258.07
    Dot source codeblock with parameter       301.57
    Dot source codeblock without parameter    201.94
    Class                                     108.79
    Steppable Pipeline                         85.54
    Hardcoded                                  35.17
    

    As it turns out, using the steppable pipeline is the fastest way (apart from the WET -Write Everything Twice- hardcoded solution). Unfortunately, the overhead of creating a steppable pipeline doesn't add much to a clearer code by it self...
    Besides, I would expect somehow to be able to reuse static code without (almost) any extra costs.

    See also discussion: Optimize static code reusage #19322