Why is my code so slow?

Can you spot the difference between this code:

And this code:

This is not quite a trick question, the only difference is that one uses $vm and one uses $ in the second line. Both of these are valid and produce the same results. However, there’s a really important difference between them. I timed these code samples in my environment and here are the results:

  • Code sample 1 (aka Fast): 13.44 seconds.
  • Code sample 2 (aka Slow): 3178.55 seconds.

In other words, Slow takes more than 200 times longer to execute than Fast does. What could possibly explain this dramatic difference in speed?

We’ll return to that after we take a look at the PowerShell signature for the Get-HardDisk cmdlet.

If you’re versed in PowerShell you know that this means that the –VM argument to Get-HardDisk requires a VirtualMachine object, which is the type of object that Get-VM outputs.

You may notice, though that the second code sample above uses the VM’s Name property, which is a string as the –VM argument. How is it possible for something like Get-HardDisk -VM "myVM" to work at all? After all, “myVM” is a string, not a VirtualMachine, so shouldn’t this fail?

The reason this works is because VI Toolkit takes advantage of a feature of PowerShell that lets you transform the arguments you receive on the command line. This is the basis of what we call the VI Toolkit’s “Object By Name” feature: If you specify a string where an object should be, VI Toolkit works behind the scenes to replace this string with the object that the string represents.

Inevitably this lookup has a cost, the question is how much is that cost? This brings us to a rather unfortunate property of VI Toolkit, which is that when you get a VM, all filtering is done on the client side. On one hand this is good because it allows us to support wildcards and case-insensitivity. However there is one very unfortunate consequence, which is that it takes just as long to load one VM as it takes to load all of them (more on how we are improving this below). This is the basic reason that the second example is so slow: every time Get-HardDisk is called, VI Toolkit looks up that one machine object behind the scenes.

Computer sciencey-types have some fancy terminology called “Big O Notation” that helps the discussion here. If you don’t know Big O Notation, don’t worry there’s no quiz at the end. But basically Big O Notation lets us understand why some approaches are fast and some approaches are slow. The first code sample is “order of N” or O(N), where N is the number of VMs on the system. This is because you need to load N many VM objects into memory, using Get-VM, then feed the objects into Get-HardDisk, which is pretty quick. The second code sample is “order of N squared” or O(N2) because for each of those N VMs you load using Get-VM you turn around and load the exact same N VMs when Get-HardDisk goes through all VMs looking for the one with that particular name. Since you load those N objects N many times, you end up with O(N2). The larger N is, the more dramatic the differences in execution time. In my case I had 465 VMs and the code took about 230 times longer, which sounds reasonable enough (this is not a very exact science). If I had 1000 VMs you could expect that it would take something closer to 1000 times longer, for 2000 VMs it would be closer to 2000 times longer. You get the idea: You don’t want to do this sort of stuff if you’ve got a big environment.

As we see from this example, there are some fairly subtle differences that can cause your code to be really slow. How do you avoid these landmines in your code? Use these best practices to maximize your code’s performance:

  1. Try to load as many objects as possible into arrays beforehand. Once you’ve got them loaded you can use them as arguments to multiple calls without having to resort to potentially expensive lookups every time.
  2. Just like in sample 1 above, when you’ve loaded objects, use the objects directly rather than using their names. This is usually not hard as our cmdlets are designed to take object first-and-foremost, and names are supported just as a convenience.
  3. If you absolutely need to load a single VM object by name, load it using the Get-VMFast function below. While this approach can certainly help, it’s not nearly as good as using the other two techniques mentioned above.

One thing you may be happy to hear is that in the future we will be optimizing the case of looking up a single VM by name and it will be a lot faster. This will be done using a technique similar to the one used by Get-VMFast. You should note though that even with this faster lookup of single VMs, the second code sample above is still O(N2), but the code will still be a lot faster because those second lookups are so much faster. The computer sciencey-types would say that we have a much smaller constant factor. Still, eventually that factor of N takes over and things will get slow again. To put it another way, the landmine is still there but it will be small enough that most people probably won’t manage to step on it. But, if you need maximum performance out of your code, you need to use suggestions 1 and 2 as outlined above.


13 comments have been added so far

  1. How come when I run the Get-VMFast function, I keep getting this error?
    Get-VIObjectByVIView : Failed to connect to the VI server at: https://xxxxxxxxxxxxxxx
    I’ve tried this on several VC servers and it’s not working. I can’t seem to get a VIObject returned…

  2. Justin, Can you try this:
    Function Get-VMFast {
    $view = Get-View -ViewType VirtualMachine -Filter @{“Name” = $name} -property ConfigStatus
    return Get-VIObjectByVIView -Server $defaultVIServer -MoRef $view.MoRef

  3. Carter,
    This now works, however the time difference between get-vm and get-vmfast is inconsequential in my test here – using Measure-Command I see Get-VM takes 1.068 seconds, and Get-VMFast takes 1.098 seconds!
    I’ve tested multiple times and I get around the same numbers for both…

  4. 1. Try to load as many objects as possible into arrays beforehand. Once you’ve got them loaded you can use them as arguments to multiple calls without having to resort to potentially expensive lookups every time.
    This tip goes A LONG way. I shaved 35 seconds off a complex function just by eliminating redundant get-view calls. It’s cool when you find a little nugget that lets you go back through an optimize all of your code.

Leave a Reply

Your email address will not be published. Required fields are marked *