Optimize PowerShell Code?


Summary: Microsoft Scripting Guy, Ed Wilson, talks about optimizing Windows PowerShell code.

Microsoft Scripting Guy, Ed Wilson, is here. I was looking at my Windows Phone this morning and I noticed that today is Friday the 13th. What seemed a bit strange to me is that I thought there was also a Friday the 13th last month. I then became curious. Exactly how often does the 13th of the month fall on a Friday? Or put another way, how many Friday the 13th’s are there in a year—or at least for this year?

Perhaps I could have opened the Internet and used a Bing search to find out, But I was fairly certain that whatever search terms I used would return lots of stuff about certain movies that I really did not want to think about first thing in the morning…especially today. In any event, I am pretty good with writing Windows PowerShell code, so it took me less than two minutes to whip up a bit to return the answer.

The first thing I did was use the For statement to count from 1 to 12 in increments of 1:

For($d = 1; $d -le 12; $d++)

Next, I created a date using my $d variable:

$datetime = [datetime]"$d/13/15"

If the DayOfWeek property matches Friday, I want to display the date. This is shown here:

If($datetime.dayofweek -match 'Friday')

     { $datetime }

So that is it—as you can see here, four lines of code:

For($d = 1; $d -le 12; $d++)

{

  $datetime = [datetime]"$d/13/15"

  If($datetime.dayofweek -match 'Friday')

     { $datetime }}

When I run the script, the following output appears in the output pane of my Windows PowerShell ISE:

Image of command output

Optimize the code

I decided to ask a few people if they could optimize my code. You see, I did not spend any time at all trying to improve or optimize it. The first to respond was Microsoft PFE, Gary Siepser. He came back with the following:

((1..12).foreach({[datetime]"$_/13/15"}).where({$_.dayofweek -match 'Friday'}))

Gary uses the .Foreach and .Where methods from Windows PowerShell 4.0 to create one monster command. His own remark is, “It is uglier, but at least it is one line.”

He then goes on to say that he did not really understand what I meant when I asked him to “optimize” the code. Did I mean for performance or for simplicity? (I actually think my code is elegant.)

And that was part of my point. I wanted to see what different people automatically think of when we talk about optimization. There are several things that could be thought of as inherent in code optimization:

  • Readability
  • Performance
  • Conciseness

Next to respond was Microsoft PFE, Ashley McGlone. He also sent back a one-line command. Here is his result:

1..12 | % {If (([datetime]"$_/13/15").DayOfWeek -eq 'Friday') {[datetime]"$_/13/15"}}

In Ashley’s example, he stayed with the If statement, but he converted to using the pipeline to create an array of numbers. His code was six characters longer than Gary’s. One big difference between the code from Ashley and Gary may be in terms of readability. Because Gary uses the syntax from Windows PowerShell 4.0, it might not be as readable to some as Ashley’s might be. Also Ashley uses the pipeline, but Gary does not.

Microsoft PFE, Jason Walker, was the next to return an answer. His is the shortest so far—by a long shot, his command is only 63 characters long. How does he do it? Here it is:

1..12 | %{[datetime]"$_/13/15" | Where DayOfWeek -eq 'Friday'} 

Jason uses the pipeline like Ashley did, but instead of imbedding an If statement, Jason pipes the date to the Where-Object cmdlet. This ends up with a command that is a lot shorter.

Microsoft senior SDE, Lee Holmes, also returned an answer. He took the same approach as Jason, but his command was two characters shorter. How did he do that? Well, when making a comparison to the day of the week, he realized that the quotation marks are not required around the word Friday. Here is his answer:

1..12 | % { [DateTime] "$_/13/15" } | ? DayOfWeek -eq Friday

Optimized code summary

All of the code returned exactly the same results. This is shown here:

Image of command output

So which is fastest? I can determine that by using the Measure-Command cmdlet. Here are the results:

Image of command output

One thing to keep in mind is that millisecond results are not accurate for Measure-Command. Also, each time I ran the code, I obtained a different result. What did not change was the order of the results: Gary, Ashly, Lee, and then Jason.

What was really interesting was that although Jason’s and Lee’s one-liners were nearly identical, Jason’s code always showed up as being nearly twice as slow as Lee’s. The only difference was spaces, quotation marks, and aliases. Strange.

And my script? Well, as shown here, it showed up as the fastest of all:

Image of command output

… that is, until I got Bruce Payette’s submission.

Obviously, I emailed a whole bunch of people and asked them to “optimize” the code. Windows PowerShell MVPs, Jim Christopher and Sean Kearney, used the same approach that Lee and Jason used. Here are their samples:

Jim Christopher:

1..12| % {[datetime]"$_/13/15"} | where dayofweek -match 'friday'

Sean Kearney:

1..12 | foreach { [datetime]"$($_)/13/15" | where { $_.DayOfWeek -match 'Friday' }}

Gary wrote me that he showed the optimization question to the Windows PowerShell class he was teaching this week, and one of his students came up with the following. It is a way cool approach:

1..12 | select @{name="FF";Expression={[datetime]"$_/13/15"|? dayofweek -like 'F*'}} -Unique

Microsoft PFE, Brian Wilhite, saw several of the responses to the question in the email thread, so he opted for a different approach for the sake of being different. (By the way, his command was not as fast as Gary Siepser’s.) Here is his script:

$Date = Get-Date 1/1/15

    while ($Date.Year -eq 2015) {

        If (($Date.DayOfWeek -eq 'Friday') -and ($Date.Day -eq 13)){

            $Date

        }

        $Date = $Date.AddDays(1)

    }

Microsoft principal SDE, Bruce Payette, took a completely different approach. He used a ForEach loop instead of the pipeline or the For statement:

foreach ($m in 1..12)

{

    $dt = [datetime]"$m/13/15"

    if ($dt.dayofweek -match 'Friday') { $dt }

Bruce explains that a Foreach loop is faster than the For statement or the pipeline. He goes on to say, “Alternatively, you could use the Foreach/Where method operators to do something like the following:

(1..12).foreach{[datetime]"$_/13/15"}.where{$_.DayOfWeek -eq "Friday"}

This is arguably simpler than his previous script, but it is less efficient due to the script block calls in Foreach and Where.”

When I used Measure-Command on Bruce’s script, he was right—way right. Here is the result:

Image of command output

One of the things I thought was interesting is that many of the people who replied did not even ask what I meant by “optimize.” They just seemed to think “one-liner.” Several people who replied did ask what I meant by optimize—and some even asked if I meant shorter, faster, or more efficient. So what do you think? How would you optimize the code I showed today?

I want to thank all of the people who replied to my email questions and who took the time from their busy schedules to help me optimize this code.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy 

Comments (8)

  1. jrv says:

    Good discussion. Here is one that is faster than Brians:

    Measure-command {((1..12).foreach({[datetime]"$_/13/15"}).where({$_.dayofweek -eq 5}))}

    Just eliminate his string match and the equality direct as "DayOfWeek" is an enumerated int. It is stored as an int and can be decoded to a string but that is slow.

  2. @ed

    You could almost have used the title "Dueling Scripters" with this one. So many variants on the same idea. I can almost hear "Name that tune" in the background with "I write that code in only 70 characters….65 characters…. 63!"

    Cue up the Dueling Banjos music! 🙂

    Sean

  3. jrv says:

    Darn web site code formatting:


    for($i=1;$i -le 12;$i++){([datetime]"$i/13/15").Where{$_.DayOfWeek -eq 5}}
    (Measure-Command {1..100 | %{
    Measure-Command {
    for($i=1;$i -le 12;$i++){([datetime]"$i/13/15").Where{$_.DayOfWeek -eq 5}}
    }
    }}).TotalMilliseconds/100

  4. Anonymous says:

    Wow, Robert’s example is fast! Do you have any explanation as to why it is faster than any of the other very fast examples?

  5. jrv says:

    Even faster:

    for($i=1;$i -le 12;$i++){([datetime]"$i/13/15").Where{$_.DayOfWeek -eq 5}}
    (Measure-Command {1..100 | %{
    Measure-Command {
    for($i=1;$i -le 12;$i++){([datetime]"$i/13/15").Where{$_.DayOfWeek -eq 5}}
    }
    }}).TotalMilliseconds/100

  6. Marc Sherman says:

    Extremely interesting, thanks!

  7. Robert Kozak says:

    This is the Optimized for the fastest. On my machine it performs faster than any of the above by a factor of 10

    1..12 |%{if (([datetime]"$_/13/15").dayofweek -eq ‘Friday’) { [datetime]"$_/13/15" }}

    This is the results I got from the test script below:

    Mine
    0.0863283
    JVR
    0.9350128

    cls

    "Mine"
    (Measure-Command {1..1000 | %{

    {1..12 |%{
    if (([datetime]"$_/13/15").dayofweek -match ‘Friday’) { [datetime]"$_/13/15" }
    }
    }

    }}).TotalMilliseconds/1000

    "JRV"
    (Measure-Command {1..1000 | %{

    for($i=1;$i -le 12;$i++){([datetime]"$i/13/15").Where{$_.DayOfWeek -eq 5}}

    }}).TotalMilliseconds/1000

  8. Calin says:

    Your example:
    (Measure-command {((1..12)|foreach({[datetime]"$_/13/15"})|where({$_.dayofweek -eq 5}))}).TotalMilliseconds
    16.2514

    But i made something Funny 🙂
    cls
    "+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-"
    [int]$an_final =Read-Host "The year of birth (ex.1920)"
    $xx = @()
    foreach($x in $an_final..2015 ){
    foreach($d in 1..12 ){
    $dt = [datetime]"$d/13/$x"
    If($dt.dayofweek -match ‘Friday’){$xx += (($dt | Out-String).Replace(" 00:00:00","")).trim()}
    }
    }
    $($XX | ft -AutoSize)
    " By 2015 you lived $($XX.Count) days of Friday the 13th."
    "+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-"
    for me was 74 days 🙂

Skip to main content