다음을 통해 공유


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

래핑되지 않은 예제는372배 더 빠릅니다. 또한 첫 번째 구현에는 이후 구현에 필요하지 않은 추가 매개 변수가 필요합니다.

개체 만들기

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