PowerShell 스크립팅 성능 고려 사항
.NET을 직접 활용하고 파이프라인을 방지하는 PowerShell 스크립트는 Idiomatic PowerShell보다 빠른 경향이 있습니다. Idiomatic PowerShell은 cmdlet 및 PowerShell 함수를 사용하며, 종종 파이프라인을 활용하고 필요한 경우에만 .NET에 의존합니다.
메모
여기에 설명된 대부분의 기술은 Idiomatic PowerShell이 아니며 PowerShell 스크립트의 가독성을 줄일 수 있습니다. 스크립트 작성자는 성능이 달리 결정되지 않는 한 Idiomatic PowerShell을 사용하는 것이 좋습니다.
출력 표시 안 함
파이프라인에 개체를 쓰지 않도록 하는 방법에는 여러 가지가 있습니다.
-
$null
할당 또는 파일 리디렉션 -
[void]
캐스팅 - 파이프에서
Out-Null
$null
할당, [void]
캐스팅 및 $null
파일 리디렉션 속도는 거의 동일합니다. 그러나 특히 PowerShell 5.1에서는 큰 루프에서 Out-Null
호출하는 속도가 훨씬 느려질 수 있습니다.
$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'
}
}
}
이러한 테스트는 PowerShell 7.3.4의 Windows 11 컴퓨터에서 실행되었습니다. 결과는 다음과 같습니다.
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
시간과 상대 속도는 하드웨어, PowerShell 버전 및 시스템의 현재 워크로드에 따라 달라질 수 있습니다.
배열 추가
항목 목록을 생성하는 작업은 종종 더하기 연산자가 있는 배열을 사용하여 수행됩니다.
$results = @()
$results += Get-Something
$results += Get-SomethingElse
$results
배열의 크기가 고정되어 있으므로 배열 추가는 비효율적입니다. 배열에 추가할 때마다 왼쪽 및 오른쪽 피연산자의 모든 요소를 저장할 수 있을 만큼 큰 새 배열이 만들어집니다. 두 피연산자의 요소가 새 배열에 복사됩니다. 작은 컬렉션의 경우 이 오버헤드는 중요하지 않을 수 있습니다. 대규모 컬렉션의 경우 성능이 저하할 수 있습니다.
몇 가지 대안이 있습니다. 실제로 배열이 필요하지 않은 경우 형식화된 제네릭 목록([List<T>]
)을 사용하는 것이 좋습니다.
$results = [System.Collections.Generic.List[object]]::new()
$results.AddRange((Get-Something))
$results.AddRange((Get-SomethingElse))
$results
배열 추가 사용의 성능 영향은 컬렉션의 크기와 추가 수에 따라 기하급수적으로 증가합니다. 이 코드는 배열 추가를 사용하고 [List<T>]
개체에서 Add(T)
메서드를 사용하여 배열에 값을 명시적으로 할당하는 방법을 비교합니다. 명시적 할당을 성능 기준선으로 정의합니다.
$tests = @{
'PowerShell Explicit Assignment' = {
param($count)
$result = foreach($i in 1..$count) {
$i
}
}
'.Add(T) 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'
}
}
}
이러한 테스트는 PowerShell 7.3.4의 Windows 11 컴퓨터에서 실행되었습니다.
CollectionSize Test TotalMilliseconds RelativeSpeed
-------------- ---- ----------------- -------------
5120 PowerShell Explicit Assignment 26.65 1x
5120 .Add(T) to List<T> 110.98 4.16x
5120 += Operator to Array 402.91 15.12x
10240 PowerShell Explicit Assignment 0.49 1x
10240 .Add(T) to List<T> 137.67 280.96x
10240 += Operator to Array 1678.13 3424.76x
102400 PowerShell Explicit Assignment 11.18 1x
102400 .Add(T) to List<T> 1384.03 123.8x
102400 += Operator to Array 201991.06 18067.18x
큰 컬렉션을 사용하는 경우 배열 추가는 List<T>
추가하는 것보다 훨씬 느립니다.
[List<T>]
개체를 사용하는 경우 [String]
또는 [Int]
같은 특정 형식으로 목록을 만들어야 합니다. 목록에 다른 형식의 개체를 추가하면 지정된 형식으로 캐스팅됩니다. 지정된 형식으로 캐스팅할 수 없는 경우 메서드는 예외를 발생합니다.
$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
목록이 다양한 유형의 개체 컬렉션이어야 하는 경우 목록 형식으로 [Object]
사용하여 만듭니다. 컬렉션에 있는 개체의 형식을 검사 하는 컬렉션을 열거할 수 있습니다.
$objectList = [System.Collections.Generic.List[object]]::new()
$objectList.Add(1)
$objectList.Add('2')
$objectList.Add(3.0)
$objectList | ForEach-Object { "$_ is $($_.GetType())" }
1 is int
2 is string
3 is double
배열이 필요한 경우 목록에서 ToArray()
메서드를 호출하거나 PowerShell에서 배열을 만들도록 할 수 있습니다.
$results = @(
Get-Something
Get-SomethingElse
)
이 예제에서 PowerShell은 배열 식 내에서 파이프라인에 기록된 결과를 저장할 [ArrayList]
만듭니다.
$results
할당하기 직전에 PowerShell은 [ArrayList]
[Object[]]
변환합니다.
문자열 추가
문자열은 변경할 수 없습니다. 문자열에 추가할 때마다 실제로 왼쪽 및 오른쪽 피연산자의 내용을 저장할 수 있을 만큼 큰 새 문자열을 만든 다음 두 피연산자의 요소를 새 문자열에 복사합니다. 작은 문자열의 경우 이 오버헤드는 중요하지 않을 수 있습니다. 큰 문자열의 경우 성능 및 메모리 사용량에 영향을 줄 수 있습니다.
두 가지 이상의 대안이 있습니다.
-
-join
연산자 문자열 연결 - .NET
[StringBuilder]
클래스는 변경 가능한 문자열을 제공합니다.
다음 예제에서는 문자열을 작성하는 세 가지 메서드의 성능을 비교합니다.
$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'
}
}
}
이러한 테스트는 PowerShell 7.4.2의 Windows 11 컴퓨터에서 실행되었습니다. 출력은 -join
연산자가 가장 빠른 다음 [StringBuilder]
클래스임을 보여 줍니다.
Iterations Test TotalMilliseconds RelativeSpeed
---------- ---- ----------------- -------------
10240 Join operator 14.75 1x
10240 StringBuilder 62.44 4.23x
10240 Addition Assignment += 619.64 42.01x
51200 Join operator 43.15 1x
51200 StringBuilder 304.32 7.05x
51200 Addition Assignment += 14225.13 329.67x
102400 Join operator 85.62 1x
102400 StringBuilder 499.12 5.83x
102400 Addition Assignment += 67640.79 790.01x
시간과 상대 속도는 하드웨어, PowerShell 버전 및 시스템의 현재 워크로드에 따라 달라질 수 있습니다.
대용량 파일 처리
PowerShell에서 파일을 처리하는 idiomatic 방법은 다음과 같습니다.
Get-Content $path | Where-Object Length -GT 10
이는 .NET API를 직접 사용하는 것보다 크기가 느려질 수 있습니다. 예를 들어 .NET [StreamReader]
클래스를 사용할 수 있습니다.
try {
$reader = [System.IO.StreamReader]::new($path)
while (-not $reader.EndOfStream) {
$line = $reader.ReadLine()
if ($line.Length -gt 10) {
$line
}
}
}
finally {
if ($reader) {
$reader.Dispose()
}
}
StreamReader
래핑하여 읽기 프로세스를 간소화하는 [System.IO.File]
ReadLines
메서드를 사용할 수도 있습니다.
foreach ($line in [System.IO.File]::ReadLines($path)) {
if ($line.Length -gt 10) {
$line
}
}
큰 컬렉션에서 속성별로 항목 조회
한 목록에서 ID를 검색하고 다른 목록에서 전자 메일을 검색하는 것과 같이 공유 속성을 사용하여 다른 컬렉션에서 동일한 레코드를 식별해야 하는 것이 일반적입니다. 첫 번째 목록을 반복하여 두 번째 컬렉션에서 일치하는 레코드를 찾는 속도가 느립니다. 특히 두 번째 컬렉션의 반복 필터링에는 오버헤드가 큽니다.
ID이름있는 컬렉션과 이름전자 메일있는 컬렉션이 두 개 있습니다.
$Employees = 1..10000 | ForEach-Object {
[PSCustomObject]@{
Id = $_
Name = "Name$_"
}
}
$Accounts = 2500..7500 | ForEach-Object {
[PSCustomObject]@{
Name = "Name$_"
Email = "Name$_@fabrikam.com"
}
}
이러한 컬렉션을 조정하여 ID, 이름및 Email 속성을 사용하여 개체 목록을 반환하는 일반적인 방법은 다음과 같습니다.
$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
}
}
그러나 이 구현은 $Employee
컬렉션의 모든 항목에 대해 $Accounts
컬렉션의 모든 5,000개 항목을 한 번 필터링해야 합니다. 이 단일 값 조회에도 몇 분 정도 걸릴 수 있습니다.
대신 공유 Name 속성을 키로 사용하고 일치하는 계정을 값으로 사용하는 해시 테이블 만들 수 있습니다.
$LookupHash = @{}
foreach ($Account in $Accounts) {
$LookupHash[$Account.Name] = $Account
}
해시 테이블에서 키를 조회하는 것은 속성 값별로 컬렉션을 필터링하는 것보다 훨씬 빠릅니다. PowerShell은 컬렉션의 모든 항목을 확인하는 대신 키가 정의되어 있는지 확인하고 해당 값을 사용할 수 있습니다.
$Results = $Employees | ForEach-Object -Process {
$Email = $LookupHash[$_.Name].Email
[pscustomobject]@{
Id = $_.Id
Name = $_.Name
Email = $Email
}
}
이것은 훨씬 빠릅니다. 반복 필터를 완료하는 데 몇 분 정도 걸렸지만 해시 조회는 1초 미만이 걸립니다.
신중하게 Write-Host 사용
Write-Host
명령은 개체를 Success 파이프라인에 쓰는 대신 호스트 콘솔에 서식이 지정된 텍스트를 작성해야 하는 경우에만 사용해야 합니다.
Write-Host
pwsh.exe
, powershell.exe
또는 powershell_ise.exe
같은 특정 호스트에 대해 [Console]::WriteLine()
크기보다 느려질 수 있습니다. 그러나 [Console]::WriteLine()
모든 호스트에서 작동하도록 보장되지는 않습니다. 또한 [Console]::WriteLine()
사용하여 작성된 출력은 Start-Transcript
시작한 대본에 기록되지 않습니다.
JIT 컴파일
PowerShell은 해석되는 바이트 코드로 스크립트 코드를 컴파일합니다. PowerShell 3부터 루프에서 반복적으로 실행되는 코드의 경우 PowerShell은 코드를 네이티브 코드로 컴파일하는 JIT(Just-In-Time)를 통해 성능을 향상시킬 수 있습니다.
지침이 300개 미만인 루프는 JIT 컴파일에 적합합니다. 이보다 큰 루프는 컴파일하기에 너무 비용이 많이 듭니다. 루프가 16번 실행되면 스크립트는 백그라운드에서 JIT 컴파일됩니다. JIT 컴파일이 완료되면 실행이 컴파일된 코드로 전송됩니다.
함수에 대한 반복 호출 방지
함수 호출은 비용이 많이 드는 작업일 수 있습니다. 장기 실행 꽉 루프에서 함수를 호출하는 경우 함수 내에서 루프를 이동하는 것이 좋습니다.
다음 예제를 고려하세요.
$tests = @{
'Simple for-loop' = {
param([int] $RepeatCount, [random] $RanGen)
for ($i = 0; $i -lt $RepeatCount; $i++) {
$null = $RanGen.Next()
}
}
'Wrapped in a function' = {
param([int] $RepeatCount, [random] $RanGen)
function Get-RandomNumberCore {
param ($rng)
$rng.Next()
}
for ($i = 0; $i -lt $RepeatCount; $i++) {
$null = Get-RandomNumberCore -rng $RanGen
}
}
'for-loop in a function' = {
param([int] $RepeatCount, [random] $RanGen)
function Get-RandomNumberAll {
param ($rng, $count)
for ($i = 0; $i -lt $count; $i++) {
$null = $rng.Next()
}
}
Get-RandomNumberAll -rng $RanGen -count $RepeatCount
}
}
5kb, 10kb, 100kb | ForEach-Object {
$rng = [random]::new()
$groupResult = foreach ($test in $tests.GetEnumerator()) {
$ms = Measure-Command { & $test.Value -RepeatCount $_ -RanGen $rng }
[pscustomobject]@{
CollectionSize = $_
Test = $test.Key
TotalMilliseconds = [math]::Round($ms.TotalMilliseconds,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'
}
}
}
Basic for-loop 예제는 성능을 위한 기본 줄입니다. 두 번째 예제에서는 꽉 루프에서 호출되는 함수에서 난수 생성기를 래핑합니다. 세 번째 예제에서는 루프를 함수 내부로 이동합니다. 함수는 한 번만 호출되지만 코드는 여전히 동일한 양의 난수를 생성합니다. 각 예제의 실행 시간 차이를 확인합니다.
CollectionSize Test TotalMilliseconds RelativeSpeed
-------------- ---- ----------------- -------------
5120 for-loop in a function 9.62 1x
5120 Simple for-loop 10.55 1.1x
5120 Wrapped in a function 62.39 6.49x
10240 Simple for-loop 17.79 1x
10240 for-loop in a function 18.48 1.04x
10240 Wrapped in a function 127.39 7.16x
102400 for-loop in a function 179.19 1x
102400 Simple for-loop 181.58 1.01x
102400 Wrapped in a function 1155.57 6.45x
cmdlet 파이프라인 래핑 방지
대부분의 cmdlet은 순차적 구문 및 프로세스인 파이프라인에 대해 구현됩니다. 예를 들어:
cmdlet1 | cmdlet2 | cmdlet3
새 파이프라인 초기화는 비용이 많이 들 수 있으므로 cmdlet 파이프라인을 다른 기존 파이프라인으로 래핑하지 않아야 합니다.
다음 예제를 고려해 보세요.
Input.csv
파일에는 2100줄이 포함되어 있습니다.
Export-Csv
명령은 ForEach-Object
파이프라인 내부에 래핑됩니다.
Export-Csv
cmdlet은 ForEach-Object
루프의 모든 반복에 대해 호출됩니다.
$measure = Measure-Command -Expression {
Import-Csv .\Input.csv | ForEach-Object -Begin { $Id = 1 } -Process {
[PSCustomObject]@{
Id = $Id
Name = $_.opened_by
} | Export-Csv .\Output1.csv -Append
}
}
'Wrapped = {0:N2} ms' -f $measure.TotalMilliseconds
Wrapped = 15,968.78 ms
다음 예제에서는 Export-Csv
명령이 ForEach-Object
파이프라인 외부로 이동되었습니다.
이 경우 Export-Csv
한 번만 호출되지만 여전히 ForEach-Object
전달된 모든 개체를 처리합니다.
$measure = Measure-Command -Expression {
Import-Csv .\Input.csv | ForEach-Object -Begin { $Id = 2 } -Process {
[PSCustomObject]@{
Id = $Id
Name = $_.opened_by
}
} | Export-Csv .\Output2.csv
}
'Unwrapped = {0:N2} ms' -f $measure.TotalMilliseconds
Unwrapped = 42.92 ms
래핑되지 않은 예제는
개체 만들기
New-Object
cmdlet을 사용하여 개체를 만드는 속도가 느려질 수 있습니다. 다음 코드는 New-Object
cmdlet을 사용하여 개체를 만드는 성능을 [pscustomobject]
형식 가속기와 비교합니다.
Measure-Command {
$test = 'PSCustomObject'
for ($i = 0; $i -lt 100000; $i++) {
$resultObject = [PSCustomObject]@{
Name = 'Name'
Path = 'FullName'
}
}
} | Select-Object @{n='Test';e={$test}},TotalSeconds
Measure-Command {
$test = 'New-Object'
for ($i = 0; $i -lt 100000; $i++) {
$resultObject = New-Object -TypeName PSObject -Property @{
Name = 'Name'
Path = 'FullName'
}
}
} | Select-Object @{n='Test';e={$test}},TotalSeconds
Test TotalSeconds
---- ------------
PSCustomObject 0.48
New-Object 3.37
PowerShell 5.0은 모든 .NET 형식에 대한 new()
정적 메서드를 추가했습니다. 다음 코드는 New-Object
cmdlet을 사용하여 개체를 만드는 성능을 new()
메서드와 비교합니다.
Measure-Command {
$test = 'new() method'
for ($i = 0; $i -lt 100000; $i++) {
$sb = [System.Text.StringBuilder]::new(1000)
}
} | Select-Object @{n='Test';e={$test}},TotalSeconds
Measure-Command {
$test = 'New-Object'
for ($i = 0; $i -lt 100000; $i++) {
$sb = New-Object -TypeName System.Text.StringBuilder -ArgumentList 1000
}
} | Select-Object @{n='Test';e={$test}},TotalSeconds
Test TotalSeconds
---- ------------
new() method 0.59
New-Object 3.17
OrderedDictionary를 사용하여 동적으로 새 개체 만들기
일부 입력을 기반으로 개체를 동적으로 만들어야 하는 경우가 있습니다. 가장 일반적으로 사용되는 방법으로 새 PSObject 만든 다음 Add-Member
cmdlet을 사용하여 새 속성을 추가합니다. 이 기술을 사용하는 소규모 컬렉션의 성능 비용은 무시할 수 있지만 큰 컬렉션에서는 매우 두드러질 수 있습니다. 이 경우 [OrderedDictionary]
사용한 다음 [pscustomobject]
형식 가속기를 사용하여 PSObject 변환하는 것이 좋습니다. 자세한 내용은 about_Hash_Tables순서가 지정된 사전 만들기 섹션을 참조하세요.
변수 $json
다음 API 응답이 저장되어 있다고 가정합니다.
{
"tables": [
{
"name": "PrimaryResult",
"columns": [
{ "name": "Type", "type": "string" },
{ "name": "TenantId", "type": "string" },
{ "name": "count_", "type": "long" }
],
"rows": [
[ "Usage", "63613592-b6f7-4c3d-a390-22ba13102111", "1" ],
[ "Usage", "d436f322-a9f4-4aad-9a7d-271fbf66001c", "1" ],
[ "BillingFact", "63613592-b6f7-4c3d-a390-22ba13102111", "1" ],
[ "BillingFact", "d436f322-a9f4-4aad-9a7d-271fbf66001c", "1" ],
[ "Operation", "63613592-b6f7-4c3d-a390-22ba13102111", "7" ],
[ "Operation", "d436f322-a9f4-4aad-9a7d-271fbf66001c", "5" ]
]
}
]
}
이제 이 데이터를 CSV로 내보내려고 합니다. 먼저 새 개체를 만들고 Add-Member
cmdlet을 사용하여 속성과 값을 추가해야 합니다.
$data = $json | ConvertFrom-Json
$columns = $data.tables.columns
$result = foreach ($row in $data.tables.rows) {
$obj = [psobject]::new()
$index = 0
foreach ($column in $columns) {
$obj | Add-Member -MemberType NoteProperty -Name $column.name -Value $row[$index++]
}
$obj
}
OrderedDictionary
사용하여 코드를 다음과 같이 변환할 수 있습니다.
$data = $json | ConvertFrom-Json
$columns = $data.tables.columns
$result = foreach ($row in $data.tables.rows) {
$obj = [ordered]@{}
$index = 0
foreach ($column in $columns) {
$obj[$column.name] = $row[$index++]
}
[pscustomobject] $obj
}
두 경우 모두 $result
출력은 동일합니다.
Type TenantId count_
---- -------- ------
Usage 63613592-b6f7-4c3d-a390-22ba13102111 1
Usage d436f322-a9f4-4aad-9a7d-271fbf66001c 1
BillingFact 63613592-b6f7-4c3d-a390-22ba13102111 1
BillingFact d436f322-a9f4-4aad-9a7d-271fbf66001c 1
Operation 63613592-b6f7-4c3d-a390-22ba13102111 7
Operation d436f322-a9f4-4aad-9a7d-271fbf66001c 5
개체 및 멤버 속성 수가 증가함에 따라 후자의 접근 방식이 기하급수적으로 더 효율적입니다.
다음은 5개의 속성이 있는 개체를 만들기 위한 세 가지 기술의 성능 비교입니다.
$tests = @{
'[ordered] into [pscustomobject] cast' = {
param([int] $iterations, [string[]] $props)
foreach ($i in 1..$iterations) {
$obj = [ordered]@{}
foreach ($prop in $props) {
$obj[$prop] = $i
}
[pscustomobject] $obj
}
}
'Add-Member' = {
param([int] $iterations, [string[]] $props)
foreach ($i in 1..$iterations) {
$obj = [psobject]::new()
foreach ($prop in $props) {
$obj | Add-Member -MemberType NoteProperty -Name $prop -Value $i
}
$obj
}
}
'PSObject.Properties.Add' = {
param([int] $iterations, [string[]] $props)
# this is how, behind the scenes, `Add-Member` attaches
# new properties to our PSObject.
# Worth having it here for performance comparison
foreach ($i in 1..$iterations) {
$obj = [psobject]::new()
foreach ($prop in $props) {
$obj.PSObject.Properties.Add(
[psnoteproperty]::new($prop, $i))
}
$obj
}
}
}
$properties = 'Prop1', 'Prop2', 'Prop3', 'Prop4', 'Prop5'
1kb, 10kb, 100kb | ForEach-Object {
$groupResult = foreach ($test in $tests.GetEnumerator()) {
$ms = Measure-Command { & $test.Value -iterations $_ -props $properties }
[pscustomobject]@{
Iterations = $_
Test = $test.Key
TotalMilliseconds = [math]::Round($ms.TotalMilliseconds, 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'
}
}
}
그리고 결과는 다음과 같습니다.
Iterations Test TotalMilliseconds RelativeSpeed
---------- ---- ----------------- -------------
1024 [ordered] into [pscustomobject] cast 22.00 1x
1024 PSObject.Properties.Add 153.17 6.96x
1024 Add-Member 261.96 11.91x
10240 [ordered] into [pscustomobject] cast 65.24 1x
10240 PSObject.Properties.Add 1293.07 19.82x
10240 Add-Member 2203.03 33.77x
102400 [ordered] into [pscustomobject] cast 639.83 1x
102400 PSObject.Properties.Add 13914.67 21.75x
102400 Add-Member 23496.08 36.72x
관련 링크
PowerShell