【问题标题】:Algorithms and techniques for string search across multiple GiB of text files跨多个 GiB 文本文件进行字符串搜索的算法和技术
【发布时间】:2022-11-25 00:24:21
【问题描述】:

我必须创建一个实用程序来尽快搜索 40 到 60 GiB 的文本文件。
每个文件都有大约 50 MB 的数据,其中包含日志行(每个文件大约 630.000 行)。
不幸的是,NOSQL 文档数据库是没有选择的……

截至目前,我正在使用 Aho-Corsaick 算法进行搜索,我从他的 blog 中窃取了 Tomas Petricek。它工作得很好。

我在任务中处理文件。每个文件都通过简单地调用File.ReadAllLines(path)加载到内存中。然后将这些行一一输入 Aho-Corsaick,因此每个文件都会导致对算法的大约 600.000 次调用(我需要结果中的行号)。

这会花费很多时间,并且需要大量内存和 CPU。
我在这个领域的专业知识很少,因为我通常从事图像处理工作。
你们能推荐可以加快处理速度的算法和方法吗?

下面是非常标准的任务创建和文件加载的更详细视图。有关 Aho-Corsaick 的更多信息,请访问上面链接的博客页面。

private KeyValuePair<string, StringSearchResult[]> FindInternal(
    IStringSearchAlgorithm algo, 
    string file)
{
    List<StringSearchResult> result = new List<StringSearchResult>();
    string[] lines = File.ReadAllLines(file);
    for (int i = 0; i < lines.Length; i++)
    {
        var results = algo.FindAll(lines[i]);
        for (int j = 0; j < results.Length; j++)
        {
            results[j].Row = i;
        }
    }
    foreach (string line in lines)
    {
        result.AddRange(algo.FindAll(line));
    }
    return new KeyValuePair<string, StringSearchResult[]>(
        file, result.ToArray());
}


public Dictionary<string, StringSearchResult[]> Find(
    params string[] search)
{
    IStringSearchAlgorithm algo = new StringSearch();
    algo.Keywords = search;
    Task<KeyValuePair<string, StringSearchResult[]>>[] findTasks
        = new Task<KeyValuePair<string, StringSearchResult[]>>[_files.Count];
    Parallel.For(0, _files.Count, i => {
        findTasks[i] = Task.Factory.StartNew(
            () => FindInternal(algo, _files[i])
        );
    });
    Task.WaitAll(findTasks);
    return findTasks.Select(t => t.Result)
        .ToDictionary(x => x.Key, x => x.Value);
}

【问题讨论】:

  • 我不认为你想将文本逐行输入算法,我认为这可能会破坏搜索算法
  • 你为什么不采用 Tomas 的算法并将其作为针对单个文件的单个调用进行测试 - PS 我对这个算法一无所知
  • 我也会扔掉所有并行的东西,直到你让它工作,在 parralel 中运行东西可能会使它运行得快 N 倍(N 可能 < 10)但它值得优化算法然后将并行性投入它,如果它不破坏算法
  • Tomas 还指出创建索引很慢……但查找速度很快
  • @MrDatKookerellaLtd 感谢您的输入。现在我放弃了整个并行性并保持线性。我也放弃了 Aho-Corsaick,因为它太慢了,而且我仍然需要模式匹配,所以我改用 Regex。

标签: c# algorithm search string-search file-search


【解决方案1】:

在切换到 MemoryMappedFile 并从 Aho-Corasick 返回到 Regex 之后,我得到了迄今为​​止最好的结果(已经提出模式匹配是必须的)。

仍有部分可以优化或更改,我确信这不是最快或最好的解决方案,但它没关系。

以下代码可在 30 秒内返回 25 GiB 数据的结果:

// GNU coreutil wc defined buffer size.
// Had best performance with this buffer size.
//
// Definition in wc.c:
// -------------------
// /* Size of atomic reads. */
// #define BUFFER_SIZE (16 * 1024)
//
private const int BUFFER_SIZE = 16 * 1024;

private KeyValuePair<string, SearchData[]> FindInternal(Regex[] rgx, string file)
{
    // Buffer for data segmentation.
    byte[] buffer = new byte[BUFFER_SIZE];
    // Get size of file.
    FileInfo fInfo = new FileInfo(file);
    long fSize = fInfo.Length;
    fInfo = null;

    // List of results.
    List<SearchData> results = new List<SearchData>();

    // Create MemoryMappedFile.
    string name = "mmf_" + Path.GetFileNameWithoutExtension(file);
    using (var mmf = MemoryMappedFile.CreateFromFile(
        file, FileMode.Open, name))
    {
        // Create read-only in-memory access to file data.
        using (var accessor = mmf.CreateViewStream(
            0, fSize,
            MemoryMappedFileAccess.Read))
        {
            // Store current position.
            int pos = (int)accessor.Position;
            // Check if file size is less then the 
            // default buffer size.
            int cnt = (int)(fSize - BUFFER_SIZE > 0 
                    ? BUFFER_SIZE 
                    : fSize - BUFFER_SIZE);

            // Iterate through file until end of file is reached.
            while (accessor.Position < fSize)
            {
                // Write data to buffer.
                accessor.Read(buffer, 0, cnt);
                // Update position.
                pos = (int)accessor.Position;
                // Update next buffer size.
                cnt = (int)(fSize - pos >= BUFFER_SIZE 
                    ? BUFFER_SIZE 
                    : fSize - pos);
                // Convert buffer data to string for Regex search.
                string s = Encoding.UTF8.GetString(buffer);
                // Run regex against extracted data.
                foreach (Regex r in rgx) {
                    // Get matches.
                    MatchCollection matches = r.Matches(s);
                    // Create SearchData struct to reduce memory 
                    // impact and only keep relevant data.
                    foreach (Match m in matches) {
                        SearchData sd = new SearchData();
                        // The actual matched string.
                        sd.Match = m.Value; 
                        // The index in the file.
                        sd.Index = m.Index + pos;
                        // Index to find beginning of line.
                        int nFirst = m.Index;
                        // Index to find end of line.
                        int nLast = m.Index;
                        // Go back in line until the end of the
                        // preceeding line has been found.
                        while (s[nFirst] != '
' && nFirst > 0) {
                            nFirst--;
                        }
                        // Append length of 
 (new line).
                        // Change this to 1 if you work on Unix system.
                        nFirst+=2;
                        // Go forth in line until the end of the
                        // current line has been found.
                        while (s[nLast] != '
' && nLast < s.Length-1)  {
                            nLast++;
                        }
                        // Remove length of 
 (new line).
                        // Change this to 1 if you work on Unix system.
                        nLast-=2;
                        // Store whole line in SearchData struct.
                        sd.Line = s.Substring(nFirst, nLast - nFirst);
                        // Add result.
                        results.Add(sd);
                    }
                }
            }
        }
    }
    return new KeyValuePair<string, SearchData[]>(file, results.ToArray());
}


public List<KeyValuePair<string, SearchData[]>> Find(params string[] search)
{
    var results = new List<KeyValuePair<string, SearchData[]>>();
    // Prepare regex objects.
    Regex[] regexes = new Regex[search.Length];
    for (int i=0; i<regexes.Length; i++) {
        regexes[i] = new Regex(search[i], RegexOptions.Compiled);                
    }

    // Get all search results.
    // Creating the Regex once and passing it
    // to the sub-routine is best as the regex
    // engine adds a lot of overhead.
    foreach (var file in _files) {
        var data = FindInternal(regexes, file);                
        results.Add(data);
    }
    return results;
}

昨天我有一个愚蠢的想法,虽然它可能会解决将文件数据转换为位图并在像素内寻找输入的问题,因为像素检查非常快。

只是为了咯咯笑......这是那个愚蠢想法的非优化测试代码:

public struct SearchData
{
    public string Line;
    public string Search;
    public int Row;

    public SearchData(string l, string s, int r) {
        Line    = l;
        Search  = s;
        Row     = r;
    }
}


internal static class FileToImage
{
    public static unsafe SearchData[] FindText(string search, Bitmap bmp)
    {
        byte[] buffer = Encoding.ASCII.GetBytes(search);

        BitmapData data = bmp.LockBits(
            new Rectangle(0, 0, bmp.Width, bmp.Height),
            ImageLockMode.ReadOnly, bmp.PixelFormat);

        List<SearchData> results = new List<SearchData>();
        int bpp = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8;
        byte* ptFirst = (byte*)data.Scan0;
        byte firstHit = buffer[0];
        bool isFound = false;
        for (int y=0; y<data.Height; y++) {
            byte* ptStride = ptFirst + (y * data.Stride);
            for (int x=0; x<data.Stride; x++) {
                if (firstHit == ptStride[x]) {
                    byte[] temp = new byte[buffer.Length];                       
                    if (buffer.Length < data.Stride-x) {
                        int ret = 0;                            
                        for (int n=0, xx=x; n<buffer.Length; n++, xx++) {                             
                            if (ptStride[xx] != buffer[n]) {
                                break;
                            }
                            ret++;
                        }
                        if (ret == buffer.Length) {

                            int lineLength = 0;
                            for (int n = 0; n<data.Stride; n+=bpp) {
                                if (ptStride[n+2] == 255 &&
                                    ptStride[n+1] == 255 &&
                                    ptStride[n+0] == 255) 
                                {
                                    lineLength=n;
                                }
                            }

                            SearchData sd = new SearchData();
                            byte[] lineBytes = new byte[lineLength];
                            Marshal.Copy((IntPtr)ptStride, lineBytes, 0, lineLength);
                            sd.Search = search;
                            sd.Line = Encoding.ASCII.GetString(lineBytes);
                            sd.Row = y;
                            results.Add(sd);
                        }
                    }
                }
            }             
        }
        return results.ToArray();
        bmp.UnlockBits(data);
        return null;
    }
    

    private static unsafe Bitmap GetBitmapInternal(string[] lines, int startIndex, Bitmap bmp)
    {
        int bpp = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8;
        BitmapData data = bmp.LockBits(
            new Rectangle(0, 0, bmp.Width, bmp.Height),
            ImageLockMode.ReadWrite,
            bmp.PixelFormat);

        int index = startIndex;
        byte* ptFirst = (byte*)data.Scan0;
        int maxHeight = bmp.Height;
        if (lines.Length - startIndex < maxHeight) {
            maxHeight = lines.Length - startIndex -1;
        }
        for (int y = 0; y < maxHeight; y++) {
            byte* ptStride = ptFirst + (y * data.Stride);
            index++;
            int max = lines[index].Length;
            max += (max % bpp);
            lines[index] += new string('
猜你喜欢
  • 1970-01-01
  • 2013-09-13
  • 2018-09-07
  • 2021-03-28
  • 2012-10-07
  • 2019-10-24
  • 2011-04-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多