Back
Featured image of post PowerShell: foreach vs. .ForEach and ForEach-Object with IF vs. .Where

PowerShell: foreach vs. .ForEach and ForEach-Object with IF vs. .Where

I do a lot of workshops with developers and administrators (or DevOps if you like to call them that way). During one of the last workshops, one of the admins asked me a clever question: “Why do you pipe so much? Why don’t you use .ForEach and .Where instead? This should be faster!”.

So we took an existing function (One I use to clean up all my event log entries, and decided to refactor it. To see the difference, we also created a wrapper script that can handle all the measurement for us.

#requires -Version 4.0

<#
      .SYNOPSIS
      Compare a old and a refactored function to get any Performance differences

      .DESCRIPTION
      This script compares a simple function (That deletes all Windows Eventlog Entries) with an refacored one.
      The request came up during a workshop: I was asked why I use pipes so much and if there is another way, without pipes.

      The refactored version was created during the workshop as a prototype.
      And to make it easier to compare them, I created this test script.

      .EXAMPLE
      PS C:\> .\Clear-EnAllEventLogs_TESTS.ps1

      .NOTES
      Version: 1.0.0

      GUID: a0a633ca-6fd1-4806-a160-05bf1f76342b

      Author: Joerg Hochwald

      Companyname: enabling Technology

      Copyright: Copyright (c) 2ß18-2019, enabling Technology - All rights reserved.

      License: https://opensource.org/licenses/BSD-3-Clause

      Releasenotes:
      1.0.0 2019-07-24 Initial Version

      THIS CODE IS MADE AVAILABLE AS IS, WITHOUT WARRANTY OF ANY KIND. THE ENTIRE RISK OF THE USE OR THE RESULTS FROM THE USE OF THIS CODE REMAINS WITH THE USER.

      Dependencies:
      NONE

      .LINK
      https://www.enatec.io
#>
[CmdletBinding(ConfirmImpact = 'None')]
[OutputType([psobject])]
param ()

#region VersionOfJosh
function Clear-EnAllEventLogs
{
   <#
         .SYNOPSIS
         Delete all Windows event log entries

         .DESCRIPTION
         Delete all Windows event log entries, without any further interaction.
         I use this only after I do some tests on a virtual machine.

         Please Note:
         It Might be dangerous! It might delete more than you like.

         Warning:
         All security related will also be removed completely.
         If there were any issues, you might never find any information about it!

         .PARAMETER ComputerName
         Computer Name as String. Multi Value is possible

         .EXAMPLE
         PS C:\> Clear-EnAllEventLogs

         Delete all Windows EventLog Entries on the local Computer.

         .EXAMPLE
         PS C:\> Clear-EnAllEventLogs -ComputerName FRADC01

         Delete all Windows EventLog Entries on the Computer with the name FRADC01.

         .EXAMPLE
         PS C:\> Clear-EnAllEventLogs -ComputerName 'FRADC01', 'FRADC02'

         Delete all Windows EventLog Entries on the Computers with the names FRADC01 and FRADC02.

         .NOTES
         Version: 1.2.3

         GUID: a0a633ca-6fd1-4806-a160-05bf1f76342b

         Author: Joerg Hochwald

         Companyname: enabling Technology

         Copyright: Copyright (c) 2ß18-2019, enabling Technology - All rights reserved.

         License: https://opensource.org/licenses/BSD-3-Clause

         Releasenotes:

         THIS CODE IS MADE AVAILABLE AS IS, WITHOUT WARRANTY OF ANY KIND. THE ENTIRE RISK OF THE USE OR THE RESULTS FROM THE USE OF THIS CODE REMAINS WITH THE USER.

         Dependencies:
         TNONE

         .LINK
         https://www.enatec.io

         .LINK
         about_foreach

         .LINK
         Foreach-Object

         .LINK
         Get-EventLog

         .LINK
         Clear-EventLog
   #>
   [CmdletBinding(ConfirmImpact = 'Medium',
   SupportsShouldProcess)]
   param
   (
      [Parameter(ValueFromPipeline,
      ValueFromPipelineByPropertyName)]
      [string[]]
      $ComputerName = "$env:COMPUTERNAME"
   )

   process
   {

      foreach ($SingleComputerName in $ComputerName)
      {
         if ($pscmdlet.ShouldProcess($SingleComputerName, 'Cleanup All EventLogs'))
         {
            $paramGetEventLog = @{
               ComputerName = $SingleComputerName
               List         = $true
            }
            $null = (Get-EventLog @paramGetEventLog | ForEach-Object -Process {
                  if ($_.Entries)
                  {
                     $paramClearEventLog = @{
                        LogName     = $_.Log
                        Confirm     = $false
                        ErrorAction = 'SilentlyContinue'
                     }
                     $null = (Clear-EventLog @paramClearEventLog)
                  }
            })
         }
      }
   }
}
#endregion VersionOfJosh

#region RefactoredVersion
function Clear-EnAllEventLogsv2
{
   <#
         .SYNOPSIS
         Delete all Windows event log entries

         .DESCRIPTION
         Delete all Windows event log entries, without any further interaction.
         I use this only after I do some tests on a virtual machine.

         Please Note:
         It Might be dangerous! It might delete more than you like.

         Warning:
         All security related will also be removed completely.
         If there were any issues, you might never find any information about it!

         .PARAMETER ComputerName
         Computer Name as String. Multi Value is possible

         .EXAMPLE
         PS C:\> Clear-EnAllEventLogsv2

         Delete all Windows EventLog Entries on the local Computer.

         .EXAMPLE
         PS C:\> Clear-EnAllEventLogsv2 -ComputerName FRADC01

         Delete all Windows EventLog Entries on the Computer with the name FRADC01.

         .EXAMPLE
         PS C:\> Clear-EnAllEventLogsv2 -ComputerName 'FRADC01', 'FRADC02'

         Delete all Windows EventLog Entries on the Computers with the names FRADC01 and FRADC02.

         .NOTES
         Version: 2.0.0

         GUID: 0b2fcd13-0b5e-48df-9970-7c5fab649ee7

         Author: Joerg Hochwald

         Companyname: enabling Technology

         Copyright: Copyright (c) 2ß18-2019, enabling Technology - All rights reserved.

         License: https://opensource.org/licenses/BSD-3-Clause

         Releasenotes:
         2.0.0 2019-07-23: Refactored version

         THIS CODE IS MADE AVAILABLE AS IS, WITHOUT WARRANTY OF ANY KIND. THE ENTIRE RISK OF THE USE OR THE RESULTS FROM THE USE OF THIS CODE REMAINS WITH THE USER.

         Dependencies:
         NONE

         .LINK
         https://www.enatec.io

         .LINK
         about_foreach

         .LINK
         Foreach-Object

         .LINK
         Get-EventLog

         .LINK
         Clear-EventLog
   #>
   [CmdletBinding(ConfirmImpact = 'Medium',
   SupportsShouldProcess)]
   param
   (
      [Parameter(ValueFromPipeline,
      ValueFromPipelineByPropertyName)]
      [string[]]
      $ComputerName = "$env:COMPUTERNAME"
   )

   process
   {

      foreach ($SingleComputerName in $ComputerName)
      {
         if ($pscmdlet.ShouldProcess($SingleComputerName, 'Cleanup All EventLogs'))
         {
            $paramGetEventLog = @{
               ComputerName = $SingleComputerName
               List         = $true
            }
            $null = ((Get-EventLog @paramGetEventLog).Where({
                     if ($_.Entries)
                     {
                        $_
                     }
               }).ForEach({
                     $paramClearEventLog = @{
                        LogName     = $_.Log
                        Confirm     = $false
                        ErrorAction = 'SilentlyContinue'
                     }
                     $null = (Clear-EventLog @paramClearEventLog)
            }))
         }
      }
   }
}
#endregion RefactoredVersion

#region CreateTestData
function Invoke-CreateTestData
{
   <#
         .SYNOPSIS
         Crete 10.000 a few Dummy entries

         .DESCRIPTION
         Crete 10.000 a few Dummy entrie

         .EXAMPLE
         PS C:\> Invoke-CreateTestData

         .NOTES
         Internal Helper Function to create some useless Test Data

         Version: 1.0.1

         GUID: bb8bf55b-c3f3-4046-a6e8-2e389fd525d7

         Author: Joerg Hochwald

         Companyname: enabling Technology

         Copyright: Copyright (c) 2ß18-2019, enabling Technology - All rights reserved.

         License: https://opensource.org/licenses/BSD-3-Clause

         Releasenotes:
         1.0.1 2019-07-23: Splat the parameters for better radability
         1.0.0 2019-07-23: Initial Version

         THIS CODE IS MADE AVAILABLE AS IS, WITHOUT WARRANTY OF ANY KIND. THE ENTIRE RISK OF THE USE OR THE RESULTS FROM THE USE OF THIS CODE REMAINS WITH THE USER.

         Dependencies:
         NONE

         .LINK
         https://www.enatec.io

         .LINK
         Write-EventLog

         .LINK
         about_foreach

         .LINK
         Foreach-Object
   #>

   [CmdletBinding(ConfirmImpact = 'None')]
   param ()

   begin
   {
      # Splat the parameters
      $paramWriteEventLog = @{
         LogName     = 'Application'
         EventId     = 2001
         EntryType   = 'Information'
         Source      = 'HAL9000'
         Message     = 'I think you know what the problem is just as well as I do.'
         ErrorAction = 'SilentlyContinue'
      }
   }

   process
   {
      # Change the number to fit your needs
      1 .. 1000 | ForEach-Object -Process {
         $null = (Write-EventLog @paramWriteEventLog)
      }
   }
}
#endregion CreateTestData

# Initial Cleanup
$null = (Clear-EnAllEventLogs -ErrorAction SilentlyContinue)

# Create a few new objects
$OldWayAverage = @()
$OldWaySum = @()
$NewWayAverage = @()
$NewWaySum = @()

# Create the new Eventlog
$null = (New-EventLog -LogName Application -Source 'HAL9000' -ErrorAction SilentlyContinue)

#region OldWay
$null = (1..10 | ForEach-Object {
      # Create some Test Data
      $null = (Invoke-CreateTestData -ErrorAction SilentlyContinue)

      #region OldWaySingle
      $OldWaySingle = (Measure-Command -Expression {
            $null = (Clear-EnAllEventLogs -ErrorAction SilentlyContinue)
      })
      #endregion OldWaySingle
      $OldWaySum += $OldWaySingle
})
$OldWayAverage = (($OldWaySum | Measure-Object -Property TotalMilliseconds -Average).Average)
#endregion OldWay

#region NewWay
$null = (1..10 | ForEach-Object {
      # Create some Test Data
      $null = (Invoke-CreateTestData -ErrorAction SilentlyContinue)

      #region NewWaySingle
      $NewWaySingle = (Measure-Command -Expression {
            $null = (Clear-EnAllEventLogsv2 -ErrorAction SilentlyContinue)
      })
      #endregion NewWaySingle
      $NewWaySum += $NewWaySingle
})
$NewWayAverage = (($NewWaySum | Measure-Object -Property TotalMilliseconds -Average).Average)
#endregion NewWay

#Region DumpData
Write-Verbose -Message 'Time measured in milliseconds' -Verbose

[pscustomobject]@{
   OldWay = $OldWayAverage
   NewWay = $NewWayAverage
}
#endregion DumpData

The refactored version is a bit faster.

This is also part of my open-source repository.