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:
Especially if the concerned code block is inexpensive but very verbose.
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:
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
)@{ @(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.)
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
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?
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