2010/09/20
PowerShellでもStringBuilderを使おう
文字列を連結する際、+=演算子などを使うとループ回数によっては非常に時間がかかることがあります。これは+=演算子が実行されるたびに毎回string型の新しいインスタンスを生成しているからです。同じ文字列変数に対して+=演算子で文字列を追加していくループがある場合は、+=演算子の代わりにStringBuilderクラスを使うのが良いです。
たとえば、
$str="" @("aaaa","bbbb","cccc","dddd")| %{$str += $_} $str
というようなコードと同様の結果を得るためには、
$sb=New-Object System.Text.StringBuilder @("aaaa","bbbb","cccc","dddd")| %{[void]$sb.Append($_)} $sb.ToString()
のようにします。[void]にキャストしているのは、AppendメソッドがStringBuilderのインスタンスを返すため、それを表示させないようにするためです。結果はいずれも
aaaabbbbccccdddd
となり、文字列の連結が可能です。
このようにループ回数が少ない場合はそれほど所要時間に差はないのですが、数千の文字列を連結していく場合だと+=演算子を使うと非常に時間がかかります。どれくらい差が出るのか、スクリプトを書いて検証してみました。
function Measure-StringJoinCommand { param([int]$itemCount) $randomStrs=@() @(1..$itemCount)|%{$randomStrs += Get-Random} $StringJoinTime=@() $StringBuilderTime=@() @(1..3)| %{ $StringJoinTime += (Measure-Command { $str="" $randomStrs|%{$str+=$_} $str }).Ticks $StringBuilderTime += (Measure-Command { $sb=New-Object System.Text.StringBuilder $randomStrs|%{[void]$sb.Append($_)} $sb.ToString() }).Ticks } $result=New-Object psobject $result|Add-Member -MemberType noteproperty -Name ElementCount -Value $itemCount $result|Add-Member -MemberType noteproperty -Name StringJoinTime -Value (($StringJoinTime|Measure-Object -Average).Average/10000) $result|Add-Member -MemberType noteproperty -Name StringBuilderTime -Value (($StringBuilderTime|Measure-Object -Average).Average/10000) $result|Add-Member -MemberType noteproperty -Name StringJoinTicksPerElement -Value (($StringJoinTime|Measure-Object -Average).Average/$itemCount) $result|Add-Member -MemberType noteproperty -Name StringBuilderTicksPerElement -Value (($StringBuilderTime|Measure-Object -Average).Average/$itemCount) $result } @(100,300,500,800,1000,3000,5000,8000,10000,30000,50000,80000)| %{Measure-StringJoinCommand -itemCount $_}| Format-Table -Property ` @{Label="Elements";Expression={$_.ElementCount.ToString("#,##0")};Width=8}, @{Label="StringJoin(msec)";Expression={$_.StringJoinTime.ToString("#,##0")};Width=20}, @{Label="StringBuilder(msec)";Expression={$_.StringBuilderTime.ToString("#,##0")};Width=20}, @{Label="StringJoin(tick/element)";Expression={$_.StringJoinTicksPerElement.ToString("#,##0")};Width=30}, @{Label="StringBuilder(tick/element)";Expression={$_.StringBuilderTicksPerElement.ToString("#,##0")};Width=30}
CPU=Intel(R) Core(TM)2 CPU 6600 @ 2.40GHz,memory=3GB,Windows 7 x86での実行結果は次の通り
Elements StringJoin(msec) StringBuilder(msec) StringJoin(tick/element) StringBuilder(tick/element) -------- ---------------- ------------------- ------------------------ --------------------------- 100 8 6 830 646 300 22 22 719 746 500 36 34 718 689 800 60 55 755 690 1,000 86 72 857 721 3,000 264 192 880 638 5,000 573 334 1,146 667 8,000 1,534 522 1,917 653 10,000 2,562 731 2,562 731 30,000 23,386 2,204 7,795 735 50,000 60,805 3,730 12,161 746 80,000 184,407 6,541 23,051 818
この表は左から、連結する文字列要素数(ループ回数)、+=演算子を使った場合の所要時間(ミリ秒)、StringBuilderを使った場合の所要時間(ミリ秒)、+=演算子を使った場合の1要素あたりの所要時間(tick=100ナノ秒)、StringBuilderを使った場合の1要素あたりの所要時間(tick=100ナノ秒)、です。なお1要素当たりの平均文字数は9文字程度です。また、測定は3回おこない平均値を取っています。
この表によるとループ回数が5000回程度であれば所要時間にさほど差違は見られませんが、それ以降は急激に+=演算子の所要時間が増えることが分かります。また、+=演算子はループが5000回より増えるとループ回数が増えれば増えるほど1要素あたりにかかる時間も増えるのに対し、StringBuilderの場合はほぼ一定です。
というわけで1ループあたりに追加する文字数とループ回数が少ない場合は+=演算子でもそれほど問題にはなりませんが、そうでない場合はStringBuilderを使うのが良さそうです。これらの値が増加する可能性がある場合は、最初からStringBuilderを使っておけば、ある日突然処理がめちゃくちゃ重くなる、という事態も避けられるでしょう。PowerShellでStringBuilderを使っているサンプルがネットにはあまり見当たらなかったのですが、PowerShellでも積極的に使うと幸せになれると思います。
元記事:http://blogs.wankuma.com/mutaguchi/archive/2010/09/20/193089.aspx
Copyright © 2005-2018 Daisuke Mutaguchi All rights reserved
mailto: mutaguchi at roy.hi-ho.ne.jp
プライバシーポリシー