読者です 読者をやめる 読者になる 読者になる

tech.guitarrapc.cóm

C#, PowerShell, Unity, Cloud, Serverless Technical Update and Features

PowerShell の +=演算子 パフォーマンス と その回避方法

PowerShell

配列や文字列対する += 演算子

+=演算子 で 文字列の追加を行う場合に、PowerShell の @() による Array は固定長なので配列をいちいち作り直します。

+= 演算子 での操作であれば ArrayListList でも変わりません。

これを回避するには、 +=演算子 ではなく AddメソッドStringBuilder を使う必要があります。

先に本件をまとめた記事がありますので、手元で確認しつつ見てみましょう。

PowerShell Performance: The += Operator (and When to Avoid It)

テスト概要

項目 内容
処理 1..任意の数分だけ、文字列を加える。任意の数は 100, 1000, 20000
+=処理 String, String[], ArrayList, List
Method処理 ArrayList.Add(), List.Add(), StringBuilder.Append()

[String[]][array]にするとArrayList やList と性能としては同一結果となる*1

ベンチメーク結果

+= 演算子

+= 演算子 による処理 1..100結果 (ms) 1..1000 結果(ms) 1..20000 結果(ms)
String 1.334993 10.130783 3063.76350
String[] 2.600920 180.654732 82795.10800
ArrayList 1.141817 41.127059 17757.93110
List 1.110024 42.076567 18247.20830

Method

Merhod 処理 1..100 結果(ms) 1..1000 結果(ms) 1..20000 結果(ms)
ArrayList.Add() 0.520307 4.739987 109.12396
List.Add() 0.485547 4.400812 91.80697
StringBuilder.Append() 0.528979 4.703192 96.20759

テストコード

GitHubにも置いてあります。

Benchmark_SOperatorAndMethodToString

+= 演算子

function Get-StringForeachTest{
 
    param(
    $int
    )
 
    [string]$string  = ""
 
    measure-Command{
        foreach ($i in $int)
        {
            $addString = "hello $i"
            $string += $addString
        }

        $string
    }

    # $string.GetType().FullName
}


function Get-ArrayForeachTest{
 
    param(
    $int
    )
 
    [string[]]$array  = @()
 
    measure-Command{
        foreach ($i in $int)
        {
            $addString = "hello $i"
            $array += $addString
        }

        $array
    }

    # $array.GetType().FullName

}


function Get-ArrayListForeachTest{
 
    param(
    $int
    )
 
    $arraylist  = New-Object System.Collections.ArrayList


    measure-Command{
        foreach ($i in $int)
        {
            $addString = "hello $i"
            $arraylist += $addString
        }
        
        $arraylist
    }

    # $arraylist.GetType().FullName
}



function Get-ListForeachTest{
 
    param(
    $int
    )
 
    $list  = New-Object 'System.Collections.Generic.List[System.String]'
 
    measure-Command{
        foreach ($i in $int)
        {
            $addString = "hello $i"
            $list += $addString
        }

        $list
    }

    # $list.GetType().FullName
}

Method

function Get-ArrayListAddForeachTest{
 
    param(
    $int
    )
 
    $arraylist  = New-Object System.Collections.ArrayList

    Measure-Command{ 
        foreach ($i in $int)
        {
            $addString = "hello $i"
            $arraylist.Add($addString) > $null
        }

        $arraylist.ToArray()
    }

    # $arraylist.GetType().FullName
}


function Get-ListAddForeachTest{
 
    param(
    $int
    )
 
    $list  = New-Object 'System.Collections.Generic.List[System.String]'
 
    measure-Command{
        foreach ($i in $int)
        {
            $addString = "hello $i"
            $list.Add($addString)
        }

        $list.ToArray()
    }

    # $list.GetType().FullName
}



function Get-StringBuilderForeachTest{
 
    param(
    $int
    )
 
    $stringBuilder = New-Object System.Text.StringBuilder
 
    Measure-Command{
        foreach ($i in $int)
        {
            $addString = "hello $i"
            $stringBuilder.Append($addString) > $null
        }

        $stringBuilder.ToString()
    }
    
    # $stringBuilder.GetType().FullName
}

テスト実行コード

20000回のテストに利用したコードです。

+= 演算子

$max = 20000

# += Operator

Write-Host "String += Operator 1..$max test" -ForegroundColor Cyan
Get-StringForeachTest -int (1..$max) | Measure-Object -Property TotalMilliseconds -Average | select -ExpandProperty Average

Write-Host "Array += Operator 1..$max test" -ForegroundColor Cyan
Get-ArrayForeachTest -int (1..$max) | Measure-Object -Property TotalMilliseconds -Average | select -ExpandProperty Average

Write-Host "ArrayList += Operator 1..$max test" -ForegroundColor Cyan
Get-ArrayListForeachTest -int (1..$max) | Measure-Object -Property TotalMilliseconds -Average | select -ExpandProperty Average

Write-Host "List += Operator 1..$max test" -ForegroundColor Cyan
Get-ListForeachTest -int (1..$max) | Measure-Object -Property TotalMilliseconds -Average | select -ExpandProperty Average

Method

# Method
Write-Host "ArrayList Add Method 1..$max test" -ForegroundColor Green
1..10 | %{Get-ArrayListAddForeachTest -int (1..$max)} | Measure-Object -Property TotalMilliseconds -Average | select -ExpandProperty Average

Write-Host "List Add Method 1..$max test" -ForegroundColor Green
1..10 | %{Get-ListAddForeachTest -int (1..$max)} | Measure-Object -Property TotalMilliseconds -Average | select -ExpandProperty Average

Write-Host "StringBuilder Append Method 1..$max test" -ForegroundColor Green
1..10 | %{Get-StringBuilderForeachTest -int (1..$max)} | Measure-Object -Property TotalMilliseconds -Average | select -ExpandProperty Average

まとめ

当然といえば当然の結果ですが、ベンチマークの結果は一目瞭然です。

PowerShell では、サクサク書けるような ゆるふわ感 がありますが、意識しないといけないところもあるので気を付けてください。

特に += は危なく、驚くほど処理にコストがかかります。

私としては、ListStringBuilder を使うことをおすすめします。

*1: [String[]]は、[Array]とした場合、ArrayListやList への += と同様に Object[] となり性能も同一となります。つまり[Array]としたほうが[String[]]よりも高速ですが、型がObject[]となる。