Почему я должен избегать использования оператора увеличения присваивания (+ =) для создания коллекции - PullRequest
2 голосов
/ 16 марта 2020

Оператор присвоения увеличения (+=) часто используется в [PowerShell] вопросах и ответах на сайте StackOverflow для создания объектов коллекции, например:

$Collection = @()
1..$Size | ForEach-Object {
    $Collection += [PSCustomObject]@{Index = $_; Name = "Name$_"}
}

Тем не менее, это выглядит очень неэффективной операцией .

Можно ли вообще утверждать, что следует избегать оператора назначения увеличения (+=) для создания коллекции объектов в PowerShell?

1 Ответ

4 голосов
/ 16 марта 2020

Да, следует избегать увеличения оператора присваивания (+=) при построении коллекции объектов.
Помимо того факта, что использование оператора += обычно требует больше операторов (из-за инициализации массива = @()) ) и это поощряет хранить целую коллекцию в памяти, а не помещать ее промежуточно в конвейер, неэффективно .

Причина, по которой она неэффективна, заключается в том, что каждый раз, когда вы используете оператор += будет выполнять:

$Collection = $Collection + $NewObject

Поскольку массивы неизменны с точки зрения количества элементов, вся коллекция будет воссоздана с каждой итерацией.

Правильный синтаксис PowerShell:

$Collection = 1..$Size | ForEach-Object {
    [PSCustomObject]@{Index = $_; Name = "Name$_"}
}

Примечание: как и для других командлетов; если есть только один элемент (итерация), на выходе будет скаляр , а не массив, чтобы принудительно преобразовать его в массив, вы можете либо использовать тип [Array]: [Array]$Collection = 1..$Size | ForEach-Object { ... }, либо использовать Оператор подвыражения массива @( ): $Collection = @(1..$Size | ForEach-Object { ... })

Там, где рекомендуется даже не сохранять результаты в переменной ($a = ...), а затем передавать их в конвейер для экономии памяти, например:

1..$Size | ForEach-Object {
    [PSCustomObject]@{Index = $_; Name = "Name$_"}
} | ConvertTo-Csv .\Outfile.csv

Примечание: Можно также использовать System.Collections.ArrayList класс , обычно это почти так же быстро, как конвейер PowerShell, но недостатком является то, что он потребляет намного больше памяти, чем (должным образом) при использовании конвейера PowerShell.

см. Также: Самый быстрый способ получить уникально индексированный элемент из свойства массива

Измерение производительности

Чтобы показать связь с размером коллекции и снижением производительности, вы можете проверить следующий тест Результаты:

1..20 | ForEach-Object {
    $size = 1000 * $_
    $Performance = @{Size = $Size}
    $Performance.Pipeline = (Measure-Command {
        $Collection = 1..$Size | ForEach-Object {
            [PSCustomObject]@{Index = $_; Name = "Name$_"}
        }
    }).Ticks
    $Performance.Increase = (Measure-Command {
        $Collection = @()
        1..$Size | ForEach-Object {
            $Collection  += [PSCustomObject]@{Index = $_; Name = "Name$_"}
        }
    }).Ticks
    [pscustomobject]$Performance
} | Format-Table *,@{n='Factor'; e={$_.Increase / $_.Pipeline}; f='0.00'} -AutoSize

 Size  Increase Pipeline Factor
 ----  -------- -------- ------
 1000   1554066   780590   1.99
 2000   4673757  1084784   4.31
 3000  10419550  1381980   7.54
 4000  14475594  1904888   7.60
 5000  23334748  2752994   8.48
 6000  39117141  4202091   9.31
 7000  52893014  3683966  14.36
 8000  64109493  6253385  10.25
 9000  88694413  4604167  19.26
10000 104747469  5158362  20.31
11000 126997771  6232390  20.38
12000 148529243  6317454  23.51
13000 190501251  6929375  27.49
14000 209396947  9121921  22.96
15000 244751222  8598125  28.47
16000 286846454  8936873  32.10
17000 323833173  9278078  34.90
18000 376521440 12602889  29.88
19000 422228695 16610650  25.42
20000 475496288 11516165  41.29

Это означает, что с размером коллекции 20,000 объектов с использованием оператора += примерно на 40x медленнее, чем для этого используется конвейер PowerShell.

Шаги для исправления сценарий

Очевидно, что некоторые люди испытывают трудности с исправлением сценария, в котором уже используется оператор увеличения (+=). Поэтому я создал небольшую инструкцию для этого:

  1. Удалите все присвоения <variable> += из соответствующей итерации, просто оставьте только элемент объекта . Если объект не назначен, объект будет просто помещен в конвейер.
    Неважно, если в итерации есть несколько назначений увеличения или если есть встроенные итерации или функция, конечный результат будет таким же.
    Значение, это:

ForEach ( ... ) {
    $Array += $Object1
    $Array += $Object2
    ForEach ( ... ) {
        $Array += $Object3
        $Array += Get-Object

    }
}

По сути то же самое, что:

$Array = ForEach ( ... ) {
    $Object1
    $Object2
    ForEach ( ... ) {
        $Object3
        Get-Object

    }
}

Примечание: если нет итерации, вероятно, нет причин менять ваш скрипт, поскольку, скорее всего, это касается только нескольких дополнений

Назначьте выходные данные итерации (все, что помещено в конвейер) соответствующей переменной. Обычно это на том же уровне, на котором был инициализирован массив ($Array = @()). Например:

$Array = ForEach { ... 

Примечание 1: Опять же, если вы хотите одиночный Чтобы объект действовал как массив, вы, вероятно, захотите использовать оператор подвыражения Array @( ), но вы можете также подумать об этом в момент использования массива, например: @($Array).Count или ForEach ($Item in @($Array))
Примечание 2: Опять же, вам лучше не назначать вывод вообще, но передавать вывод конвейера непосредственно следующему командлету чтобы освободить память: ForEach ( ... ) { ... } | Export-Csv .\File.csv.

Удалить инициализацию массива <Variable> = @()

Полный пример см .: Сравнение массивов в Powershell

...