【问题标题】:Time complexity of finding duplicate files in bash在 bash 中查找重复文件的时间复杂度
【发布时间】:2015-10-24 05:00:53
【问题描述】:

我今天不得不编写一个 Bash 脚本来删除重复文件,使用它们的 md5 哈希值。我将这些哈希作为文件存储在一个临时目录中:

for i in * ; do
    hash=$(md5sum /tmp/msg | cut -d " " -f1) ;
    if [ -f /tmp/hashes/$hash ] ;
    then
        echo "Deleted $i" ;
        mv $i /tmp/deleted ;
    else
        touch /tmp/hashes/$hash ;
    fi ;
done

它工作得很好,但让我想知道:这是一种省时的方式吗?我最初想将 MD5 哈希值存储在一个文件中,但后来我想“不,因为检查给定的 MD5 是否在这个文件中需要每次都重新读取它”。现在,我想知道:使用“在目录中创建文件”方法时是否相同?当同一目录中有很多文件时,Bash [ -f ] 是否检查线性或准常数复杂性?

如果依赖于文件系统,tmpfs 的复杂度是多少?

【问题讨论】:

  • 如果文件系统变得太慢,请使用带有关联数组的 Awk。
  • 对于任何体面的文件系统,我希望它大致是对数(以文件数计),但您仍然可以更快地将哈希存储在内存哈希表中。例如,如果您可以使用 Python,这将是一件微不足道的事情。
  • 或者只是做md5sum *然后比较你得到的两个文本文件。
  • @NayukiMinase MD5 可能已经足够了。对于知识渊博的对手来说,这并不安全,但是两个随机文件发生冲突的可能性仍然只有 2^128 中的 1 个,或者大约 3.4e38 中的 1 个。 (尽管如此,如果您使用的是足够快的系统,如果您极度偏执,请随意使用较慢的校验和。)
  • 此外,all 散列会产生冲突,因为可能的散列空间远小于您可能想要散列的对象集。 MD5 的问题在于,有一些技术可以生成与给定文件具有相同哈希值的文件。

标签: bash time-complexity tmp tmpfs


【解决方案1】:

在读取包含散列的文件的内容和在作为散列的文件名的目录中查找散列之间的选择基本上归结为“内核在读取目录时更快还是程序在读取文件时更快”。两者都将涉及对每个散列的线性搜索,因此您最终会得到几乎相同的行为。您可能会争辩说内核应该快一点,但余量不会很大。请注意,大多数情况下,线性搜索将是详尽的,因为散列不存在(除非您有很多重复文件)。因此,如果您要处理几千个文件,则搜索将处理几百万个条目——这是二次行为。

如果您有数百或数千个文件,您可能会使用两级层次结构做得更好 - 例如,包含两个字符的子目录 00 .. FF 的目录,然后存储其余的子目录中的名称(或全名)。例如,在terminfo 目录中使用了这种技术的一个小变种。优点是内核只需要读取相对较小的目录来查找文件是否存在。

【讨论】:

    【解决方案2】:

    我还没有“散列”出来,但我会尝试将你的 md5sum 存储在 bash 散列中。

    How to define hash tables in Bash?

    将 md5sum 存储为键,如果需要,将文件名存储为值。对于每个文件,只需查看密钥是否已存在于哈希表中。如果是这样,您不关心该值,但可以使用它打印出原始重复文件的名称。然后删除当前文件(使用重复键)。不是我开始寻找的 bash 专家。

    【讨论】:

      【解决方案3】:

      我喜欢使用正确的工具来完成这项工作。在这种情况下,您只想查看重复文件。我已经针对我可以使用的数千个文件对此进行了测试,并且重新阅读该文件似乎没有任何问题。另外,我注意到我有数百个重复文件。当我将哈希存储在单独的文件中,然后处理大量文件时,我的系统在一个目录中大约有 10,000 个哈希文件之后慢慢地爬行。将所有哈希值放在一个文件中大大加快了速度。

      # This uses md5deep.  An alternate is presented later.
      md5deep -r some_folder > hashes.txt
      
      # If you do not have md5deep
      find . -type f -exec md5sum \{\} \;
      

      这会为您提供所有内容的哈希值。

      cut -b -32 hashes.txt | sort | uniq -d > dupe_hashes.txt
      

      这将使用cut 获取每个文件的哈希值,对哈希值进行排序,然后找到任何重复的哈希值。这些被写入dupe_hashes.txt,没有附加文件名。现在我们需要将哈希映射回文件。

      (for hash in $(cat dupe_hashes.txt); do
          grep "^$hash" hashes.txt | tail -n +2 | cut -b 35-
      done) > dupe_files.txt
      

      这对我来说似乎运行缓慢。 Linux 内核在将此类文件保存在内存中而不是频繁地从磁盘中读取它们方面做得非常好。如果您更愿意将其强制存储在内存中,您可以只使用/dev/shm/hashes.txt 而不是hashes.txt。我发现在我的测试中它是不必要的。

      这会为您提供每个重复的文件。到目前为止,一切都很好。您可能需要查看此列表。如果您还想列出原始的,请从命令中删除 tail -n +2 | 位。

      当您觉得可以删除每个列出的文件时,您可以将内容通过管道传输到 xargs。这将删除 50 个一组的文件。

      xargs -L 50 rm < dupe_files.txt
      

      【讨论】:

      • 如果没有其他文件具有完全相同的大小,为什么要计算文件的 md5sum?不可能有重复。 fdupes 之类的工具已经知道这一点。
      • 如果只有两个具有给定大小的文件,您最好将它们分块进行比较,这样如果您发现它们早期不同的地方,您就可以停下来而不用全部阅读。天真的 let's-just-md5sum-everything 方法丢弃了 很多 优化。
      • (检查 stat 返回相同大小的两个目录条目在对它们进行两次散列之前不指向同一个 inode 是另一个如此简单、廉价的优化)。
      【解决方案4】:

      我将尝试定性地回答 tmpfs 上的文件存在测试有多快,然后我可以建议您如何使整个程序运行得更快。

      首先,tmpfs 目录查找依赖于(在内核中)目录条目缓存哈希表查找,它对目录中的文件数量不那么敏感。它们受到影响,但是是次线性的。这与正确完成哈希表查找需要一些恒定时间(O(1))这一事实有关,无论哈希表中有多少项。

      为了解释,我们可以看看test -f[ -f X ]所做的工作,来自coreutils (gitweb):

      case 'e':
         unary_advance ();
         return stat (argv[pos - 1], &stat_buf) == 0;
      ... 
      case 'f':                   /* File is a file? */
         unary_advance ();
         /* Under POSIX, -f is true if the given file exists
            and is a regular file. */
         return (stat (argv[pos - 1], &stat_buf) == 0
                 && S_ISREG (stat_buf.st_mode));
      

      所以它直接在文件名上使用stat()test 没有明确列出目录,但stat 的运行时间可能会受到目录中文件数量的影响。 stat 调用的完成时间将取决于底层文件系统实现。

      对于每个文件系统,stat 会将路径拆分为目录组件,然后向下走。例如,对于路径/tmp/hashes/the_md5:首先/,获取它的inode,然后在其中查找tmp,获取该inode(它是一个新的挂载点),然后获取hashes inode,最后是测试文件名及其 inode。您可以期待一直到 /tmp/hashes/ 的 inode 被缓存,因为它们在每次迭代中都会重复,因此这些查找速度很快并且可能不需要磁盘访问。每个查找将取决于父目录所在的文件系统。在/tmp/ 部分之后,会在 tmpfs 上进行查找(所有这些都在内存中,除非您内存不足并需要使用交换)。

      linux中的tmpfs依赖simple_lookup来获取目录中文件的inode。 tmpfs 位于树 linux mm/shmem.c 中的旧名称下。 tmpfs,很像 ramfs,似乎并没有实现自己的数据结构来跟踪虚拟数据,它只是依赖于 VFS 目录条目缓存(在Directory Entry Caches 下)。

      因此,我怀疑在目录中查找文件的 inode 就像查找哈希表一样简单。 我想说,只要你所有的临时文件都适合你的内存,并且你使用 tmpfs/ramfs,那么有多少文件并不重要 - 每次都是 O(1) 查找。强>

      但是,其他文件系统(如 Ext2/3)会产生与目录中存在的文件数量成线性关系的惩罚。

      将它们存储在内存中

      正如其他人所建议的那样,您也可以通过将 MD5 存储在 bash 变量中来将它们存储在内存中,并避免文件系统(和相关的系统调用)的惩罚。将它们存储在文件系统上的好处是,如果要中断循环,您可以从离开的位置恢复(您的 md5 可能是指向其摘要匹配的文件的符号链接,您可以依赖它,在随后的运行中),但是慢一点。

      MD5=d41d8cd98f00b204e9800998ecf8427e
      let SEEN_${MD5}=1
      ...
      digest=$(md5hash_of <filename>)
      let exists=SEEN_$digest
      if [[ "$exists" == 1 ]]; then
         # already seen this file
      fi
      

      更快的测试

      您可以使用[[ -f my_file ]] 而不是[ -f my_file ]。命令[[ 是内置的bash,并且比每次比较生成一个新进程(/usr/bin/[)要快得多。这将产生更大的影响。

      什么是 /usr/bin/[

      /usr/bin/test/usr/bin/[ 是两个不同的程序,但[ 的源代码(lbracket.c)与test.c 相同(同样在coreutils 中):

      #define LBRACKET 1
      #include "test.c"
      

      所以它们可以互换。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2017-01-15
        • 1970-01-01
        • 2012-11-27
        • 1970-01-01
        • 1970-01-01
        • 2017-12-18
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多