Powershell Speed: как ускорить проверку ForEach-Object MD5 / ha sh - PullRequest
4 голосов
/ 26 января 2020

Я выполняю следующую проверку MD5 на 500 миллионов файлов, чтобы проверить наличие дубликатов. Сценарии работают вечно, и мне было интересно, как это ускорить. Как я мог ускорить это? Могу ли я использовать попытку catch l oop вместо Содержания, чтобы выдать ошибку, когда вместо этого уже существует ha sh? Что бы вы все порекомендовали?

$folder = Read-Host -Prompt 'Enter a folder path'

$hash = @{}
$lineCheck = 0

Get-ChildItem $folder -Recurse | where {! $_.PSIsContainer} | ForEach-Object {
    $lineCheck++
    Write-Host $lineCheck
    $tempMD5 = (Get-FileHash -LiteralPath $_.FullName -Algorithm MD5).Hash;

    if(! $hash.Contains($tempMD5)){
        $hash.Add($tempMD5,$_.FullName)
    }
    else{
        Remove-Item -literalPath $_.fullname;
    }
} 

Ответы [ 3 ]

3 голосов
/ 26 января 2020

Как предлагается в комментариях, вы можете рассмотреть возможность начала хэширования файлов только , если сначала найдено совпадение с длиной файла. Это означает, что вы не будете вызывать дорогостоящий метод ha sh для любой уникальной длины файла.

* Примечание: команда Write-Host сама по себе довольно дорогая, поэтому я бы не отображать каждую итерацию (Write-Host $lineCheck), но, например, только когда совпадение найдено.

$Folder = Read-Host -Prompt 'Enter a folder path'

$FilesBySize = @{}
$FilesByHash = @{}

Function MatchHash([String]$FullName) {
    $Hash = (Get-FileHash -LiteralPath $FullName -Algorithm MD5).Hash
    $Found = $FilesByHash.Contains($Hash)
    If ($Found) {$Null = $FilesByHash[$Hash].Add($FullName)}
    Else {$FilesByHash[$Hash] = [System.Collections.ArrayList]@($FullName)}
    $Found
}

Get-ChildItem $Folder -Recurse | Where-Object -Not PSIsContainer | ForEach-Object {
    $Files = $FilesBySize[$_.Length]
    If ($Files) {
        If ($Files.Count -eq 1) {$Null = MatchHash $Files[0]}
        If ($Files.Count -ge 1) {If (MatchHash $_) {Write-Host 'Found match:' $_.FullName}}
        $Null = $FilesBySize[$_.Length].Add($_.FullName)
    } Else {
        $FilesBySize[$_.Length] = [System.Collections.ArrayList]@($_.FullName)
    }
}

Отображать найденные дубликаты:

ForEach($Hash in $FilesByHash.GetEnumerator()) {
    If ($Hash.Value.Count -gt 1) {
        Write-Host 'Hash:' $Hash.Name
        ForEach ($File in $Hash.Value) {
            Write-Host 'File:' $File
        }
    }
}
2 голосов
/ 26 января 2020

Я предполагаю, что самая медленная часть вашего кода - это вызов Get-FileHash, поскольку все остальное не требует больших вычислительных ресурсов или ограничено вашим оборудованием (дисковые IOPS).

Вы можете попробовать заменить его с вызовом нативного инструмента, который имеет более оптимизированную реализацию MD5, и посмотрите, помогает ли он.

Могу ли я использовать try catch l oop вместо Содержит, чтобы выдать ошибку, когда ha sh уже существует вместо этого?

Исключения медленные, и их использование для управления потоком не рекомендуется:

Хотя использование обработчиков исключений для перехвата ошибок и других событий, которые нарушают выполнение программы, является хорошей практикой, использование обработчика исключений в качестве части обычной логики выполнения программы c может быть дорогим и его следует избегать

На этот вопрос есть однозначный ответ от парня, который их реализовал - Крис Брамм. Он написал отличную статью в блоге на эту тему (предупреждение - это очень долго) (warning2 - это очень хорошо написано, если вы технический специалист ie, вы прочтете его до конца, а затем придется наверстать упущенное после работа :))

Резюме: они медленные. Они реализованы как исключения Win32 SEH, поэтому некоторые даже пересекают границу ЦП кольца 0!

1 голос
/ 26 января 2020

Я знаю, что это вопрос PowerShell, но вы можете хорошо использовать распараллеливание в C#. Вы также упомянули в одном из комментариев об использовании C# в качестве альтернативы, поэтому я подумал, что не повредит опубликовать возможную реализацию того, как это можно сделать.

Сначала можно создать метод для вычисления контрольная сумма MD5 для файла:

private static string CalculateMD5(string filename)
{
    using var md5 = MD5.Create();
    using var stream = File.OpenRead(filename);
    var hash = md5.ComputeHash(stream);
    return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant();
}

Затем вы можете создать метод с запросами всех хэшей файлов в parellel, используя ParallelEnumerable.AsParallel():

private static IEnumerable<FileHash> FindFileHashes(string directoryPath)
{
    var allFiles = Directory
        .EnumerateFiles(directoryPath, "*", SearchOption.AllDirectories);

    var hashedFiles = allFiles
        .AsParallel()
        .Select(filename => new FileHash { 
            FileName = filename, 
            Hash = CalculateMD5(filename) 
        });

    return hashedFiles;
}

Тогда Вы можете просто использовать вышеуказанный метод для удаления дубликатов файлов:

private static void DeleteDuplicateFiles(string directoryPath)
{
    var fileHashes = new HashSet<string>();

    foreach (var fileHash in FindFileHashes(directoryPath))
    {
        if (!fileHashes.Contains(fileHash.Hash))
        {
            Console.WriteLine($"Found - File : {fileHash.FileName} Hash : {fileHash.Hash}");
            fileHashes.Add(fileHash.Hash);
            continue;
        }

        Console.WriteLine($"Deleting - File : {fileHash.FileName} Hash : {fileHash.Hash}");
        File.Delete(fileHash.FileName);
    }
}

Полная программа:

using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Security.Cryptography;

namespace Test
{
    internal class FileHash
    {
        public string FileName { get; set; }
        public string Hash { get; set; }
    }

    public class Program
    {
        public static void Main()
        { 
            var path = "C:\\Path\To\Files";
            if (File.Exists(path))
            {
                Console.WriteLine($"Deleting duplicate files at {path}");
                DeleteDuplicateFiles(path);
            }
        }

        private static void DeleteDuplicateFiles(string directoryPath)
        {
            var fileHashes = new HashSet<string>();

            foreach (var fileHash in FindFileHashes(directoryPath))
            {
                if (!fileHashes.Contains(fileHash.Hash))
                {
                    Console.WriteLine($"Found - File : {fileHash.FileName} Hash : {fileHash.Hash}");
                    fileHashes.Add(fileHash.Hash);
                    continue;
                }

                Console.WriteLine($"Deleting - File : {fileHash.FileName} Hash : {fileHash.Hash}");
                File.Delete(fileHash.FileName);
            }
        }

        private static IEnumerable<FileHash> FindFileHashes(string directoryPath)
        {
            var allFiles = Directory
                .EnumerateFiles(directoryPath, "*", SearchOption.AllDirectories);

            var hashedFiles = allFiles
                .AsParallel()
                .Select(filename => new FileHash { 
                    FileName = filename, 
                    Hash = CalculateMD5(filename) 
                });

            return hashedFiles;
        }

        private static string CalculateMD5(string filename)
        {
            using var md5 = MD5.Create();
            using var stream = File.OpenRead(filename);
            var hash = md5.ComputeHash(stream);
            return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant();
        }
    }
}
...