【问题标题】:awk associative array grows fastawk 关联数组增长很快
【发布时间】:2015-06-09 19:08:41
【问题描述】:

我有一个将数字分配给 md5sum 的文件,如下所示:

0   0000001732816557DE23435780915F75
1   00000035552C6F8B9E7D70F1E4E8D500
2   00000051D63FACEF571C09D98659DC55
3   0000006D7695939200D57D3FBC30D46C
4   0000006E501F5CBD4DB56CA48634A935
5   00000090B9750D99297911A0496B5134
6   000000B5AEA2C9EA7CC155F6EBCEF97F
7   00000100AD8A7F039E8F48425D9CB389
8   0000011ADE49679AEC057E07A53208C1

另一个文件每行包含三个 md5sum,如下所示:

00000035552C6F8B9E7D70F1E4E8D500    276EC96E149571F8A27F4417D7C6BC20    9CFEFED8FB9497BAA5CD519D7D2BB5D7
00000035552C6F8B9E7D70F1E4E8D500    44E48C092AADA3B171CE899FFC6943A8    1B757742E1BF2AA5DB6890E5E338F857

我想要的是用第一个文件的整数替换第二个文件中的第一个和第三个 md5sums。目前我正在尝试以下 awk 脚本:

awk '{OFS="\t"}FNR==NR{map[$2]=$1;next}
{print map[$1],$2,map[$3]}' mapping.txt relation.txt

问题是脚本需要超过 16g 的内存,尽管硬盘上的第一个文件只有 5.7g。

【问题讨论】:

  • 除了拆分文件并分块执行之外,您在脚本中没有什么不同的方法可以减少该数字。如果这就是它所需要的,那么这就是它所需要的。对不起。顺便说一句,不相关 - 将 {OFS="\t"} 更改为 BEGIN{OFS="\t"}
  • 你没有说第二个文件有多大。如果它与 file1 的行数相同,那么我看不到您的 16G RAM 问题的解决方案。祝你好运。
  • 为什么第二个文件的大小很重要?脚本只逐行打印替换。
  • 您超出了 16GB 限制(估计值)多少?如果只是少量,则可能值得以更密集的格式存储 md5sum 或者使用子字符串...
  • 从行长来看,应该有大约 1.4 亿条记录,这意味着 2.8 亿个短字符串——其中一半是 32 字节长,一半甚至更短。在 64 位系统(必须如此)上,仅字符串数据的指针就是 2GB,这不是唯一的元数据。如果你想一次性做到这一点,我认为你不能用脚本语言来做到这一点。用母语将其降低到(估计)6-7 GB 应该是可能的。你会接受使用通用库的 C++ 解决方案吗?

标签: awk out-of-memory associative-array


【解决方案1】:

如果你没有足够的内存来存储第一个文件,那么你需要写这样的东西来为第二个文件中的每个值查找第一个文件:

awk 'BEGIN{OFS="\t"}
{
    val1 = val3 = ""
    while ( (getline line < "mapping.txt") > 0 ) {
        split(line,flds)
        if (flds[2] == $1) {
            val1 = flds[1]
        }
        if (flds[2] == $3) {
            val3 = flds[1]
        }
        if ( (val1 != "") && (val3 != "") ) {
            break
        }
    }
    close("mapping.txt")

    print val1,$2,val3

}' relation.txt

会很慢。如果你愿意,你可以添加 N 行 getline-d 的缓存来加速它。

【讨论】:

  • 感觉这个太慢了。第二个文件包含大约 4 亿条记录,这意味着读取第一个文件 4 亿次。这只需要很多时间。我目前已经通过创建一个 mysql 数据库并使用 select into outfile 解决了这个问题,但我觉得对于这类问题有更多的轻量级解决方案。
  • 正如我所说,它很慢,如果它太慢,您总是可以将最后 N 行缓存在一个数组中,并且只有在所需值不存在时才执行 getline。
【解决方案2】:

这个问题可以解决,如下(file1.txt是整数和md5sums的文件,file2.txt是md5sums三列的文件):

#!/bin/sh
# First sort each of file 1 and the first and third columns of file 2 by MD5
awk '{ print $2 "\t" $1}' file1.txt | sort >file1_n.txt
# Before we sort the file 2 columns, we number the rows so we can put them
# back into the original order later
cut -f1 file2.txt | cat -n - | awk '{ print $2 "\t" $1}' | sort >file2_1n.txt
cut -f3 file2.txt | cat -n - | awk '{ print $2 "\t" $1}' | sort >file2_3n.txt
# Now do a join between them, extract the two columns we want, and put them back in order
join -t'    ' file2_1n.txt file1_n.txt | awk '{ print $2 "\t" $3}' | sort -n | cut -f2 >file2_1.txt
join -t'    ' file2_3n.txt file1_n.txt | awk '{ print $2 "\t" $3}' | sort -n | cut -f2 >file2_3.txt
cut -f2 file2.txt | paste file2_1.txt - file2_3.txt >file2_new1.txt

对于 file1.txtfile2.txt 各有 100 万行长的情况,此解决方案和 Ed Morton 的 awk-only 解决方案在我的系统上花费的时间大致相同。无论使用哪种方法,我的系统都需要很长时间才能解决 1.4 亿行的问题,但我为 1000 万行的文件运行了一个测试用例。

我曾假设依赖sort(在需要时会自动使用临时文件)的解决方案对于大量行应该更快,因为它将是 O(N log N) 运行时,而重新如果两个文件的大小相似,则为输入的每一行读取映射文件将为 O(N^2)。

计时结果

我对两个候选解决方案的性能关系的假设对于我尝试过的测试用例来说是错误的。在我的系统上,基于sort 的解决方案和仅awk 的解决方案对于100 万行和1000 万行输入文件中的每一个都花费了相似(30% 以内)的时间,awk-唯一的解决方案在每种情况下都更快。当然,我不知道当输入文件大小再增加 10 倍以上时,这种关系是否成立。

奇怪的是,这两种解决方案运行 1000 万行问题所需的时间大约是 100 万行问题的 10 倍,这让我感到困惑,因为我本以为这两种解决方案都与文件长度存在非线性关系。

【讨论】:

  • 感谢加入计划是我想要的。顺便说一句,第一个文件已经按哈希排序,第三个文件按第一行排序。所以我所要做的就是:join -t $'\t' -12 -21 -o1.1,2.2,2.3 mapping.txt relation.txt | sort --parallel=4 -S4g -k3 &gt; relation_step1.txt 和:join -t $'\t' -12 -23 -o2.1,2.2,1.1 mapping.txt relation_step1.txt &gt; relation_result.txt
【解决方案3】:

如果文件的大小导致 awk 内存不足,那么要么使用其他工具,要么完全使用其他方法。

sed 命令可能会以更少的内存使用成功。这个想法是读取索引文件并创建一个执行重新映射的 sed 脚本,然后在生成的 sedscript 上调用 sed。

下面的 bash 脚本就是这个想法的实现。它包括一些 STDERR 输出以帮助跟踪进度。我喜欢为大型数据集或其他类型的耗时处理问题生成进度跟踪输出。

这个脚本已经在一小部分数据上进行了测试;它可能对您的数据起作用。请试一试。

#!/bin/bash

# md5-indexes.txt
# 0   0000001732816557DE23435780915F75
# 1   00000035552C6F8B9E7D70F1E4E8D500
# 2   00000051D63FACEF571C09D98659DC55
# 3   0000006D7695939200D57D3FBC30D46C
# 4   0000006E501F5CBD4DB56CA48634A935
# 5   00000090B9750D99297911A0496B5134
# 6   000000B5AEA2C9EA7CC155F6EBCEF97F
# 7   00000100AD8A7F039E8F48425D9CB389
# 8   0000011ADE49679AEC057E07A53208C1

# md5-data.txt
# 00000035552C6F8B9E7D70F1E4E8D500    276EC96E149571F8A27F4417D7C6BC20    9CFEFED8FB9497BAA5CD519D7D2BB5D7
# 00000035552C6F8B9E7D70F1E4E8D500    44E48C092AADA3B171CE899FFC6943A8    1B757742E1BF2AA5DB6890E5E338F857

# Goal replace field 1 and field 3 with indexes to md5 checksums from md5-indexes

md5_indexes='md5-indexes.txt'
md5_data='md5-data.txt'

talk()  { echo 1>&2 "$*" ; }
talkf() { printf 1>&2 "$@" ; }
track() {
  local var="$1" interval="$2"
  local val
  eval "val=\$$var"
  if (( interval == 0 || val % interval == 0 )); then
    shift 2
    talkf "$@"
  fi
  eval "(( $var++ ))"   # increment the counter
}

# Build a sedscript to translate all occurances of the 1st & 3rd MD5 sums into their
# corresponding indexes

talk "Building the sedscript from the md5 indexes.."

sedscript=/tmp/$$.sed

linenum=0
lines=`wc -l <$md5_indexes`
interval=$(( lines / 100 ))

while read index md5sum ; do
  track linenum $interval "..$linenum"
  echo "s/^[[:space:]]*[[:<:]]$md5sum[[:>:]]/$index/" >>$sedscript
  echo "s/[[:<:]]$md5sum[[:>:]]\$/$index/"            >>$sedscript
done <$md5_indexes
talk ''

sedlength=`wc -l <$sedscript`

talkf "The sedscript is %d lines\n" $sedlength

cmd="sed -E -f $sedscript -i .bak $md5_data"
talk "Invoking: $cmd"

$cmd

changes=`diff -U 0 $md5_data.bak $md5_data | tail +3 | grep -c '^+'`

talkf "%d lines changed in $md5_data\n" $changes

exit

这是两个文件:

cat md5-indexes.txt
0   0000001732816557DE23435780915F75
1   00000035552C6F8B9E7D70F1E4E8D500
2   00000051D63FACEF571C09D98659DC55
3   0000006D7695939200D57D3FBC30D46C
4   0000006E501F5CBD4DB56CA48634A935
5   00000090B9750D99297911A0496B5134
6   000000B5AEA2C9EA7CC155F6EBCEF97F
7   00000100AD8A7F039E8F48425D9CB389
8   0000011ADE49679AEC057E07A53208C1

cat md5-data.txt
00000035552C6F8B9E7D70F1E4E8D500    276EC96E149571F8A27F4417D7C6BC20    9CFEFED8FB9497BAA5CD519D7D2BB5D7
00000035552C6F8B9E7D70F1E4E8D500    44E48C092AADA3B171CE899FFC6943A8    1B757742E1BF2AA5DB6890E5E338F857

这是示例运行:

$ ./md5-reindex.sh
Building the sedscript from the md5 indexes..
..0..1..2..3..4..5..6..7..8
The sedscript is 18 lines
Invoking: sed -E -f /tmp/83800.sed -i .bak md5-data.txt
2 lines changed in md5-data.txt

最后,生成的文件:

$ cat md5-data.txt
1    276EC96E149571F8A27F4417D7C6BC20    9CFEFED8FB9497BAA5CD519D7D2BB5D7
1    44E48C092AADA3B171CE899FFC6943A8    1B757742E1BF2AA5DB6890E5E338F857

【讨论】:

  • 按照我的口味,这个解决方案看起来有点复杂,但无论如何我都不能接受。
猜你喜欢
  • 2014-06-21
  • 2014-02-16
  • 2011-03-17
  • 1970-01-01
  • 2021-11-11
  • 2012-05-20
相关资源
最近更新 更多