Вводная: с данной заметке описывается как получить ускорение в 5-10 (и более раз) при обработке большого количества строк используя вместо String объект StringBuilder.

Вызов конструктора System.Text.StringBuilder:

$SomeString = New-Object System.Text.StringBuilder

Обратное преобразование в String:

$Result = $Str.ToString()

Во время написании скрипта, обрабатывающего много текстовых файлов, была обнаружена особенность работы со строками в powershell, а именно — значительно снижается скорость парсинга, если пытаться обработать строки при помощи стандартного объекта string.

Исходные данные — файл забитый строками по типу:

key;888;0xA9498353,888_FilialName

В сырой версии скрипта для контроля обработки применялись промежуточные текстовые файлы, потери времени на обработку файла в 1000 строк — 24 секунды, при увеличении размера файла задержка быстро растет. Пример:

function test 
    {
    $Path = 'C:\Powershell\test\test.txt'

    $PSGF = Get-Content $Path

    # создаем файл
    $PSGFFileName = $Path + '-compare.txt'
    Remove-Item -Path $PSGFFileName -ErrorAction SilentlyContinue | Out-Null
    New-Item $PSGFFileName -Type File -ErrorAction SilentlyContinue | Out-Null

    # ToDo
    # в этом блоке теряется время, надо оптимизировать.
    # не использовать промежуточный файл Add-Content, потери на нем
    foreach ($Key in $PSGF)
    {
        $Val = $Key.ToString().Split(';')
        $test = $val[2]
        $Val = $test.ToString().Split(',')
        $test = $Val[0]
        Add-Content $PSGFFileName -Value $Test
    }

    $Result = Get-Content $PSGFFileName
    Remove-Item -Path $PSGFFileName -ErrorAction SilentlyContinue | Out-Null
    ### не оптимизированный код # end ################################
    return $Result
    }

Результат прогона:

99 строк — 1,8 секунды
1000 строк — 24,4 секунды
2000 строк — 66,17 секунды

Оптимизация №1


Ясно, что это никуда не годится. Заменяем выгрузку в файл операциями в памяти:

function test 
    {
    $Path = 'C:\Powershell\test\test.txt'

    $PSGF = Get-Content $Path
    $Result = ''

    # 
    foreach ($Key in $PSGF)
    {
        $Val = $Key.ToString().Split(';')
        $test = $val[2]
        $Val = $test.ToString().Split(',')
        $test = $Val[0]
        $Result = $Result + "$test`r`n"
    }

    return $Result
    }

Measure-Command {  test }

Результат прогона:

99 строк — 0.0037 секунды
1000 строк — 0.055 секунды
2000 строк — 0.190 секунды

Вроде бы все хорошо, ускорение получено, но давайте посмотрим что происходит если строк в объекте больше:

10000 строк — 1,92 секунды
20000 строк — 8,07 секунды
40000 строк — 26,01 секунд

Такой метод обработки подходит для списков не более чем 5-8 тысяч строк, после начинаются потери на конструкторе объекта, менеджер памяти постоянно выделяет новую память при добавлении строки и копирует объект.

Оптимизация №2


Попробуем сделать лучше, используем «программистский» подход:

function test 
    {
    $Path = 'C:\Powershell\test\test.txt'

    $PSGF = Get-Content $Path

    # берем объект из дотнета
    $Str = New-Object System.Text.StringBuilder

    foreach ($Key in $PSGF)
    {
        $Val = $Key.ToString().Split(';')
        $temp = $val[2].ToString().Split(',')
        $Val = $temp
        $temp = $Str.Append( "$Val`r`n" )
    }

    $Result = $Str.ToString()
    }

Measure-Command {  test }

Результат прогона: 40000 строк — 1,8 секунды.

Дальнейшие улучшения типа замены foreach на for, выбрасывание внутренней переменной $test не дали значимого прироста скорости.

Кратко:

Для эффективной работы с большим количеством строк используйте объект System.Text.StringBuilder. Вызов конструктора:

$SomeString = New-Object System.Text.StringBuilder

Преобразование в строку:

$Result = $Str.ToString()

Объяснение работы StringBuilder (весь секрет в более эффективной работе менеджера памяти).

Комментарии (3)


  1. WNeZRoS
    21.12.2015 21:58

    Ещё для ускорения можно заменить Split на LastIndexOf, IndexOf, Substring. Опять же будет более эффективная работа с памятью: не будут выделяться в отдельные строки не нужные части.


    1. pak-nikolai
      23.12.2015 22:30

      я уже дальше копать не стал, т.к. не получил сколь нибудь значимого прироста скорости от оптимизации внутри цикла


  1. Dywar
    22.12.2015 11:32

    Еще можно скачать Powergui, собрать из скрипта PE файл и запускать с запросом пароля или без.
    И/или использовать C# напрямую:

    PS C:\>$source = @"
    public class BasicTest
    {
      public static int Add(int a, int b)
        {
            return (a + b);
        }
      public int Multiply(int a, int b)
        {
        return (a * b);
        }
    }
    "@
    
    PS C:\>Add-Type -TypeDefinition $source
    PS C:\>[BasicTest]::Add(4, 3)
    PS C:\>$basicTestObject = New-Object BasicTest
    PS C:\>$basicTestObject.Multiply(5, 2)
    


    Протестировать и оптимизировать код поможет cshell или linqpad. Замерять скорость поможет Stopwatch.