A PowerShell-szkriptek teljesítményével kapcsolatos szempontok
A .NET-et közvetlenül használó és a folyamatot elkerülő PowerShell-szkriptek általában gyorsabbak, mint az idiomatikus PowerShell. Az idiomatikus PowerShell parancsmagokat és PowerShell-függvényeket használ, gyakran használja a folyamatot, és csak akkor használja a .NET-et, ha szükséges.
Feljegyzés
Az itt ismertetett technikák közül számos nem idiomatikus PowerShell, és csökkentheti a PowerShell-szkriptek olvashatóságát. A szkriptkészítőknek ajánlott idiomatikus PowerShellt használniuk, hacsak a teljesítmény másként nem diktál.
Kimenet letiltása
Számos módon kerülheti el, hogy objektumokat írjon a folyamatba.
- Hozzárendelés a
$null
- Öntés a
[void]
- Fájl átirányítása ide:
$null
- Cső a
Out-Null
A hozzárendelés $null
, a kiosztás [void]
és a fájlátirányítás $null
sebessége szinte azonos. A nagy hurok hívása Out-Null
azonban jelentősen lassabb lehet, különösen a PowerShell 5.1-ben.
$tests = @{
'Assign to $null' = {
$arrayList = [System.Collections.ArrayList]::new()
foreach ($i in 0..$args[0]) {
$null = $arraylist.Add($i)
}
}
'Cast to [void]' = {
$arrayList = [System.Collections.ArrayList]::new()
foreach ($i in 0..$args[0]) {
[void]$arraylist.Add($i)
}
}
'Redirect to $null' = {
$arrayList = [System.Collections.ArrayList]::new()
foreach ($i in 0..$args[0]) {
$arraylist.Add($i) > $null
}
}
'Pipe to Out-Null' = {
$arrayList = [System.Collections.ArrayList]::new()
foreach ($i in 0..$args[0]) {
$arraylist.Add($i) | Out-Null
}
}
}
10kb, 50kb, 100kb | ForEach-Object {
$groupResult = foreach ($test in $tests.GetEnumerator()) {
$ms = (Measure-Command { & $test.Value $_ }).TotalMilliseconds
[pscustomobject]@{
Iterations = $_
Test = $test.Key
TotalMilliseconds = [math]::Round($ms, 2)
}
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
}
$groupResult = $groupResult | Sort-Object TotalMilliseconds
$groupResult | Select-Object *, @{
Name = 'RelativeSpeed'
Expression = {
$relativeSpeed = $_.TotalMilliseconds / $groupResult[0].TotalMilliseconds
[math]::Round($relativeSpeed, 2).ToString() + 'x'
}
}
}
Ezeket a teszteket Windows 11 rendszerű gépen futtatták a PowerShell 7.3.4-ben. Az eredmények az alábbiakban láthatók:
Iterations Test TotalMilliseconds RelativeSpeed
---------- ---- ----------------- -------------
10240 Assign to $null 36.74 1x
10240 Redirect to $null 55.84 1.52x
10240 Cast to [void] 62.96 1.71x
10240 Pipe to Out-Null 81.65 2.22x
51200 Assign to $null 193.92 1x
51200 Cast to [void] 200.77 1.04x
51200 Redirect to $null 219.69 1.13x
51200 Pipe to Out-Null 329.62 1.7x
102400 Redirect to $null 386.08 1x
102400 Assign to $null 392.13 1.02x
102400 Cast to [void] 405.24 1.05x
102400 Pipe to Out-Null 572.94 1.48x
Az idő és a relatív sebesség a hardvertől, a PowerShell verziójától és a rendszer aktuális számítási feladataitól függően változhat.
Tömb hozzáadása
Az elemek listájának létrehozása gyakran egy tömb használatával történik, a hozzáadás operátorral:
$results = @()
$results += Do-Something
$results += Do-SomethingElse
$results
A tömbök hozzáadása nem hatékony, mert a tömbök mérete rögzített. A tömb minden egyes hozzáadása létrehoz egy új tömböt, amely elég nagy ahhoz, hogy mind a bal, mind a jobb operandus összes elemét megtartsa. A rendszer mindkét operandus elemeit az új tömbbe másolja. A kis gyűjtemények esetében ez a többletterhelés nem feltétlenül számít. A nagy gyűjtemények teljesítménycsökkenést okozhatnak.
Van néhány alternatíva. Ha valójában nincs szüksége tömbre, fontolja meg egy beírt általános lista (T> lista<) használatát:
$results = [System.Collections.Generic.List[object]]::new()
$results.AddRange((Do-Something))
$results.AddRange((Do-SomethingElse))
$results
A tömbök összeadásának teljesítményhatása exponenciálisan nő a gyűjtemény méretével és a szám összeadásaival. Ez a kód összehasonlítja az értékek tömbhöz való explicit hozzárendelését a tömbök összeadásával és a T> lista<metódusának Add()
használatával. Explicit hozzárendelést határoz meg a teljesítmény alapkonfigurációjaként.
$tests = @{
'PowerShell Explicit Assignment' = {
param($count)
$result = foreach($i in 1..$count) {
$i
}
}
'.Add(..) to List<T>' = {
param($count)
$result = [Collections.Generic.List[int]]::new()
foreach($i in 1..$count) {
$result.Add($i)
}
}
'+= Operator to Array' = {
param($count)
$result = @()
foreach($i in 1..$count) {
$result += $i
}
}
}
5kb, 10kb, 100kb | ForEach-Object {
$groupResult = foreach($test in $tests.GetEnumerator()) {
$ms = (Measure-Command { & $test.Value -Count $_ }).TotalMilliseconds
[pscustomobject]@{
CollectionSize = $_
Test = $test.Key
TotalMilliseconds = [math]::Round($ms, 2)
}
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
}
$groupResult = $groupResult | Sort-Object TotalMilliseconds
$groupResult | Select-Object *, @{
Name = 'RelativeSpeed'
Expression = {
$relativeSpeed = $_.TotalMilliseconds / $groupResult[0].TotalMilliseconds
[math]::Round($relativeSpeed, 2).ToString() + 'x'
}
}
}
Ezeket a teszteket Windows 11 rendszerű gépen futtatták a PowerShell 7.3.4-ben.
CollectionSize Test TotalMilliseconds RelativeSpeed
-------------- ---- ----------------- -------------
5120 PowerShell Explicit Assignment 26.65 1x
5120 .Add(..) to List<T> 110.98 4.16x
5120 += Operator to Array 402.91 15.12x
10240 PowerShell Explicit Assignment 0.49 1x
10240 .Add(..) to List<T> 137.67 280.96x
10240 += Operator to Array 1678.13 3424.76x
102400 PowerShell Explicit Assignment 11.18 1x
102400 .Add(..) to List<T> 1384.03 123.8x
102400 += Operator to Array 201991.06 18067.18x
Ha nagy gyűjteményekkel dolgozik, a tömbök hozzáadása jelentősen lassabb, mint a T> lista<hozzáadása.
A T> lista<használatakor létre kell hoznia a listát egy adott típussal, például sztringgel vagy inttel. Ha más típusú objektumokat ad hozzá a listához, azok a megadott típusba kerülnek. Ha nem lehet a megadott típusra leadni őket, a metódus kivételt hoz létre.
$intList = [System.Collections.Generic.List[int]]::new()
$intList.Add(1)
$intList.Add('2')
$intList.Add(3.0)
$intList.Add('Four')
$intList
MethodException:
Line |
5 | $intList.Add('Four')
| ~~~~~~~~~~~~~~~~~~~~
| Cannot convert argument "item", with value: "Four", for "Add" to type
"System.Int32": "Cannot convert value "Four" to type "System.Int32".
Error: "The input string 'Four' was not in a correct format.""
1
2
3
Ha azt szeretné, hogy a lista különböző típusú objektumok gyűjteménye legyen, hozza létre az objektumot listatípusként. A gyűjtemény számbavételével megvizsgálhatja a benne lévő objektumok típusait.
$objectList = [System.Collections.Generic.List[object]]::new()
$objectList.Add(1)
$objectList.Add('2')
$objectList.Add(3.0)
$objectList.GetEnumerator().ForEach({ "$_ is $($_.GetType())" })
1 is int
2 is string
3 is double
Ha tömbre van szüksége, meghívhatja a ToArray()
metódust a listában, vagy engedélyezheti a PowerShell számára a tömb létrehozását:
$results = @(
Do-Something
Do-SomethingElse
)
Ebben a példában a PowerShell létrehoz egy Tömblistát , amely a tömbkifejezésen belül a folyamatba írt eredményeket tárolja. A PowerShell a hozzárendelés $results
előtt objektummá alakítja a Tömblistát[].
Sztring hozzáadása
A sztringek nem módosíthatók. A sztring minden egyes hozzáadása létrehoz egy új sztringet, amely elég nagy ahhoz, hogy mind a bal, mind a jobb operandus tartalmát megtartsa, majd mindkét operandus elemeit átmásolja az új sztringbe. Kis sztringek esetén ez a többletterhelés nem feltétlenül számít. Nagy sztringek esetén ez hatással lehet a teljesítményre és a memóriahasználatra.
Legalább két alternatíva létezik:
- Az
-join
operátor összefűzi a sztringeket - A .NET StringBuilder osztály egy mutable sztringet biztosít
Az alábbi példa a sztringek létrehozásának három módszerét hasonlítja össze.
$tests = @{
'StringBuilder' = {
$sb = [System.Text.StringBuilder]::new()
foreach ($i in 0..$args[0]) {
$sb = $sb.AppendLine("Iteration $i")
}
$sb.ToString()
}
'Join operator' = {
$string = @(
foreach ($i in 0..$args[0]) {
"Iteration $i"
}
) -join "`n"
$string
}
'Addition Assignment +=' = {
$string = ''
foreach ($i in 0..$args[0]) {
$string += "Iteration $i`n"
}
$string
}
}
10kb, 50kb, 100kb | ForEach-Object {
$groupResult = foreach ($test in $tests.GetEnumerator()) {
$ms = (Measure-Command { & $test.Value $_ }).TotalMilliseconds
[pscustomobject]@{
Iterations = $_
Test = $test.Key
TotalMilliseconds = [math]::Round($ms, 2)
}
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
}
$groupResult = $groupResult | Sort-Object TotalMilliseconds
$groupResult | Select-Object *, @{
Name = 'RelativeSpeed'
Expression = {
$relativeSpeed = $_.TotalMilliseconds / $groupResult[0].TotalMilliseconds
[math]::Round($relativeSpeed, 2).ToString() + 'x'
}
}
}
Ezeket a teszteket Windows 10 rendszerű gépen futtatták a PowerShell 7.3.4-ben. A kimenet azt mutatja, hogy az -join
operátor a leggyorsabb, amelyet a StringBuilder osztály követ.
Iterations Test TotalMilliseconds RelativeSpeed
---------- ---- ----------------- -------------
10240 Join operator 7.08 1x
10240 StringBuilder 54.10 7.64x
10240 Addition Assignment += 724.16 102.28x
51200 Join operator 41.76 1x
51200 StringBuilder 318.06 7.62x
51200 Addition Assignment += 17693.06 423.68x
102400 Join operator 106.98 1x
102400 StringBuilder 543.84 5.08x
102400 Addition Assignment += 90693.13 847.76x
Az idő és a relatív sebesség a hardvertől, a PowerShell verziójától és a rendszer aktuális számítási feladataitól függően változhat.
Nagyméretű fájlok feldolgozása
A Fájlok PowerShellben történő feldolgozásának idiomatikus módja a következőhöz hasonló lehet:
Get-Content $path | Where-Object { $_.Length -gt 10 }
Ez nagyságrenddel lassabb lehet, mint a .NET API-k közvetlen használata:
try
{
$stream = [System.IO.StreamReader]::new($path)
while ($line = $stream.ReadLine())
{
if ($line.Length -gt 10)
{
$line
}
}
}
finally
{
$stream.Dispose()
}
Bejegyzések keresése tulajdonság szerint nagy gyűjteményekben
Gyakran előfordul, hogy egy megosztott tulajdonság használatával azonos rekordot kell azonosítani a különböző gyűjteményekben, például egy névvel lekérni egy azonosítót az egyik listából, és egy e-mailt egy másikból. Lassú az iterálás az első listában, hogy megtalálja az egyező rekordot a második gyűjteményben. Különösen a második gyűjtemény ismételt szűrése nagy többletterheléssel jár.
Két gyűjteményt adott meg, az egyik azonosítóval és névvel, a másik pedig névvel és e-mail-címmel:
$Employees = 1..10000 | ForEach-Object {
[PSCustomObject]@{
Id = $_
Name = "Name$_"
}
}
$Accounts = 2500..7500 | ForEach-Object {
[PSCustomObject]@{
Name = "Name$_"
Email = "Name$_@fabrikam.com"
}
}
A gyűjtemények egyeztetésének szokásos módja, hogy visszaadja az objektumok listáját az azonosító, a név és az e-mail tulajdonságaival, a következőképpen nézhet ki:
$Results = $Employees | ForEach-Object -Process {
$Employee = $_
$Account = $Accounts | Where-Object -FilterScript {
$_.Name -eq $Employee.Name
}
[pscustomobject]@{
Id = $Employee.Id
Name = $Employee.Name
Email = $Account.Email
}
}
Ennek a megvalósításnak azonban a gyűjteményben lévő $Accounts
5000 elemet egyszer kell szűrnie a gyűjtemény minden elemére $Employee
. Ez akár perceket is igénybe vehet, még ehhez az egyértékű kereséshez is.
Ehelyett létrehozhat egy kivonattáblát , amely kulcsként a megosztott név tulajdonságot, az egyező fiókot pedig értékként használja.
$LookupHash = @{}
foreach ($Account in $Accounts) {
$LookupHash[$Account.Name] = $Account
}
A kulcsok keresése egy kivonattáblában sokkal gyorsabb, mint a gyűjtemények tulajdonságértékek szerinti szűrése. A gyűjtemény minden elemének ellenőrzése helyett a PowerShell ellenőrizheti, hogy a kulcs definiálva van-e, és használja-e az értékét.
$Results = $Employees | ForEach-Object -Process {
$Email = $LookupHash[$_.Name].Email
[pscustomobject]@{
Id = $_.Id
Name = $_.Name
Email = $Email
}
}
Ez sokkal gyorsabb. Amíg a ciklusszűrő végrehajtása percekig tartott, a kivonatkeresés kevesebb mint egy másodpercet vesz igénybe.
Írási gazdagép elkerülése
Általában rossz gyakorlatnak számít a kimenet közvetlenül a konzolra írása, de ha van értelme, sok szkript használja Write-Host
.
Ha sok üzenetet kell írnia a konzolra, nagyságrendekkel lassabb lehet, Write-Host
mint [Console]::WriteLine()
bizonyos gazdagépek esetében, például pwsh.exe
, powershell.exe
vagy powershell_ise.exe
. Azonban nem garantált, [Console]::WriteLine()
hogy minden gazdagépen működik. Emellett a használatával [Console]::WriteLine()
írt kimenet nem a kezdőbetűs Start-Transcript
átiratokra lesz megírva.
A használat Write-Host
helyett fontolja meg a Write-Output használatát.
JIT-összeállítás
A PowerShell lefordítja a szkriptkódot az értelmezett bájtkódra. A PowerShell 3-tól kezdődően a ciklusban ismétlődően végrehajtott kód esetében a PowerShell képes javítani a teljesítményt úgy, hogy a kódot natív kódba rendezi.
A 300-nál kevesebb utasítást tartalmazó hurkok jogosultak a JIT-fordításra. Azoknál nagyobb hurkok túl költségesek a fordításhoz. Amikor a ciklus 16 alkalommal lett végrehajtva, a szkript jiT-fordítást végez a háttérben. Amikor a JIT-fordítás befejeződik, a végrehajtás átkerül a lefordított kódba.
A függvények ismételt hívásának elkerülése
A függvények meghívása költséges művelet lehet. Ha egy függvényt hosszú ideig futó szoros hurokban hív meg, fontolja meg a hurok áthelyezését a függvényen belül.
Tekintse az alábbi példákat:
$ranGen = New-Object System.Random
$RepeatCount = 10000
'Basic for-loop = {0}ms' -f (Measure-Command -Expression {
for ($i = 0; $i -lt $RepeatCount; $i++) {
$Null = $ranGen.Next()
}
}).TotalMilliseconds
'Wrapped in a function = {0}ms' -f (Measure-Command -Expression {
function Get-RandNum_Core {
param ($ranGen)
$ranGen.Next()
}
for ($i = 0; $i -lt $RepeatCount; $i++) {
$Null = Get-RandNum_Core $ranGen
}
}).TotalMilliseconds
'For-loop in a function = {0}ms' -f (Measure-Command -Expression {
function Get-RandNum_All {
param ($ranGen)
for ($i = 0; $i -lt $RepeatCount; $i++) {
$Null = $ranGen.Next()
}
}
Get-RandNum_All $ranGen
}).TotalMilliseconds
Az alapszintű for-loop példa a teljesítmény alapvonala. A második példa a véletlenszerű számgenerátort egy szoros hurokban hívott függvénybe csomagolja. A harmadik példa a ciklust a függvényen belülre helyezi. A függvény csak egyszer van meghívva, de a kód továbbra is 10000 véletlenszerű számot hoz létre. Figyelje meg az egyes példák végrehajtási idejének különbségét.
Basic for-loop = 47.8668ms
Wrapped in a function = 820.1396ms
For-loop in a function = 23.3193ms
Kerülje a parancsmag-folyamatok körbefuttatását
A legtöbb parancsmag implementálva van a folyamathoz, amely szekvenciális szintaxis és folyamat. Példa:
cmdlet1 | cmdlet2 | cmdlet3
Az új folyamatok inicializálása költséges lehet, ezért kerülje a parancsmagfolyamatok egy másik meglévő folyamatba való burkolását.
Gondolja át a következő példát. A Input.csv
fájl 2100 sort tartalmaz. A Export-Csv
parancs be van csomagolva a ForEach-Object
folyamatba. A Export-Csv
parancsmag a ciklus minden iterációjára ForEach-Object
meghívódik.
'Wrapped = {0:N2} ms' -f (Measure-Command -Expression {
Import-Csv .\Input.csv | ForEach-Object -Begin { $Id = 1 } -Process {
[PSCustomObject]@{
Id = $Id
Name = $_.opened_by
} | Export-Csv .\Output1.csv -Append
}
}).TotalMilliseconds
Wrapped = 15,968.78 ms
A következő példában a parancs a Export-Csv
folyamaton kívülre ForEach-Object
került.
Ebben az esetben Export-Csv
a rendszer csak egyszer hívja meg, de továbbra is feldolgozza az összes objektumot, amelyből ForEach-Object
ki lett adva.
'Unwrapped = {0:N2} ms' -f (Measure-Command -Expression {
Import-Csv .\Input.csv | ForEach-Object -Begin { $Id = 2 } -Process {
[PSCustomObject]@{
Id = $Id
Name = $_.opened_by
}
} | Export-Csv .\Output2.csv
}).TotalMilliseconds
Unwrapped = 42.92 ms
A le nem írt példa 372-szer gyorsabb. Azt is figyelje meg, hogy az első implementációhoz szükség van a Append paraméterre, amely nem szükséges a későbbi implementációhoz.
Visszajelzés
https://aka.ms/ContentUserFeedback.
Hamarosan elérhető: 2024-ben fokozatosan kivezetjük a GitHub-problémákat a tartalom visszajelzési mechanizmusaként, és lecseréljük egy új visszajelzési rendszerre. További információ:Visszajelzés küldése és megtekintése a következőhöz: