【问题标题】:Fast counting of nucleotide types in a large number of sequences快速计数大量序列中的核苷酸类型
【发布时间】:2023-03-11 14:04:01
【问题描述】:

首先,关于我的问题的一些背景知识。
我是一名生物信息学家,这意味着我进行信息学处理以试图回答一个生物学问题。在我的问题中,我必须操作一个名为 FASTA 的文件,它看起来像这样:

>Header 1  
ATGACTGATCGNTGACTGACTGTAGCTAGC  
>Header 2  
ATGCATGCTAGCTGACTGATCGTAGCTAGC  
ATCGATCGTAGCT

因此,FASTA 文件基本上只是一个标题,前面有一个“>”字符,然后是一个或多行由核苷酸组成的序列。核苷酸是可以取 5 种可能值的字符:A、T、C、G 或 N。

我想做的是计算每种核苷酸类型出现的次数,所以如果我们考虑这个虚拟 FASTA 文件:

>Header 1  
ATTCGN

我应该有,结果:
A:1 T:2 C:1 G:1 N:1

这是我目前得到的:

ifstream sequence_file(input_file.c_str());
string line;
string sequence = "";
map<char, double> nucleotide_counts;

while(getline(sequence_file, line)) {
    if(line[0] != '>') {
        sequence += line;
    }
    else {
        nucleotide_counts['A'] = boost::count(sequence, 'A');
        nucleotide_counts['T'] = boost::count(sequence, 'T');
        nucleotide_counts['C'] = boost::count(sequence, 'C');
        nucleotide_counts['G'] = boost::count(sequence, 'G');
        nucleotide_counts['N'] = boost::count(sequence, 'N');
        sequence = "";
    }
}

所以它逐行读取文件,如果遇到'>'作为该行的第一个字符,它就知道序列完整并开始计数。现在我面临的问题是我有数百万个序列,总共有数十亿个核苷酸。我可以看到我的方法没有优化,因为我在同一个序列上调用了五次boost::count

我尝试过的其他事情:

  • 解析序列以增加每种核苷酸类型的计数器。我尝试使用 map&lt;char, double&gt; 将每个核苷酸映射到一个值,但这比 boost 解决方案要慢。
  • 使用算法库的std::count,但这也太慢了。

我在互联网上搜索了解决方案,但如果序列数量很少,我找到的每个解决方案都很好,而我的情况并非如此。你有什么想法可以帮助我加快速度吗?

编辑 1 : 我也试过这个版本,但它比 boost 慢 2 倍:

ifstream sequence_file(input_file.c_str());
string line;
string sequence = "";
map<char, double> nucleotide_counts;

while(getline(sequence_file, line)) {
    if(line[0] != '>') {
        sequence += line;
    }
    else {
        for(int i = 0; i < sequence.size(); i++) {
           nucleotide_counts[sequence[i]]++;
        }
        sequence = "";
    }
}

编辑 2 :感谢这个线程中的每个人,与 boost 原始解决方案相比,我能够获得大约 30 倍的速度提升。这是代码:

#include <map> // std::array
#include <fstream> // std::ifstream
#include <string> // std::string  

void count_nucleotides(std::array<double, 26> &nucleotide_counts, std::string sequence) {
    for(unsigned int i = 0; i < sequence.size(); i++) {
        ++nucleotide_counts[sequence[i] - 'A'];
    }
}  

std::ifstream sequence_file(input_file.c_str());
std::string line;
std::string sequence = "";
std::array<double, 26> nucleotide_counts;

while(getline(sequence_file, line)) {
    if(line[0] != '>') {
        sequence += line;
    }
    else {
        count_nucleotides(nucleotide_counts, sequence);
        sequence = "";
    }
}

【问题讨论】:

  • 我不知道 boost::count 是做什么的,但似乎你遍历同一个字符串 5 次就足够了
  • 只需单独查看每个字符并增加正确的计数器。
  • 迭代序列一次,增加每个核苷酸的映射值,但不要使用访问速度很慢的map,而是使用unordered_map或使用vector的大小26,并意识到,例如'C'-'A' == 2。 (当然你会在 vector 中浪费 19 个未使用的空间,但我敢打赌它很快,而且几乎没有多少内存可以浪费)。
  • 如果数据集非常庞大,枚举所有可能的 4 个字符组合并一次比较 4 个字节可能会更快。
  • 顺便说一句,我注意到您没有使用unordered_map,而在打开文件时使用.c_str(),这让我怀疑您使用的是不是非常旧的C++版本。你知道你用的是哪个版本吗?

标签: c++ count character fasta


【解决方案1】:

按重要性排序:

  1. 此任务的优秀代码将 100% 受 I/O 限制。您的处理器计算字符的速度比您的磁盘将它们泵入 CPU 的速度要快得多。因此,我的第一个问题是:您的存储介质的吞吐量是多少?您理想的 RAM 和缓存吞吐量是多少?这些是上限。如果您遇到了它们,那么进一步查看您的代码就没有多大意义了。您的增强解决方案可能已经存在。

  2. std::map 查找相对昂贵。是的,它是O(log(N)),但您的N=5 很小且恒定,所以这不会告诉您任何信息。对于 5 个值,地图每次查找都必须追逐大约三个指针(更不用说这对于分支预测器来说是多么不可能)。您的 count 解决方案有 5 次地图查找和每个字符串的 5 次遍历,而您的手动解决方案有一个地图查找每个核苷酸(但只有一次字符串遍历)。

    严重建议:为每个计数器使用一个局部变量。这些几乎肯定会被放置在 CPU 寄存器中,因此基本上是免费的。与mapunordered_mapvector 等不同,您永远不会用这种方式污染您的缓存。
    像这样用重复来代替抽象通常不是一个好主意,但在这种情况下,您将需要更多的计数器是非常不可思议的,因此可伸缩性不是问题。

  3. 考虑 std::string_view(这将需要不同的文件读取方法)来避免创建数据副本。您将整个数据从磁盘加载到内存中,然后对每个序列进行复制。这并不是真正必要的,而且(取决于你的编译器有多聪明)会让你陷入困境。特别是因为您一直附加到字符串直到下一个标题(这是更不必要的复制 - 您可以在每一行之后计算)。

  4. 如果由于某种原因,您没有达到理论吞吐量,请考虑多线程和/或矢量化。但我无法想象这是必要的。

顺便说一句,boost::countstd::count 至少 in this version 的一个薄包装。

我认为你在这里做了正确的事:编写良好且可读的代码,然后将其识别为性能瓶颈并检查是否可以使其运行得更快(可能通过使其更丑陋一些)。

【讨论】:

  • 感谢您提供这些宝贵的见解,我编辑了我的帖子以展示我在此线程中每个人的帮助下提出的解决方案。正如我们所说,我正在努力实施您的第 3 点。
【解决方案2】:

如果这是您必须执行的主要任务,您可能会对 awk 解决方案感兴趣。使用 awk 可以轻松解决 FASTA 文件的各种问题:

awk '/^>/ && c { for(i in a) if (i ~ /[A-Z]/) printf i":"a[i]" "; print "" ; delete a }
    /^>/ {print; c++; next}
    { for(i=1;i<=length($0);++i) a[substr($0,i,1)]++ }
    END{ for(i in a) if (i ~ /[A-Z]/) printf i":"a[i]" "; print "" }' fastafile

这会在您的示例中输出:

>Header 1  
N:1 A:7 C:6 G:8 T:8 
>Header 2  
A:10 C:10 G:11 T:12 

注意:我知道这不是 C++,但展示实现相同目标的其他方法通常很有用。


使用 awk 进行基准测试:

脚本 0:(运行时间:太长) 第一个提到的脚本非常慢。仅用于小文件

脚本 1:(运行时间:484.31 秒)这是一个优化版本,我们在其中进行目标计数:

/^>/ && f { for(i in c) printf i":"c[i]" "; print "" ; delete c }
/^>/ {print; f++; next}
{   s=$0
    c["A"]+=gsub(/[aA]/,"",s)
    c["C"]+=gsub(/[cC]/,"",s)
    c["G"]+=gsub(/[gG]/,"",s)
    c["T"]+=gsub(/[tT]/,"",s)
    c["N"]+=gsub(/[nN]/,"",s)
}
END { for(i in c) printf i":"c[i]" "; print "" ; delete c }

更新 2:(runtime: 416.43 sec) 将所有子序列合并为一个序列并仅计算子序列:

function count() {
    c["A"]+=gsub(/[aA]/,"",s)
    c["C"]+=gsub(/[cC]/,"",s)
    c["G"]+=gsub(/[gG]/,"",s)
    c["T"]+=gsub(/[tT]/,"",s)
    c["N"]+=gsub(/[nN]/,"",s)
}
/^>/ && f { count(); for(i in c) printf i":"c[i]" "; print "" ; delete c; string=""}
/^>/ {print; f++; next}
{ string=string $0 }
END { count(); for(i in c) printf i":"c[i]" "; print "" }

更新 3:(运行时间:396.12 秒)改进 awk 查找记录和字段的方式,并一次性滥用它。

function count() {
    c["A"]+=gsub(/[aA]/,"",string)
    c["C"]+=gsub(/[cC]/,"",string)
    c["G"]+=gsub(/[gG]/,"",string)
    c["T"]+=gsub(/[tT]/,"",string)
    c["N"]+=gsub(/[nN]/,"",string)
}
BEGIN{RS="\n>"; FS="\n"}
{
  print $1
  string=substr($0,length($1)); count()
  for(i in c) printf i":"c[i]" "; print ""
  delete c; string=""
}

更新 4:(运行时间:259.69 秒) 更新 gsub 中的正则表达式搜索。这创造了一个有价值的加速:

function count() {
    n=length(string);
    gsub(/[aA]+/,"",string); m=length(string); c["A"]+=n-m; n=m
    gsub(/[cC]+/,"",string); m=length(string); c["C"]+=n-m; n=m
    gsub(/[gG]+/,"",string); m=length(string); c["G"]+=n-m; n=m
    gsub(/[tT]+/,"",string); m=length(string); c["T"]+=n-m; n=m
    gsub(/[nN]+/,"",string); m=length(string); c["N"]+=n-m; n=m
}
BEGIN{RS="\n>"; FS="\n"}
{
  print ">"$1
  string=substr($0,length($1)); count()
  for(i in c) printf i":"c[i]" "; print ""
  delete c; string=""
}

【讨论】:

  • 这是一个有趣且简洁的解决方案,但您对它的性能有何评论? awk 会接近 C++ 的实现吗?
  • @BoBTFish awk 会很快,但肯定不如 C++ 快。一切都取决于手头的全局任务和权衡。您是否有需要分析的千兆字节数据文件,并且您必须连续几天这样做......然后,是的,编写优化的 C++ 程序可能很有用。如果你只需要这样做几次,也许一个简单的脚本语言就足够了。
  • OP 提到了数十亿个核苷酸。这肯定相当于几千兆字节。
  • @Nelfeal "Several" GB,确切地说。 ;)
  • 出于好奇,我现在正在hgdownload.cse.ucsc.edu/goldenPath/hg38/bigZips/hg38.fa.gz上进行测试
【解决方案3】:

如果您想要速度并且可以使用数组,请不要使用地图。此外,std::getline 可以使用自定义分隔符(而不是 \n)。

ifstream sequence_file(input_file.c_str());
string sequence = "";
std::array<int, 26> nucleotide_counts;

// For one sequence
getline(sequence_file, sequence, '>');
for(auto&& c : sequence) {
    ++nucleotide_counts[c-'A'];
}

// nucleotide_counts['X'-'A'] contains the count of nucleotide X in the sequence

Demo

【讨论】:

    【解决方案4】:

    之所以这么慢,是因为你一直在间接访问,或者对同一个字符串进行 5 次扫描。

    您不需要地图,使用 5 个整数,并分别递增它们。那么它应该比boost::count 版本更快,因为你不会遍历字符串5 次,并且它会比mapunordered_map 增量更快,因为你不会有n 个间接访问。

    所以使用类似的东西:

    switch(char)
    {
    case 'A':
        ++a;
        break;
    case 'G':
        ++g;
        break;
    }
    ...
    

    【讨论】:

      【解决方案5】:

      就像cmets中的人建议的那样,尝试一下

      enum eNucleotide {
          NucleotideA = 0,
          NucleotideT,
          NucleotideC,
          NucleotideG,
          NucleotideN,
          Size,
      };
      
      void countSequence(std::string line)
      {
          long nucleotide_counts[eNucleotide::Size] = { 0 };
      
          if(line[0] != '>') {
              for(int i = 0; i < line.size(); ++i) 
              {
                 switch (line[i])
                 {
                     case 'A':
                         ++nucleotide_counts[NucleotideA];
                         break;
                     case 'T':
                         ++nucleotide_counts[NucleotideT];
                         break;                   
                     case 'C':
                         ++nucleotide_counts[NucleotideC];
                         break;                   
                     case 'G':
                         ++nucleotide_counts[NucleotideC];
                         break;                   
                     case 'N':
                         ++nucleotide_counts[NucleotideN];
                         break;                   
                     default : 
                         /// error condition
                         break;
                 }     
              }
      
          /// print results
          std::cout << "A: " << nucleotide_counts[NucleotideA];
          std::cout << "T: " << nucleotide_counts[NucleotideT];
          std::cout << "C: " << nucleotide_counts[NucleotideC];
          std::cout << "G: " << nucleotide_counts[NucleotideG];
          std::cout << "N: " << nucleotide_counts[NucleotideN] << std::endl;
          }
      }
      

      并为每一行内容调用此函数。(未测试代码。)

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2022-01-10
        • 1970-01-01
        • 2014-03-07
        • 1970-01-01
        • 1970-01-01
        • 2011-10-28
        相关资源
        最近更新 更多