You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ForEach-Object operation statements are unexpectedly slower than script blocks; both are much slower than filter functions #7700
Compare the performance of extracting the .Name property values from 1 million objects:
# PSv3+ operation statement (using parameters directly rather than a script block)
(Measure-Command { , (Get-Item/) *1e6|ForEach-Object Name }).TotalSeconds
# Script block
(Measure-Command { , (Get-Item/) *1e6|ForEach-Object { $_.Name } }).TotalSeconds
# Filter functionfilterName { $_.Name }
(Measure-Command { , (Get-Item/) *1e6| Name }).TotalSeconds
Note: (, (Get-Item /) * 1e6).Name - i.e., member enumeration - is the fastest by far, but this issue is about comparing streaming approaches.
Expected behavior
At the very least comparable performance.
Presumably, not having to evaluate a script block in each iteration has the potential to be faster.
I would expect a filter function to perform similarly to the script-block solution.
Actual behavior
The operation statement is noticeably slower than the script block.
Sample timings (averaged across 10 runs) on Windows, macOs, Ubuntu show varying ratios, ranging from the operation statement taking 1.24 as long to 1.58 to 1.85, respectively.
The factor for Windows PowerShell was 1.40.
The filter function is by far the fastest, by a factor of about 10 on macOS.
Environment data
PowerShell Core v6.1.0-rc.1 on macOS 10.13.6
PowerShell Core v6.1.0-rc.1 on Ubuntu 16.04.5 LTS
PowerShell Core v6.1.0-rc.1 on Microsoft Windows 10 Pro (64-bit; Version 1803, OS Build: 17134.165)
Windows PowerShell v5.1.17134.165 on Microsoft Windows 10 Pro (64-bit; Version 1803, OS Build: 17134.165)
First, my first few runs of the test had wildly varying results (on PS5.1), but for the life of me I don't know why. After some experimentation I ended up with this, which gave more consistent results:
$big=, (Get-Item/) *1e6# PSv3+ operation statement (using parameters directly rather than a script block)
(Measure-Command { $big|ForEach-Object Name }).TotalSeconds
# Script block
(Measure-Command { $big|ForEach-Object { $_.Name } }).TotalSeconds
On Windows PowerShell 5.1, direct property access takes me 12-13 seconds, whereas the scriptblock takes 6-7.
On PowerShell Core 6.1.0-rc.1 (official release; not one I built myself) (windows amd64), direct property access still takes 12-13 seconds... but scriptblock now takes 9 seconds.
So I do observe a slowdown from PS5.1 to PS6... but it's in scriptblock processing.
Oh... wait... you are saying that you expect direct prop access to be not slower than scriptblock...
Well, the two have very different code paths... and it's not hard to pick out a thing or two that seem like they would make things way slower, such as in the prop access path, we format a "should process" string, and then call ShouldProcessfor every single input object.
// should processstringpropertyAction=String.Format(CultureInfo.InvariantCulture,InternalCommandStrings.ForEachObjectPropertyAction,resolvedPropertyName);if(ShouldProcess(_targetString,propertyAction)){try{WriteToPipelineWithUnrolling(_propGetter.GetValue(InputObject,resolvedPropertyName));}
Whereas for the scriptblock path, we just blast through and execute the script for every input.
At the very least you could try lifting out the propertyAction string to avoid the String.Format for every input, but note that you can't just lift it straight out--the resolvedPropertyName could theoretically be different for every input object (but not very likely...)--so you would have to cache it, for strictly equivalent behavior.
But I don't know how much difference that would make... maybe ShouldProcess is more expensive. :/
Anyway, has anybody else noticed/filed the slowdown of the scriptblock path from 5.1 to 6?
changed the title [-]ForEach-Object operation statements are unexpectedly slower than script blocks[/-][+]ForEach-Object operation statements are unexpectedly slower than script blocks; both are much slower than filter functions[/+]on Apr 19, 2019
// But if we don't write anything to the pipeline when no member is found,
// the result 3 will not be generated.
WriteObject(null);
}
}
}
break;
It's long, it's convoluted, and a significant portion of it is long blocks of comments. All of this leads me to believe that there should be a much simpler solution available. I'm not certain if I have the background know-how in terms of improving it significantly, but it certainly looks like it could use some improvement.
It would be a mistake to make this just about member resolution. I mean, yes, the member resolution needs to be faster, but even scriptblock execution is an order of magnitude slower than just the call operator with a scriptblock.
Obviously the fastest way (by far) to get a single member is just to use member unrolling (i.e., wrap the expression in a parenthesis and write the property name), which is several times faster than even a filter...
Duration
CommandLine
6.91423
$Names = $lots | % Name
4.41294
$Names = $lots | % { $_.Name }
0.83100
$Names = foreach($f in $lots) { $f.Name }
0.64787
$Names = $lots | nameFilter
0.55108
$Names = $lots | &{process{$_.ToString()}}
0.20108
$Names = $lots.Name
My point is that while we should be concerned about the speed of % Name versus % { $_.Name } this cmdlet is slower in everything it does than any other way of doing those things!
We should also be very concerned about the speed of % { $_.Name } versus &{process{ $_. Name }} or even foreach($f in $lots) { $f.Name } ...
Most people think of (...) | ForEach-Object { ... } as basically the same as foreach(...){ ... } but it actually takes 5x as long!
Reacted by Michael Klement, Rain Sallow (/u/ta11ow), ExE Boss and Flavien MICHALECZEK
Joel Bennett (@Jaykul) Not that it fully explains the slowdown, but if you look at #9408 there's some definite weirdness happening with the scriptblocks in this cmdlet.
Weirdness? There's a few ways you can pass them in, but that's all just to support calling it without explicitly naming the script blocks (and still allow infinitely many process blocks).
I doubt that's going to account for the slowdown. I'm guessing most of it is due to trying to cheat scope.
This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.
microsoft-github-policy-service commented on Nov 16, 2023
This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.
microsoft-github-policy-service commented on Nov 16, 2023
This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.
Activity
jazzdelightsme commentedon Sep 6, 2018
I'm seeing very different results.
First, my first few runs of the test had wildly varying results (on PS5.1), but for the life of me I don't know why. After some experimentation I ended up with this, which gave more consistent results:
On Windows PowerShell 5.1, direct property access takes me 12-13 seconds, whereas the scriptblock takes 6-7.
On PowerShell Core 6.1.0-rc.1 (official release; not one I built myself) (windows amd64), direct property access still takes 12-13 seconds... but scriptblock now takes 9 seconds.
So I do observe a slowdown from PS5.1 to PS6... but it's in scriptblock processing.
Oh... wait... you are saying that you expect direct prop access to be not slower than scriptblock...
Well, the two have very different code paths... and it's not hard to pick out a thing or two that seem like they would make things way slower, such as in the prop access path, we format a "should process" string, and then call
ShouldProcessfor every single input object.Whereas for the scriptblock path, we just blast through and execute the script for every input.
At the very least you could try lifting out the
propertyActionstring to avoid theString.Formatfor every input, but note that you can't just lift it straight out--theresolvedPropertyNamecould theoretically be different for every input object (but not very likely...)--so you would have to cache it, for strictly equivalent behavior.But I don't know how much difference that would make... maybe
ShouldProcessis more expensive. :/Anyway, has anybody else noticed/filed the slowdown of the scriptblock path from 5.1 to 6?
[-]ForEach-Object operation statements are unexpectedly slower than script blocks[/-][+]ForEach-Object operation statements are unexpectedly slower than script blocks; both are much slower than filter functions[/+]vexx32 commentedon Apr 20, 2019
This is the relevant code that handles member resolution and method/property access for ForEach-Object:
PowerShell/src/System.Management.Automation/engine/InternalCommands.cs
Lines 317 to 596 in 759c4ab
It's long, it's convoluted, and a significant portion of it is long blocks of comments. All of this leads me to believe that there should be a much simpler solution available. I'm not certain if I have the background know-how in terms of improving it significantly, but it certainly looks like it could use some improvement.
Jaykul commentedon Apr 21, 2019
It would be a mistake to make this just about member resolution. I mean, yes, the member resolution needs to be faster, but even scriptblock execution is an order of magnitude slower than just the call operator with a scriptblock.
Obviously the fastest way (by far) to get a single member is just to use member unrolling (i.e., wrap the expression in a parenthesis and write the property name), which is several times faster than even a filter...
My point is that while we should be concerned about the speed of
% Nameversus% { $_.Name }this cmdlet is slower in everything it does than any other way of doing those things!We should also be very concerned about the speed of
% { $_.Name }versus&{process{ $_. Name }}or evenforeach($f in $lots) { $f.Name }...Most people think of
(...) | ForEach-Object { ... }as basically the same asforeach(...){ ... }but it actually takes 5x as long!vexx32 commentedon Apr 21, 2019
Joel Bennett (@Jaykul) Not that it fully explains the slowdown, but if you look at #9408 there's some definite weirdness happening with the scriptblocks in this cmdlet.
Jaykul commentedon Apr 22, 2019
Weirdness? There's a few ways you can pass them in, but that's all just to support calling it without explicitly naming the script blocks (and still allow infinitely many
processblocks).I doubt that's going to account for the slowdown. I'm guessing most of it is due to trying to cheat scope.
microsoft-github-policy-service commentedon Nov 16, 2023
This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.
microsoft-github-policy-service commentedon Nov 16, 2023
This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.
microsoft-github-policy-service commentedon Nov 16, 2023
This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.
microsoft-github-policy-service commentedon Nov 23, 2023
This issue has been marked as "No Activity" as there has been no activity for 6 months. It has been closed for housekeeping purposes.