Skip to content

Instantly share code, notes, and snippets.

@bigbadmoshe
Last active April 3, 2023 10:09
Show Gist options
  • Save bigbadmoshe/8b745ccff8fa94030ad8db02b39c720c to your computer and use it in GitHub Desktop.
Save bigbadmoshe/8b745ccff8fa94030ad8db02b39c720c to your computer and use it in GitHub Desktop.
Dotnet or not Dotnet

Dotnet or not Dotnet this is the question we will ask in this post

Lets find out if the .NET .Where() method is significantly faster than their equivalent in native PowerShell In this post, we'll compare the performance of native PowerShell methods with their .NET counterparts, specifically focusing on the .Where() method. We'll also use the .net[Linq.Enumerable] class to analyze a different dataset - passenger data from the Titanic - instead of the usual Active Directory user data.

The Code We'll be using three different methods to compare performance:

# Define a collection of objects
$Import = @( 
    [PSCustomObject]@{ Name = "John"; Sex = "male" },
    [PSCustomObject]@{ Name = "Mary"; Sex = "female" },
    [PSCustomObject]@{ Name = "Peter"; Sex = "male" }
)

# .NET LINQ method
$StopWatch = New-Object System.Diagnostics.Stopwatch
$StopWatch.Start()
$delegate = [Func[object,bool]] { $args[0].Sex -eq "male" }
$Result = [Linq.Enumerable]::Where($Import, $delegate)
$Result = [Linq.Enumerable]::ToList($Result)
$StopWatch.Stop()
$TestList.add([PSCustomObject]@{
    Method = "Linq Where-Method"
    ResultCounter = $Result.Count
    TimeElapsed = $StopWatch.Elapsed
    TimeElapsedMS = $StopWatch.ElapsedMilliseconds
})
 
# PowerShell pipeline with Where-Object
$StopWatch = New-Object System.Diagnostics.Stopwatch
$StopWatch.Start()
$Result = $Import | Where-Object{$_.Sex -eq "male"}
$StopWatch.Stop()
$TestList.add([PSCustomObject]@{
    Method = "Piped Where-Method"
    ResultCounter = $Result.Count
    TimeElapsed = $StopWatch.Elapsed
    TimeElapsedMS = $StopWatch.ElapsedMilliseconds
})

# .NET Where() method
$StopWatch = New-Object System.Diagnostics.Stopwatch
$StopWatch.Start()
$Result = $Import.Where({$_.Sex -eq "male"})
$StopWatch.Stop()
$TestList.add([PSCustomObject]@{
    Method = ".where()-Method"
    ResultCounter = $Result.Count
    TimeElapsed = $StopWatch.Elapsed
    TimeElapsedMS = $StopWatch.ElapsedMilliseconds
})

A Scary Realization: Inconsistent Execution Times As I was checking the results and testing the reliability of my code, I executed my code segments multiple times. I noticed that there were times when there was another winner when it comes to execution time, and the results were somewhat different each time I ran the code. I was wondering how this could happen, so I decided to switch from PowerShell Version 7.x to 5.1, but the results were nearly the same.

To investigate this further, I performed the same action 101 times for each version of PowerShell on my machine and took the average of each 101 runs, and put them into a table.

The Results: Comparing PowerShell Versions 7.X and 5.1

Here are the results of my tests:

AverageOf101ms Method PSVersion
3,0495049504950495 Linq Where-Method 7
5,851485148514851 Piped Where-Method 7
1,3465346534653466 .where()-Method 7

PowerShell Version 5.1

AverageOf101ms Method PSVersion
6,88118811881188 Linq Where-Method 5
11,2871287128713 Piped Where-Method 5
3,88118811881188 .where()-Method 5
$myArray = 1..1000

# Using ForEach-Object
Measure-Command {
    $myArray | ForEach-Object {
        # Do something with the array element
        $result = $_ * 2
    }
}

# Using .ForEach() method
Measure-Command {
    $myArray.ForEach({
        # Do something with the array element
        $result = $_ * 2
    })
}

Days : 0 Hours : 0 Minutes : 0 Seconds : 0 Milliseconds : 13 Ticks : 136114 TotalDays : 1.57539351851852E-07 TotalHours : 3.78094444444444E-06 TotalMinutes : 0.000226856666666667 TotalSeconds : 0.0136114 TotalMilliseconds : 13.6114

Days : 0 Hours : 0 Minutes : 0 Seconds : 0 Milliseconds : 16 Ticks : 162860 TotalDays : 1.8849537037037E-07 TotalHours : 4.52388888888889E-06 TotalMinutes : 0.000271433333333333 TotalSeconds : 0.016286 TotalMilliseconds : 16.286

function Test-DeleteMethods {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,
        
        [Parameter(Mandatory = $false)]
        [switch]$Recurse,

        [Parameter(Mandatory = $false)]
        [switch]$WhatIf,

        [Parameter(Mandatory = $false)]
        [switch]$MeasureTime
    )

    $results = @{}
    $results[".NET"] = @{}
    $results["PowerShell"] = @{}

    # Delete folders
    if (Test-Path $Path -PathType Container) {
        $Type = 'Folder'
        $items = Get-ChildItem $Path -Recurse:$Recurse
        foreach ($item in $items) {
            switch ($Type) {
                'Folder' {
                    # .NET method
                    $dotNetStopwatch = [System.Diagnostics.Stopwatch]::StartNew()
                    $folders = [System.IO.Directory]::GetDirectories($item.FullName, "*", [System.IO.SearchOption]::TopDirectoryOnly)
                    if ($WhatIf) {
                        Write-Host "Removing $($item.FullName)"
                    } else {
                        $folders | ForEach-Object {
                            [System.IO.Directory]::Delete($_, $Recurse)
                        }
                        [System.IO.Directory]::Delete($item.FullName, $Recurse)
                    }
                    $dotNetStopwatch.Stop()
                    $results[".NET"]["Folder"] += $dotNetStopwatch.Elapsed.TotalSeconds

                    # PowerShell pipeline method
                    if ($WhatIf) {
                        Write-Host "Removing $($item.FullName)"
                    } else {
                        if ($MeasureTime) {
                            $psStopwatch = [System.Diagnostics.Stopwatch]::StartNew()
                            Get-ChildItem $item.FullName -Recurse:$Recurse | Remove-Item -Force
                            $psStopwatch.Stop()
                            $results["PowerShell"]["Folder"] += $psStopwatch.Elapsed.TotalSeconds
                        } else {
                            Get-ChildItem $item.FullName -Recurse:$Recurse | Remove-Item -Force
                        }
                    }
                }
            }
        }
    }
    # Delete registry keys
    elseif (Test-Path $Path -PathType Registry) {
        $Type = 'Registry'
        $root = Split-Path $Path -Parent
        $name = Split-Path $Path -Leaf
        # .NET method
        $dotNetStopwatch = [System.Diagnostics.Stopwatch]::StartNew()
        $key = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($root, $true)
        if ($key -ne $null) {
            if ($WhatIf) {
                Write-Host "Removing registry key: $Path"
            } else {
                $key.DeleteSubKeyTree($name)
            }
        }
        $dotNetStopwatch.Stop()
        $results[".NET"]["Registry"] = $dotNetStopwatch.Elapsed.TotalSeconds

        # PowerShell pipeline method
        if ($WhatIf) {
            Write-Host "Removing registry key: $Path"
        } else {
            if ($MeasureTime) {
                $psStopwatch = [System.Diagnostics.Stopwatch]::StartNew()
                Get-Item $Path | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
                $psStopwatch.Stop()
                $results["PowerShell"]["Registry"] = $psStopwatch.Elapsed.TotalSeconds
            if ($WhatIf) {
                Write-Host "Removing $($item)"
            } else {
                if ($MeasureTime) {
                    $psStopwatch = [System.Diagnostics.Stopwatch]::StartNew()
                    Remove-Item $item -Force
                    $psStopwatch.Stop()
                    $results['PowerShell'][$item] = $psStopwatch.ElapsedMilliseconds
                } else {
                    Remove-Item $item -Force
                }
            }
        }
    }
    if ($Type -eq 'Registry') {
        # .NET method
        $dotNetStopwatch = [System.Diagnostics.Stopwatch]::StartNew()
        $key = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($Path, 'ReadWriteSubTree', 'PermissionCheck')
        if ($key) {
            $subKeyNames = $key.GetSubKeyNames()
            foreach ($subKeyName in $subKeyNames) {
                $subKey = $key.OpenSubKey($subKeyName, 'ReadWriteSubTree', 'PermissionCheck')
                if ($subKey) {
                    if ($WhatIf) {
                        Write-Host "Removing $($subKey.Name)"
                    } else {
                        if ($MeasureTime) {
                            $dotNetStopwatchInner = [System.Diagnostics.Stopwatch]::StartNew()
                            $key.DeleteSubKeyTree($subKeyName)
                            $dotNetStopwatchInner.Stop()
                            $results['.NET'][$subKey.Name] = $dotNetStopwatchInner.ElapsedMilliseconds
                        } else {
                            $key.DeleteSubKeyTree($subKeyName)
                        }
                    }
                }
            }
            if ($WhatIf) {
                Write-Host "Removing $($key.Name)"
            } else {
                if ($MeasureTime) {
                    $dotNetStopwatchInner = [System.Diagnostics.Stopwatch]::StartNew()
                    $key.DeleteSubKeyTree('')
                    $dotNetStopwatchInner.Stop()
                    $results['.NET'][$key.Name] = $dotNetStopwatchInner.ElapsedMilliseconds
                } else {
                    $key.DeleteSubKeyTree('')
                }
            }
        }
        $dotNetStopwatch.Stop()
        $results['.NET']['TotalTime'] = $dotNetStopwatch.ElapsedMilliseconds
    }
}
$results
Compare-NetVsPowerShell -Path "C:\Windows\System32"
Time taken by .NET LINQ method: 0.3793316 seconds
Time taken by .NET Where() method: 0.0360991 seconds
Time taken by PowerShell pipeline with Where-Object: 1.4599728 seconds
function Test-RemoveContentSpeed {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]]$Path,

        [ValidateSet('PowerShell', '.NET')]
        [string]$Method = 'PowerShell',

        [switch]$WhatIf
    )

    $results = @{
        'Method' = $Method
        'Path'   = $Path
        'Time'   = [System.TimeSpan]::Zero
    }

    $measureScriptBlock = {
        $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
        Remove-Item -Path $using:Path -Recurse -Force -Confirm:$false -WhatIf:$using:WhatIf
        $stopwatch.Stop()
        $stopwatch.Elapsed
    }

    switch ($Path) {
        { $_ -like 'HKLM:\*' -or $_ -like 'HKCU:\*' } {
            # Registry method
            $results['PathType'] = 'Registry'
            $results['Size'] = 0
            $results['Count'] = 0
            $results['Time'] = & $measureScriptBlock
        }
        { Test-Path $_ -PathType Container } {
            # Folders method
            $results['PathType'] = 'Folder'
            $results['Size'] = (Get-ChildItem -Path $Path -Recurse -Force | Measure-Object -Property Length -Sum).Sum
            $results['Count'] = (Get-ChildItem -Path $Path -Recurse -Force | Measure-Object).Count
            $results['Time'] = & $measureScriptBlock
        }
        default {
            throw "Unsupported path type. Only registry and folder paths are supported."
        }
    }

    $results
}
Test-RemoveContentSpeed -Path 'C:\Test' -Method PowerShell -WhatIf
Test-RemoveContentSpeed -Path 'HKLM:\Software\MyCompany' -Method .NET
Time taken by .NET LINQ method: 0.0400406 seconds
Time taken by PowerShell pipeline with Where-Object: 0.1624441 seconds
Time taken by .NET Where() method: 0.0061397 seconds
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment