Можно ли упростить следующий цикл вложенного цикла foreach в PowerShell? - PullRequest
2 голосов
/ 09 октября 2019

Я создал скрипт, который перебирает массив и исключает любые переменные, которые находятся во втором массиве.

Пока работает код;мне стало интересно, можно ли это упростить или передать по трубам.

   $result = @()
   $ItemArray = @("a","b","c","d")
   $exclusionArray = @("b","c")

    foreach ($Item in $ItemArray)
    {
        $matchFailover = $false
        :gohere
        foreach ($ExclusionItem in $exclusionArray)
        {
            if ($Item -eq $ExclusionItem)
            {
                Write-Host "Match: $Item = $ExclusionItem"
                $matchFailover = $true
                break :gohere
            }
            else{
            Write-Host "No Match: $Item != $ExclusionItem"
            }
        }
        if (!($matchFailover))
        {
            Write-Host "Adding $Item to results"
            $result += $Item
        }
    }
    Write-Host "`nResults are"
    $result

Ответы [ 3 ]

3 голосов
/ 09 октября 2019

Чтобы дать вашей задаче имя: Вы ищете относительное дополнение иначе установить разницу между двумя массивами:

InОбозначения теории множеств: $ItemArray \ $ExclusionArray, т. е. те элементы в $ItemArray, которых нет в $ExclusionArray.

Этот связанный вопрос ищет симметричная разница между двумя наборами, т. Е. Набор элементов, уникальных для любой стороны - наконец, это то, что реализуют решения на основе Compare-Object, но только в предположении, чтокаждый массив имеет без дубликатов .


полезный ответ EyIM является концептуально простым и лаконичным .

A потенциальная проблема: производительность : поиск в массиве исключений должен выполняться для каждого элемента во входном массиве .

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

При использовании больших массивов LINQ предлагает существенно быструюЭто решение :

Примечание : Чтобы воспользоваться преимуществами решения LINQ, ваши массивы должны быть в памяти уже , иПреимущество тем больше, чем больше массив исключений. Если ваши входные данные передаются по конвейеру, накладные расходы на его выполнение могут сделать попытки оптимизации обработки массива бессмысленными или даже контрпродуктивными, и в этом случае имеет смысл придерживаться собственного решения PowerShell - см. ответ iRon .

# Declare the arrays as [string[]]
# so that calling the LINQ method below works as-is.
# (You could also cast to [string[]] ad hoc.)
[string[]] $ItemArray = 'a','b','c','d'
[string[]] $exclusionArray = 'b','c'

# Return only those elements in $ItemArray that aren't also in $exclusionArray
# and convert the result (a lazy enumerable of type [IEnumerable[string]])
# back to an array to force its evaluation
# (If you directly enumerate the result in a pipeline, that step isn't needed.)
[string[]] [Linq.Enumerable]::Except($ItemArray, $exclusionArray) # -> 'a', 'd'

Обратите внимание на необходимость явного использования типов LINQ через их статические методы, поскольку PowerShell, начиная с v7, не поддерживает методы расширения . Тем не менее, есть предложение на GitHub , чтобы добавить такую ​​поддержку; это связанное предложение запрашивает улучшенную поддержку для вызова универсальных методов.

См. этот ответ для обзора того, как в настоящее время вызывать методы LINQ из PowerShell.


Сравнение производительности:

Наконечник шляпы для iRon для его ввода.

В следующем эталонном коде используется Time-Command функция для сравнения двух подходов, используя массивы примерно с 4000 и 2000 элементами соответственно, которые - как и в вопросе - отличаются только на 2 элемента.

Обратите внимание, что для выравниванияигровое поле, метод .Where() массива (PSv4 +) используется вместо Where-Object командлета на основе конвейера, так как .Where() быстрее с массивами, уже находящимися в памяти.

Вот результаты, усредненные за 10 прогонов;обратите внимание на относительную производительность, как показано в столбцах Factor;от одноядерной виртуальной машины Windows 10 с Windows PowerShell v5.1.:

Factor Secs (10-run avg.) Command                              TimeSpan
------ ------------------ -------                              --------
1.00   0.046              # LINQ...                            00:00:00.0455381
8.40   0.382              # Where ... -notContains...          00:00:00.3824038

Решение LINQ значительно быстрее - в 8 раз (хотя даже более медленное решение заняло всего около 0,4 секунды)для запуска).

Кажется, что разрыв в производительности еще больше в PowerShell Core , где я видел коэффициент около 19 с v7.0.0-preview.4 .;Интересно, что оба теста выполнялись быстрее по отдельности, чем в Windows PowerShell.

Код теста:

# Script block to initialize the arrays.
# The filler arrays are randomized to eliminate caching effects in LINQ.
$init = {
  $fillerArray = 1..1000 | Get-Random -Count 1000
  [string[]] $ItemArray = $fillerArray + 'a' + $fillerArray + 'b' + $fillerArray + 'c' + $fillerArray + 'd'
  [string[]] $exclusionArray = $fillerArray + 'b' + $fillerArray + 'c'
}

# Compare the average of 10 runs.
Time-Command -Count 10 { # LINQ
  . $init
  $result = [string[]] [Linq.Enumerable]::Except($ItemArray, $exclusionArray)
}, { # Where ... -notContains
  . $init
  $result = $ItemArray.Where({ $exclusionArray -notcontains $_ })
}
2 голосов
/ 09 октября 2019

Вы можете использовать Where-Object с -notcontains:

$ItemArray | Where-Object { $exclusionArray -notcontains $_ }

Выход:

a, d
1 голос
/ 12 октября 2019

Защита собственного PowerShell:
Согласно @ mklement0 , без сомнения, Language Integrated Query (LINQ) is //Fast...
Но в некоторых случаях собственные команды PowerShell используют конвейер, как предложено @ EylM все еще может побить LINQ. Это не просто теоретическое, но может произойти в используемых случаях, когда соответствующий процесс простаивает и ожидает медленного ввода. Например, откуда поступают данные:

  • Удаленный сервер (например, Active Directory)
  • Медленное устройство
  • Отдельный поток, который должен выполнять сложные вычисления
  • Интернет ...

Несмотря на то, что я еще не видел easy , доказать это, это предлагается на нескольких сайтах и ​​может быть вычтено из сайтовНапример, Высокопроизводительный PowerShell с LINQ и Входы и выходы конвейера PowerShell .

Доказательство

Чтобы доказать вышеупомянутый тезис, я создалнебольшой командлет Slack, который замедляет каждый элемент, выпадающий в конвейер, на 1 миллисекунду (по умолчанию):

Function Slack-Object ($Delay = 1) {
    process {
        Start-Sleep -Milliseconds $Delay
        Write-Output $_
    }
}; Set-Alias Slack Slack-Object

Теперь давайте посмотрим, может ли собственный PowerShell действительно побить LINQ:
(Чтобы получить хорошее сравнение производительности, кэши следует очистить, например, запустив новый сеанс PowerShell.)

[string[]] $InputArray = 1..200
[string[]] $ExclusionArray = 100..300

(Measure-Command {
    $Result = [Linq.Enumerable]::Except([string[]] ($InputArray | Slack), $ExclusionArray)
}).TotalMilliseconds

(Measure-Command {
    $Result = $InputArray | Slack | Where-Object {$ExclusionArray -notcontains $_}
}).TotalMilliseconds

Результаты:

      LINQ: 411,3721
PowerShell: 366,961

Чтобы исключить кэш LINQ, следует выполнить однократный тест, но каккомментарий @ mklement0, результаты отдельных прогонов могут отличаться при каждом прогоне.
Результаты также сильно зависят от размера входных массивов, размера результата, слабины, тестовой системы и т. д.

Вывод:

В некоторых случаях PowerShell может по-прежнему работать быстрее, чем LINQ!

Цитирование mklement0 Комментарий:
" В целом справедливо сказать, что в этом сценарии разница в производительности настолько мала, что не стоит выбирать подход, основанный на производительности, - и имеет смысл идти сболее PowerShell-подобный подход ( Where-Object ), учитывая, что подход LINQ далеко не очевиден. Суть в следующем: выбирайте LINQ, только если у вас есть большие массивы, которые уже находятся в памяти. Если задействован конвейер, только издержки конвейера могут сделать оптимизацию бессмысленной."

...